55
66These helpers resolve the annotation of the *input* parameter of an
77orchestrator, activity, or entity function so that inbound payloads can be
8- reconstructed into the annotated custom type (a dataclass or a type exposing a
9- ``from_json()`` classmethod) without the caller having to pass an explicit type.
8+ reconstructed into the annotated custom type without the caller having to pass
9+ an explicit type.
1010
1111Discovery is intentionally conservative: it only returns an annotation when the
12- target is a *reconstructable* custom type (a dataclass, a ``from_json()``-capable
13- type, or an ``Optional`` / ``list`` wrapping one). Primitive and unknown
14- annotations resolve to ``None`` so that existing payloads are passed through
15- unchanged -- inbound type discovery never invokes an arbitrary constructor on
16- untrusted data, and never alters the value for builtins.
12+ active :class:`~durabletask.serialization.DataConverter` reports it as
13+ *reconstructable* via :meth:`DataConverter.is_reconstructable`. The default
14+ converter recognizes dataclasses, ``from_json()``-capable types, and ``Optional``
15+ / ``list`` hints wrapping them; a custom converter can recognize its own types
16+ (e.g. ``pydantic.BaseModel``). Primitive and unknown annotations resolve to
17+ ``None`` so that existing payloads are passed through unchanged -- inbound type
18+ discovery never invokes an arbitrary constructor on untrusted data, and never
19+ alters the value for builtins.
1720
1821All public helpers swallow exceptions and return ``None`` on failure; the caller
1922treats ``None`` as "no type information available" and uses the raw payload.
2023"""
2124
2225from __future__ import annotations
2326
24- import collections .abc
25- import dataclasses
2627import functools
2728import inspect
28- import types
2929import typing
30- from typing import Any , Callable , cast
30+ from typing import Any , Callable
3131
32+ from durabletask .serialization import DEFAULT_DATA_CONVERTER , DataConverter
3233
33- def is_reconstructable (annotation : Any ) -> bool :
34- """Return True if ``annotation`` names a custom type we can rebuild.
3534
36- Reconstructable targets are dataclasses, types exposing a callable
37- ``from_json``, and ``Optional`` / ``list`` hints wrapping such types.
38- Builtins (``int``, ``str``, ``dict``, ...) and unknown annotations are not
39- reconstructable and resolve to ``False``.
40- """
41- origin = typing .get_origin (annotation )
42- if origin is not None :
43- args = typing .get_args (annotation )
44- if origin is typing .Union or origin is types .UnionType :
45- return any (
46- is_reconstructable (a ) for a in args if a is not type (None )
47- )
48- if origin in (list , collections .abc .Sequence ):
49- return any (is_reconstructable (a ) for a in args )
50- return False
51- if not isinstance (annotation , type ):
52- return False
53- if dataclasses .is_dataclass (annotation ):
54- return True
55- return callable (getattr (cast (Any , annotation ), "from_json" , None ))
35+ def _resolve_converter (converter : DataConverter | None ) -> DataConverter :
36+ """Return the supplied converter, or the shared default when ``None``."""
37+ return converter if converter is not None else DEFAULT_DATA_CONVERTER
5638
5739
5840# Bounded so a worker that registers dynamically-created functions or closures
@@ -72,14 +54,15 @@ def _resolved_hints(fn: Callable[..., Any]) -> dict[str, Any] | None:
7254 return None
7355
7456
75- def _input_annotation (fn : Callable [..., Any ], position : int ) -> Any | None :
57+ def _input_annotation (fn : Callable [..., Any ], position : int ,
58+ converter : DataConverter | None = None ) -> Any | None :
7659 """Return the resolved annotation of the positional parameter at ``position``.
7760
7861 ``position`` is the zero-based index among positional parameters (so the
7962 ``input`` parameter of a ``(ctx, input)`` function is at position 1, and the
8063 ``input`` parameter of an unbound ``(self, input)`` entity method is also at
8164 position 1). Returns ``None`` when the parameter is absent, unannotated, or
82- its annotation is not a reconstructable custom type .
65+ its annotation is not reconstructable by ``converter`` .
8366 """
8467 try :
8568 sig = inspect .signature (fn )
@@ -105,27 +88,29 @@ def _input_annotation(fn: Callable[..., Any], position: int) -> Any | None:
10588
10689 if annotation is inspect .Parameter .empty or annotation is Any :
10790 return None
108- return annotation if is_reconstructable (annotation ) else None
91+ return annotation if _resolve_converter ( converter ). is_reconstructable (annotation ) else None
10992
11093
111- def orchestrator_input_type (fn : Callable [..., Any ]) -> Any | None :
94+ def orchestrator_input_type (fn : Callable [..., Any ],
95+ converter : DataConverter | None = None ) -> Any | None :
11296 """Discover the input type of an orchestrator function ``(ctx, input)``."""
113- return _input_annotation (fn , 1 )
97+ return _input_annotation (fn , 1 , converter )
11498
11599
116- def activity_input_type (fn : Callable [..., Any ]) -> Any | None :
100+ def activity_input_type (fn : Callable [..., Any ],
101+ converter : DataConverter | None = None ) -> Any | None :
117102 """Discover the input type of an activity function ``(ctx, input)``."""
118- return _input_annotation (fn , 1 )
103+ return _input_annotation (fn , 1 , converter )
119104
120105
121- def activity_output_type (fn : Any ) -> Any | None :
106+ def activity_output_type (fn : Any , converter : DataConverter | None = None ) -> Any | None :
122107 """Discover the return type of an activity function.
123108
124- Returns the resolved return annotation when it names a reconstructable
125- custom type (a dataclass or a ``from_json()``-capable type, optionally
126- wrapped in ``Optional`` / ``list``). Returns ``None`` for plain callables
127- that are not annotated with such a type, for string activity names, or when
128- the annotation cannot be resolved.
109+ Returns the resolved return annotation when ``converter`` reports it as
110+ reconstructable (the default converter recognizes a dataclass or a
111+ ``from_json()``-capable type, optionally wrapped in ``Optional`` / ``list``).
112+ Returns ``None`` for plain callables that are not annotated with such a type,
113+ for string activity names, or when the annotation cannot be resolved.
129114 """
130115 if not callable (fn ):
131116 return None
@@ -144,10 +129,11 @@ def activity_output_type(fn: Any) -> Any | None:
144129
145130 if annotation is inspect .Signature .empty or annotation is Any or annotation is None :
146131 return None
147- return annotation if is_reconstructable (annotation ) else None
132+ return annotation if _resolve_converter ( converter ). is_reconstructable (annotation ) else None
148133
149134
150- def entity_input_type (fn : Any , operation : str ) -> Any | None :
135+ def entity_input_type (fn : Any , operation : str ,
136+ converter : DataConverter | None = None ) -> Any | None :
151137 """Discover the input type of an entity operation.
152138
153139 For class-based entities (a ``DurableEntity`` subclass) the operation is a
@@ -160,5 +146,5 @@ def entity_input_type(fn: Any, operation: str) -> Any | None:
160146 if method is None or not callable (method ):
161147 return None
162148 # Unbound method includes ``self`` at position 0, so ``input`` is at 1.
163- return _input_annotation (method , 1 )
164- return _input_annotation (fn , 1 )
149+ return _input_annotation (method , 1 , converter )
150+ return _input_annotation (fn , 1 , converter )
0 commit comments