Skip to content

Commit e307f28

Browse files
authored
Allow FlowOptions to be passed directly to the __call__ function for interactive work (#124)
Signed-off-by: Pascal Tomecek <[email protected]>
1 parent 8b83a29 commit e307f28

File tree

3 files changed

+69
-26
lines changed

3 files changed

+69
-26
lines changed

ccflow/callable.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,17 @@ def get_evaluator(self, model: CallableModelType) -> "EvaluatorBase":
194194

195195
def __call__(self, fn):
196196
# Used for building a graph of model evaluation contexts without evaluating
197-
def get_evaluation_context(model: CallableModelType, context: ContextType, as_dict: bool = False):
197+
def get_evaluation_context(model: CallableModelType, context: ContextType, as_dict: bool = False, *, _options: Optional[FlowOptions] = None):
198198
# Create the evaluation context.
199199
# Record the options that are used, in case the evaluators want to use it,
200200
# but exclude the evaluator itself to avoid potential circular dependencies
201201
# or other difficulties with serialization/caching of the options
202-
options = FlowOptionsOverride.get_options(model, self)
202+
if _options:
203+
if not isinstance(_options, FlowOptions):
204+
_options = FlowOptions.model_validate(_options)
205+
options = _options
206+
else:
207+
options = FlowOptionsOverride.get_options(model, self)
203208
evaluator = self._get_evaluator_from_options(options)
204209
options_dict = options.model_dump(mode="python", exclude={"evaluator"}, exclude_unset=True)
205210
evaluation_context = ModelEvaluationContext(model=model, context=context, fn=fn.__name__, options=options_dict)
@@ -209,7 +214,7 @@ def get_evaluation_context(model: CallableModelType, context: ContextType, as_di
209214
return ModelEvaluationContext(model=evaluator, context=evaluation_context)
210215

211216
# The decorator implementation
212-
def wrapper(model, context=Signature.empty, **kwargs):
217+
def wrapper(model, context=Signature.empty, *, _options: Optional[FlowOptions] = None, **kwargs):
213218
if not isinstance(model, CallableModel):
214219
raise TypeError("Can only decorate methods on CallableModels with the flow decorator")
215220
if not isclass(model.context_type) or not issubclass(model.context_type, ContextBase):
@@ -239,7 +244,7 @@ def wrapper(model, context=Signature.empty, **kwargs):
239244
# In this case, we don't apply the decorator again, we just call the function on the model and context.
240245
return fn(model, context)
241246

242-
evaluation_context = get_evaluation_context(model, context, as_dict=True)
247+
evaluation_context = get_evaluation_context(model, context, as_dict=True, _options=_options)
243248
# Here we call the evaluator directly on the context, instead of calling evaluation_context()
244249
# to eliminate one level in the call stack when things go wrong/when debugging.
245250
result = evaluation_context["model"](evaluation_context["context"])

ccflow/tests/test_decorator.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from unittest import TestCase
44

55
from ccflow import CallableModel, DateContext, EvaluatorBase, Flow, FlowOptions, FlowOptionsOverride, GenericResult, ModelEvaluationContext
6-
from ccflow.evaluators import MemoryCacheEvaluator
76

87

98
class MyModel(CallableModel):
@@ -56,6 +55,13 @@ def __call__(self, context: DateContext = "0d") -> GenericResult:
5655
return context.date
5756

5857

58+
class DummyEvaluator(EvaluatorBase):
59+
return_value: GenericResult = GenericResult(value=date(2020, 1, 1))
60+
61+
def __call__(self, context: ModelEvaluationContext):
62+
return self.return_value
63+
64+
5965
class TestModelEvaluationContext(TestCase):
6066
def test_model_evaluation_context(self):
6167
model = DefaultModel()
@@ -99,25 +105,33 @@ def test_evaluation_context_options(self):
99105
model = DefaultModel()
100106
context = DateContext(date=date.today())
101107
options = FlowOptions(log_level=0)
108+
109+
evaluation_context = model.__call__.get_evaluation_context(model, context, _options=options)
110+
self.assertEqual(evaluation_context.model.log_level, 0)
111+
112+
evaluation_context = model.__call__.get_evaluation_context(model, context, _options=dict(log_level=0))
113+
self.assertEqual(evaluation_context.model.log_level, 0)
114+
102115
with FlowOptionsOverride(options=options):
103116
self.assertEqual(model.__call__.get_options(model), options)
104117
evaluation_context = model.__call__.get_evaluation_context(model, context)
105118
self.assertEqual(evaluation_context.model.log_level, 0)
106119

120+
# Make sure passing _options takes precedence over the FlowOptionsOverride context
121+
evaluation_context = model.__call__.get_evaluation_context(model, context, _options=dict(log_level=1))
122+
self.assertEqual(evaluation_context.model.log_level, 1)
123+
107124
def test_new_evaluator(self):
108-
"""Test that we can use the decorator to make an evaluation with a new evaluator."""
125+
"""Test that we can call the model easily with a new evaluator."""
109126
model = MyModel()
110-
new_evaluator = MemoryCacheEvaluator()
111-
wrapped_call = Flow.call(evaluator=new_evaluator)(MyModel.__call__)
112-
out_evaluator = wrapped_call.get_evaluator(model)
113-
self.assertEqual(out_evaluator, new_evaluator)
114-
127+
new_evaluator = DummyEvaluator()
115128
context = DateContext(date=date.today())
116-
self.assertEqual(wrapped_call(model, context), model(context))
129+
self.assertEqual(model(context), GenericResult(value=context.date))
130+
self.assertEqual(model(context, _options=dict(evaluator=new_evaluator)), new_evaluator.return_value)
117131

118132
# Now test it on foo
119-
wrapped_foo = Flow.call(evaluator=new_evaluator)(MyModel.foo)
120-
self.assertEqual(wrapped_foo(model, context), model.foo(context))
133+
self.assertEqual(model(context), GenericResult(value=context.date))
134+
self.assertEqual(model.foo(context, _options=dict(evaluator=new_evaluator)), new_evaluator.return_value)
121135

122136
def test_coercion(self):
123137
model = MyModel()

docs/wiki/Workflows.md

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,29 @@ except TypeError as e:
344344

345345
Behind the `@Flow.call` decorator is where all the magic lives, including advanced features which are not yet well tested or have not yet been implemented. However, we walk through some of the more basic functionality to give an idea of how it can be used.
346346

347-
The behavior of the `@Flow.call` decorator can be controlled in two ways:
347+
The behavior of the `@Flow.call` decorator can be controlled in several ways:
348348

349-
- by using the FlowOptionsOverride context (which allows you to scope changes to specific models, specific model types or all models)
350349
- by passing arguments to it when defining the CallableModel to customize model-specific behavior
350+
- by using the FlowOptionsOverride context (which allows you to scope changes to specific models, specific model types or all models)
351+
- by setting `options` in the `meta` attribute of the CallableModel
352+
- by passing `_options` directly to the `__call__` method
353+
354+
An example of the first one (model-specific options) is to disable validation of the result type on a particular model
355+
356+
```python
357+
from ccflow import CallableModel, Flow, GenericResult, GenericContext
358+
359+
class NoValidationModel(CallableModel):
360+
@Flow.call(validate_result=False)
361+
def __call__(self, context: GenericContext[str]) -> GenericResult[float]:
362+
return "foo"
363+
364+
model = NoValidationModel()
365+
print(model("foo"))
366+
#> foo
367+
```
351368

352-
An example of the former is to change the log level for all model evaluations:
369+
An example of the latter three is to change the log level for all model evaluations (including sub-models):
353370

354371
```python
355372
import logging
@@ -369,19 +386,26 @@ with FlowOptionsOverride(options={"log_level": logging.WARN}):
369386
#> GenericResult[list[Union[int, str]]](value=[1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz'])
370387
```
371388

372-
An example of the latter (model-specific options) is to disable validation of the result type on a particular model
389+
If there is only a need to set the options for a specific model (and not any sub-models that it may call), then the `meta` attribute or passing `_options` to the `__call__` method are better options.
390+
Setting `meta.options` is particularly useful when the model is being loaded from a configuration file, though it can be done interactively as well to persist the options with the model instance.
373391

374392
```python
375-
from ccflow import CallableModel, Flow, GenericResult, GenericContext
393+
model = FizzBuzzModel()
394+
model.meta.options = {"log_level": logging.WARN}
395+
_ = model(15)
396+
#[FizzBuzzModel]: Start evaluation of __call__ on GenericContext[int](value=15).
397+
#[FizzBuzzModel]: FizzBuzzModel(meta=MetaData(name=''), fizz='Fizz', buzz='Buzz')
398+
#[FizzBuzzModel]: End evaluation of __call__ on GenericContext[int](value=15) (time elapsed: 0:00:00.000074).
399+
```
376400

377-
class NoValidationModel(CallableModel):
378-
@Flow.call(validate_result=False)
379-
def __call__(self, context: GenericContext[str]) -> GenericResult[float]:
380-
return "foo"
401+
Most convenient for interactive work is to pass the options to the `__call__` method directly.
381402

382-
model = NoValidationModel()
383-
print(model("foo"))
384-
#> foo
403+
```python
404+
model = FizzBuzzModel()
405+
_ = model(15, _options={"log_level": logging.WARN})
406+
#[FizzBuzzModel]: Start evaluation of __call__ on GenericContext[int](value=15).
407+
#[FizzBuzzModel]: FizzBuzzModel(meta=MetaData(name=''), fizz='Fizz', buzz='Buzz')
408+
#[FizzBuzzModel]: End evaluation of __call__ on GenericContext[int](value=15) (time elapsed: 0:00:00.000072).
385409
```
386410

387411
To see a list of all the available options, you can look at the schema definition of the FlowOptions class:

0 commit comments

Comments
 (0)