@@ -143,10 +143,38 @@ def decorator(cls: ClassType) -> ClassType:
143143 return decorator
144144
145145
146+ def init (
147+ init_fn : CallableType ,
148+ ) -> CallableType :
149+ """Decorator for the workflow init method.
150+
151+ This may be used on the __init__ method of the workflow class to specify
152+ that it accepts the same workflow input arguments as the ``@workflow.run``
153+ method. It may not be used on any other method.
154+
155+ If used, the workflow will be instantiated as
156+ ``MyWorkflow(**workflow_input_args)``. If not used, the workflow will be
157+ instantiated as ``MyWorkflow()``.
158+
159+ Note that the ``@workflow.run`` method is always called as
160+ ``my_workflow.my_run_method(**workflow_input_args)``. If you use the
161+ ``@workflow.init`` decorator, the parameter list of your __init__ and
162+ ``@workflow.run`` methods must be identical.
163+
164+ Args:
165+ init_fn: The __init__function to decorate.
166+ """
167+ if init_fn .__name__ != "__init__" :
168+ raise ValueError ("@workflow.init may only be used on the __init__ method" )
169+
170+ setattr (init_fn , "__temporal_workflow_init" , True )
171+ return init_fn
172+
173+
146174def run (fn : CallableAsyncType ) -> CallableAsyncType :
147175 """Decorator for the workflow run method.
148176
149- This must be set on one and only one async method defined on the same class
177+ This must be used on one and only one async method defined on the same class
150178 as ``@workflow.defn``. This can be defined on a base class method but must
151179 then be explicitly overridden and defined on the workflow class.
152180
@@ -238,7 +266,7 @@ def signal(
238266):
239267 """Decorator for a workflow signal method.
240268
241- This is set on any async or non-async method that you wish to be called upon
269+ This is used on any async or non-async method that you wish to be called upon
242270 receiving a signal. If a function overrides one with this decorator, it too
243271 must be decorated.
244272
@@ -309,7 +337,7 @@ def query(
309337):
310338 """Decorator for a workflow query method.
311339
312- This is set on any non-async method that expects to handle a query. If a
340+ This is used on any non-async method that expects to handle a query. If a
313341 function overrides one with this decorator, it too must be decorated.
314342
315343 Query methods can only have positional parameters. Best practice for
@@ -983,7 +1011,7 @@ def update(
9831011):
9841012 """Decorator for a workflow update handler method.
9851013
986- This is set on any async or non-async method that you wish to be called upon
1014+ This is used on any async or non-async method that you wish to be called upon
9871015 receiving an update. If a function overrides one with this decorator, it too
9881016 must be decorated.
9891017
@@ -1307,13 +1335,13 @@ def _apply_to_class(
13071335 issues : List [str ] = []
13081336
13091337 # Collect run fn and all signal/query/update fns
1310- members = inspect . getmembers ( cls )
1338+ init_fn : Optional [ Callable [..., None ]] = None
13111339 run_fn : Optional [Callable [..., Awaitable [Any ]]] = None
13121340 seen_run_attr = False
13131341 signals : Dict [Optional [str ], _SignalDefinition ] = {}
13141342 queries : Dict [Optional [str ], _QueryDefinition ] = {}
13151343 updates : Dict [Optional [str ], _UpdateDefinition ] = {}
1316- for name , member in members :
1344+ for name , member in inspect . getmembers ( cls ) :
13171345 if hasattr (member , "__temporal_workflow_run" ):
13181346 seen_run_attr = True
13191347 if not _is_unbound_method_on_cls (member , cls ):
@@ -1354,6 +1382,8 @@ def _apply_to_class(
13541382 )
13551383 else :
13561384 queries [query_defn .name ] = query_defn
1385+ elif name == "__init__" and hasattr (member , "__temporal_workflow_init" ):
1386+ init_fn = member
13571387 elif isinstance (member , UpdateMethodMultiParam ):
13581388 update_defn = member ._defn
13591389 if update_defn .name in updates :
@@ -1406,9 +1436,14 @@ def _apply_to_class(
14061436
14071437 if not seen_run_attr :
14081438 issues .append ("Missing @workflow.run method" )
1409- if len (issues ) == 1 :
1410- raise ValueError (f"Invalid workflow class: { issues [0 ]} " )
1411- elif issues :
1439+ if init_fn and run_fn :
1440+ if not _parameters_identical_up_to_naming (init_fn , run_fn ):
1441+ issues .append (
1442+ "@workflow.init and @workflow.run method parameters do not match"
1443+ )
1444+ if issues :
1445+ if len (issues ) == 1 :
1446+ raise ValueError (f"Invalid workflow class: { issues [0 ]} " )
14121447 raise ValueError (
14131448 f"Invalid workflow class for { len (issues )} reasons: { ', ' .join (issues )} "
14141449 )
@@ -1444,6 +1479,19 @@ def __post_init__(self) -> None:
14441479 object .__setattr__ (self , "ret_type" , ret_type )
14451480
14461481
1482+ def _parameters_identical_up_to_naming (fn1 : Callable , fn2 : Callable ) -> bool :
1483+ """Return True if the functions have identical parameter lists, ignoring parameter names."""
1484+
1485+ def params (fn : Callable ) -> List [inspect .Parameter ]:
1486+ # Ignore name when comparing parameters (remaining fields are kind,
1487+ # default, and annotation).
1488+ return [p .replace (name = "x" ) for p in inspect .signature (fn ).parameters .values ()]
1489+
1490+ # We require that any type annotations present match exactly; i.e. we do
1491+ # not support any notion of subtype compatibility.
1492+ return params (fn1 ) == params (fn2 )
1493+
1494+
14471495# Async safe version of partial
14481496def _bind_method (obj : Any , fn : Callable [..., Any ]) -> Callable [..., Any ]:
14491497 # Curry instance on the definition function since that represents an
0 commit comments