Skip to content

Commit 95119e3

Browse files
committed
Remove dynamic context, add option to Flow.call
Signed-off-by: Nijat Khanbabayev <nijat.khanbabayev@cubistsystematic.com>
1 parent 6f71e38 commit 95119e3

File tree

4 files changed

+180
-338
lines changed

4 files changed

+180
-338
lines changed

ccflow/callable.py

Lines changed: 84 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import abc
1515
import inspect
1616
import logging
17-
from functools import lru_cache, partial, wraps
17+
from functools import lru_cache, wraps
1818
from inspect import Signature, isclass, signature
1919
from typing import Any, Callable, ClassVar, Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union, get_args, get_origin
2020

@@ -46,7 +46,6 @@
4646
"EvaluatorBase",
4747
"Evaluator",
4848
"WrapperModel",
49-
"dynamic_context",
5049
)
5150

5251
log = logging.getLogger(__name__)
@@ -272,22 +271,22 @@ def wrapper(model, context=Signature.empty, *, _options: Optional[FlowOptions] =
272271
if not isinstance(model, CallableModel):
273272
raise TypeError(f"Can only decorate methods on CallableModels (not {type(model)}) with the flow decorator.")
274273

275-
# Check if this is a dynamic_context decorated method
276-
has_dynamic_context = hasattr(fn, "__dynamic_context__")
277-
if has_dynamic_context:
278-
method_context_type = fn.__dynamic_context__
274+
# Check if this is an auto_context decorated method
275+
has_auto_context = hasattr(fn, "__auto_context__")
276+
if has_auto_context:
277+
method_context_type = fn.__auto_context__
279278
else:
280279
method_context_type = model.context_type
281280

282-
# Validate context type (skip for dynamic contexts which are always valid ContextBase subclasses)
283-
if not has_dynamic_context:
281+
# Validate context type (skip for auto contexts which are always valid ContextBase subclasses)
282+
if not has_auto_context:
284283
if (not isclass(model.context_type) or not issubclass(model.context_type, ContextBase)) and not (
285284
get_origin(model.context_type) is Union and type(None) in get_args(model.context_type)
286285
):
287286
raise TypeError(f"Context type {model.context_type} must be a subclass of ContextBase")
288287

289-
# Validate result type - use __result_type__ for dynamic contexts if available
290-
if has_dynamic_context and hasattr(fn, "__result_type__"):
288+
# Validate result type - use __result_type__ for auto contexts if available
289+
if has_auto_context and hasattr(fn, "__result_type__"):
291290
method_result_type = fn.__result_type__
292291
else:
293292
method_result_type = model.result_type
@@ -334,9 +333,9 @@ def wrapper(model, context=Signature.empty, *, _options: Optional[FlowOptions] =
334333
wrap.get_options = self.get_options
335334
wrap.get_evaluation_context = get_evaluation_context
336335

337-
# Preserve dynamic context attributes for introspection
338-
if hasattr(fn, "__dynamic_context__"):
339-
wrap.__dynamic_context__ = fn.__dynamic_context__
336+
# Preserve auto context attributes for introspection
337+
if hasattr(fn, "__auto_context__"):
338+
wrap.__auto_context__ = fn.__auto_context__
340339
if hasattr(fn, "__result_type__"):
341340
wrap.__result_type__ = fn.__result_type__
342341

@@ -418,7 +417,58 @@ def __exit__(self, exc_type, exc_value, exc_tb):
418417
class Flow(PydanticBaseModel):
419418
@staticmethod
420419
def call(*args, **kwargs):
421-
"""Decorator for methods on callable models"""
420+
"""Decorator for methods on callable models.
421+
422+
Args:
423+
auto_context: Controls automatic context class generation from the function
424+
signature. Accepts three types of values:
425+
- False (default): No auto-generation, use traditional context parameter
426+
- True: Auto-generate context class with no parent
427+
- ContextBase subclass: Auto-generate context class inheriting from this parent
428+
**kwargs: Additional FlowOptions parameters (log_level, verbose, validate_result,
429+
cacheable, evaluator, volatile).
430+
431+
Basic Example:
432+
class MyModel(CallableModel):
433+
@Flow.call
434+
def __call__(self, context: MyContext) -> MyResult:
435+
return MyResult(value=context.x)
436+
437+
Auto Context Example:
438+
class MyModel(CallableModel):
439+
@Flow.call(auto_context=True)
440+
def __call__(self, *, x: int, y: str = "default") -> MyResult:
441+
return MyResult(value=f"{x}-{y}")
442+
443+
model = MyModel()
444+
model(x=42) # Call with kwargs directly
445+
446+
With Parent Context:
447+
class MyModel(CallableModel):
448+
@Flow.call(auto_context=DateContext)
449+
def __call__(self, *, date: date, extra: int = 0) -> MyResult:
450+
return MyResult(value=date.day + extra)
451+
452+
# The generated context inherits from DateContext, so it's compatible
453+
# with infrastructure expecting DateContext instances.
454+
"""
455+
# Extract auto_context option (not part of FlowOptions)
456+
# Can be: False, True, or a ContextBase subclass
457+
auto_context = kwargs.pop("auto_context", False)
458+
459+
# Determine if auto_context is enabled and extract parent class if provided
460+
if auto_context is False:
461+
auto_context_enabled = False
462+
context_parent = None
463+
elif auto_context is True:
464+
auto_context_enabled = True
465+
context_parent = None
466+
elif isclass(auto_context) and issubclass(auto_context, ContextBase):
467+
auto_context_enabled = True
468+
context_parent = auto_context
469+
else:
470+
raise TypeError(f"auto_context must be False, True, or a ContextBase subclass, got {auto_context!r}")
471+
422472
if len(args) == 1 and callable(args[0]):
423473
# No arguments to decorator, this is the decorator
424474
fn = args[0]
@@ -427,6 +477,14 @@ def call(*args, **kwargs):
427477
else:
428478
# Arguments to decorator, this is just returning the decorator
429479
# Note that the code below is executed only once
480+
if auto_context_enabled:
481+
# Return a decorator that first applies auto_context, then FlowOptions
482+
def auto_context_decorator(fn):
483+
wrapped = _apply_auto_context(fn, parent=context_parent)
484+
# FlowOptions.__call__ already applies wraps, so we just return its result
485+
return FlowOptions(**kwargs)(wrapped)
486+
487+
return auto_context_decorator
430488
return FlowOptions(**kwargs)
431489

432490
@staticmethod
@@ -444,81 +502,6 @@ def deps(*args, **kwargs):
444502
# Note that the code below is executed only once
445503
return FlowOptionsDeps(**kwargs)
446504

447-
@staticmethod
448-
def dynamic_call(*args, **kwargs):
449-
"""Decorator that combines @Flow.call with dynamic context creation.
450-
451-
Instead of defining a separate context class, this decorator creates one
452-
automatically from the function signature. The method can then be called
453-
with keyword arguments directly.
454-
455-
Basic Example:
456-
class MyModel(CallableModel):
457-
@Flow.dynamic_call
458-
def __call__(self, *, date: date, region: str = "US") -> MyResult:
459-
return MyResult(value=f"{date}-{region}")
460-
461-
model = MyModel()
462-
model(date=date.today()) # Uses default region="US"
463-
model(date=date.today(), region="EU") # Override default
464-
465-
With Parent Context:
466-
class MyModel(CallableModel):
467-
@Flow.dynamic_call(parent=DateContext)
468-
def __call__(self, *, date: date, extra: int = 0) -> MyResult:
469-
return MyResult(value=date.day + extra)
470-
471-
# Parent fields (date) must be included in the function signature.
472-
# This is useful for integrating with existing infrastructure that
473-
# expects specific context types.
474-
475-
Args:
476-
*args: The decorated function when used without parentheses
477-
**kwargs: Combined options for FlowOptions and dynamic_context:
478-
479-
Dynamic context options:
480-
parent: Parent context class to inherit from. All parent fields
481-
must appear in the function signature.
482-
483-
FlowOptions (passed through to @Flow.call):
484-
log_level: Logging level for evaluation (default: DEBUG)
485-
verbose: Use verbose logging (default: True)
486-
validate_result: Validate return against result_type (default: True)
487-
cacheable: Allow result caching (default: False)
488-
evaluator: Custom evaluator instance
489-
490-
Returns:
491-
A decorated method that accepts keyword arguments matching the signature.
492-
493-
Notes:
494-
- All parameters (except 'self') must have type annotations
495-
- Use keyword-only parameters (after *) for cleaner signatures
496-
- The generated context class is accessible via method.__dynamic_context__
497-
- The return type is accessible via method.__result_type__
498-
499-
See Also:
500-
dynamic_context: The underlying decorator for context creation
501-
Flow.call: The underlying decorator for flow evaluation
502-
"""
503-
# Import here to avoid circular import at module level
504-
from ccflow.callable import dynamic_context
505-
506-
# Extract dynamic_context-specific options
507-
parent = kwargs.pop("parent", None)
508-
509-
if len(args) == 1 and callable(args[0]):
510-
# No arguments to decorator (@Flow.dynamic_call)
511-
fn = args[0]
512-
wrapped = dynamic_context(fn, parent=parent)
513-
return Flow.call(wrapped)
514-
else:
515-
# Arguments to decorator (@Flow.dynamic_call(...))
516-
def decorator(fn):
517-
wrapped = dynamic_context(fn, parent=parent)
518-
return Flow.call(**kwargs)(wrapped)
519-
520-
return decorator
521-
522505

523506
# *****************************************************************************
524507
# Define "Evaluators" and associated types
@@ -859,30 +842,29 @@ def _validate_callable_model_generic_type(cls, m, handler, info):
859842

860843

861844
# *****************************************************************************
862-
# Dynamic Context Decorator
845+
# Auto Context (internal helper for Flow.call(auto_context=True))
863846
# *****************************************************************************
864847

865848

866-
def dynamic_context(func: Callable = None, *, parent: Type[ContextBase] = None) -> Callable:
867-
"""Decorator that creates a dynamic context class from function parameters.
849+
def _apply_auto_context(func: Callable, *, parent: Type[ContextBase] = None) -> Callable:
850+
"""Internal function that creates an auto context class from function parameters.
868851
869-
This decorator extracts the parameters from a function signature and creates
870-
a dynamic ContextBase subclass whose fields correspond to those parameters.
852+
This function extracts the parameters from a function signature and creates
853+
a ContextBase subclass whose fields correspond to those parameters.
871854
The decorated function is then wrapped to accept the context object and
872855
unpack it into keyword arguments.
873856
857+
Used internally by Flow.call(auto_context=...).
858+
874859
Example:
875860
class MyCallable(CallableModel):
876-
@Flow.dynamic_call # or @Flow.call @dynamic_context
861+
@Flow.call(auto_context=True)
877862
def __call__(self, *, x: int, y: str = "default") -> GenericResult:
878863
return GenericResult(value=f"{x}-{y}")
879864
880865
model = MyCallable()
881866
model(x=42, y="hello") # Works with kwargs
882867
"""
883-
if func is None:
884-
return partial(dynamic_context, parent=parent)
885-
886868
sig = signature(func)
887869
base_class = parent or ContextBase
888870

@@ -902,8 +884,8 @@ def __call__(self, *, x: int, y: str = "default") -> GenericResult:
902884
default = ... if param.default is inspect.Parameter.empty else param.default
903885
fields[name] = (param.annotation, default)
904886

905-
# Create dynamic context class
906-
dyn_context = create_ccflow_model(f"{func.__qualname__}_DynamicContext", __base__=base_class, **fields)
887+
# Create auto context class
888+
auto_context_class = create_ccflow_model(f"{func.__qualname__}_AutoContext", __base__=base_class, **fields)
907889

908890
@wraps(func)
909891
def wrapper(self, context):
@@ -914,10 +896,10 @@ def wrapper(self, context):
914896
wrapper.__signature__ = inspect.Signature(
915897
parameters=[
916898
inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD),
917-
inspect.Parameter("context", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=dyn_context),
899+
inspect.Parameter("context", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=auto_context_class),
918900
],
919901
return_annotation=sig.return_annotation,
920902
)
921-
wrapper.__dynamic_context__ = dyn_context
903+
wrapper.__auto_context__ = auto_context_class
922904
wrapper.__result_type__ = sig.return_annotation
923905
return wrapper

0 commit comments

Comments
 (0)