Skip to content

Commit 7ea7478

Browse files
committed
Revert JSON stuff to base
1 parent 2dbff23 commit 7ea7478

3 files changed

Lines changed: 18 additions & 95 deletions

File tree

durabletask/internal/client_helpers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,16 +205,14 @@ def build_signal_entity_req(
205205
entity_instance_id: EntityInstanceId,
206206
operation_name: str,
207207
input: Any | None = None,
208-
signal_time: datetime | None = None,
209208
data_converter: DataConverter | None = None) -> pb.SignalEntityRequest:
210209
"""Build a SignalEntityRequest for signaling an entity."""
211-
scheduled_time = helpers.new_timestamp(signal_time) if signal_time is not None else None
212210
return pb.SignalEntityRequest(
213211
instanceId=str(entity_instance_id),
214212
name=operation_name,
215213
input=helpers.get_string_value(_serialize(input, data_converter)),
216214
requestId=str(uuid.uuid4()),
217-
scheduledTime=scheduled_time,
215+
scheduledTime=None,
218216
parentTraceContext=None,
219217
requestTime=helpers.new_timestamp(datetime.now(timezone.utc))
220218
)

durabletask/internal/helpers.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,16 +255,11 @@ def new_signal_entity_action(id: int,
255255
entity_id: EntityInstanceId,
256256
operation: str,
257257
encoded_input: str | None,
258-
request_id: str,
259-
scheduled_time: datetime | None = None) -> pb.OrchestratorAction:
260-
scheduled_timestamp: timestamp_pb2.Timestamp | None = None
261-
if scheduled_time is not None:
262-
scheduled_timestamp = timestamp_pb2.Timestamp()
263-
scheduled_timestamp.FromDatetime(scheduled_time)
258+
request_id: str) -> pb.OrchestratorAction:
264259
return pb.OrchestratorAction(id=id, sendEntityMessage=pb.SendEntityMessageAction(entityOperationSignaled=pb.EntityOperationSignaledEvent(
265260
requestId=request_id,
266261
operation=operation,
267-
scheduledTime=scheduled_timestamp,
262+
scheduledTime=None,
268263
input=get_string_value(encoded_input),
269264
targetInstanceId=get_string_value(str(entity_id)),
270265
)))

durabletask/internal/json_codec.py

Lines changed: 15 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from __future__ import annotations
1515

1616
import dataclasses
17-
import inspect
1817
import json
1918
import types
2019
import typing
@@ -48,8 +47,7 @@ def to_json(obj: Any) -> str:
4847
) from e
4948

5049

51-
def from_json(json_str: str | bytes | bytearray, expected_type: type | None = None,
52-
converter: Any | None = None) -> Any:
50+
def from_json(json_str: str | bytes | bytearray, expected_type: type | None = None) -> Any:
5351
"""Deserialize a JSON string, optionally coercing the result to a type.
5452
5553
When ``expected_type`` is ``None`` (the default) the raw parsed JSON is
@@ -64,15 +62,11 @@ def from_json(json_str: str | bytes | bytearray, expected_type: type | None = No
6462
classmethod are reconstructed via that hook, and ``Optional``/``Union`` and
6563
``list`` type hints are honored recursively. The destination type is always
6664
supplied by the caller; it is never read from the payload.
67-
68-
``converter`` is the active :class:`~durabletask.serialization.DataConverter`.
69-
It is forwarded to ``from_json`` hooks that opt in (see :func:`coerce_to_type`)
70-
so they can reconstruct nested typed values via ``converter.coerce(...)``.
7165
"""
7266
if expected_type is None:
7367
return json.loads(json_str, object_hook=_legacy_object_hook)
7468
raw = json.loads(json_str, object_hook=_strip_legacy_marker)
75-
return coerce_to_type(raw, expected_type, converter)
69+
return coerce_to_type(raw, expected_type)
7670

7771

7872
def _encode_custom_object(o: Any) -> Any:
@@ -82,31 +76,18 @@ def _encode_custom_object(o: Any) -> Any:
8276
namedtuples are handled natively by the encoder (serialized as JSON arrays)
8377
and never reach this hook.
8478
"""
79+
if dataclasses.is_dataclass(o) and not isinstance(o, type):
80+
return dataclasses.asdict(o)
81+
if isinstance(o, SimpleNamespace):
82+
return vars(o)
8583
# Custom objects may opt in via a ``to_json`` hook. It is resolved off the
8684
# type and called with the instance (``type(o).to_json(o)``) so that both
8785
# instance methods and ``@staticmethod`` hooks work -- matching the calling
8886
# convention used by ``azure-functions-durable``. The hook returns a
8987
# JSON-serializable value (a structure or a string), not a JSON document.
90-
#
91-
# The hook is checked before the dataclass / ``SimpleNamespace`` branches so
92-
# a type may override the default structural encoding -- mirroring the read
93-
# path, where :func:`coerce_to_type` consults ``from_json`` before its
94-
# dataclass branch. This matters for dataclasses whose fields are not
95-
# JSON-native (e.g. ``timedelta`` / ``datetime``), which ``asdict`` alone
96-
# cannot serialize.
9788
to_json_hook = getattr(cast(Any, type(o)), "to_json", None)
9889
if callable(to_json_hook):
9990
return to_json_hook(o)
100-
if dataclasses.is_dataclass(o) and not isinstance(o, type):
101-
# Shallow-convert to a dict whose *values are the original field objects*
102-
# (unlike ``dataclasses.asdict``, which deep-recurses and would convert a
103-
# nested dataclass via ``asdict`` -- bypassing that child's ``to_json``
104-
# hook). ``json.dumps`` then recurses into each value and re-enters this
105-
# hook for any nested custom object, so nested ``to_json`` hooks fire at
106-
# every depth (including inside lists/dicts).
107-
return {f.name: getattr(o, f.name) for f in dataclasses.fields(o)}
108-
if isinstance(o, SimpleNamespace):
109-
return vars(o)
11091
# This will raise a TypeError describing the unsupported type.
11192
raise TypeError(f"Object of type '{type(o).__name__}' is not JSON serializable")
11293

@@ -124,31 +105,20 @@ def _strip_legacy_marker(d: dict[str, Any]) -> dict[str, Any]:
124105
return d
125106

126107

127-
def coerce_to_type(value: Any, expected_type: Any, converter: Any | None = None) -> Any:
108+
def coerce_to_type(value: Any, expected_type: Any) -> Any:
128109
"""Coerce an already-parsed JSON value to ``expected_type``.
129110
130111
Handles ``None``/``Optional``/``Union`` and ``list`` type hints recursively,
131112
types exposing a ``from_json()`` classmethod, and dataclasses (including
132113
nested dataclass fields). The destination type is always caller-supplied and
133114
never derived from the payload, keeping deserialization secure.
134-
135-
``converter`` is the active :class:`~durabletask.serialization.DataConverter`.
136-
A ``from_json`` hook may opt in to receiving it (by accepting a second
137-
positional parameter) and delegate nested reconstruction back to the
138-
converter, e.g. ``converter.coerce(child, ChildType)``. This keeps hooks free
139-
of manual nested deserialization and routes children through the same policy.
140115
"""
141116
if expected_type is None or value is None:
142117
return value
143118

144-
# ``Any`` imposes no constraint -- and ``isinstance(x, Any)`` raises -- so
145-
# short-circuit before any type inspection below.
146-
if expected_type is typing.Any:
147-
return value
148-
149119
origin = typing.get_origin(expected_type)
150120
if origin is not None:
151-
return _coerce_generic(value, expected_type, origin, converter)
121+
return _coerce_generic(value, expected_type, origin)
152122

153123
if not isinstance(expected_type, type):
154124
# Not a concrete, instantiable type (e.g. a typing special form we don't
@@ -160,10 +130,10 @@ def coerce_to_type(value: Any, expected_type: Any, converter: Any | None = None)
160130

161131
from_json_hook = getattr(expected_type, "from_json", None)
162132
if callable(from_json_hook):
163-
return _invoke_from_json(from_json_hook, value, converter)
133+
return from_json_hook(value)
164134

165135
if dataclasses.is_dataclass(expected_type) and isinstance(value, dict):
166-
return _build_dataclass(expected_type, cast(dict[str, Any], value), converter)
136+
return _build_dataclass(expected_type, cast(dict[str, Any], value))
167137

168138
type_ctor = cast(Any, expected_type)
169139
try:
@@ -176,69 +146,29 @@ def coerce_to_type(value: Any, expected_type: Any, converter: Any | None = None)
176146
) from e
177147

178148

179-
def _invoke_from_json(from_json_hook: Any, value: Any, converter: Any | None) -> Any:
180-
"""Invoke a ``from_json`` hook, passing the converter if the hook accepts it.
181-
182-
Hooks may be declared as ``from_json(cls, value)`` (the original contract) or
183-
``from_json(cls, value, converter)`` to opt into managed nested
184-
reconstruction. Arity is detected from the bound hook's signature, so the
185-
extra parameter is fully backwards compatible. When a converter-aware hook is
186-
found but no converter was threaded (e.g. a direct ``json_codec`` call), the
187-
shared default converter is resolved lazily so the hook always receives one.
188-
"""
189-
wants_converter = False
190-
try:
191-
params = [
192-
p for p in inspect.signature(from_json_hook).parameters.values()
193-
if p.kind in (inspect.Parameter.POSITIONAL_ONLY,
194-
inspect.Parameter.POSITIONAL_OR_KEYWORD)
195-
]
196-
wants_converter = len(params) >= 2
197-
except (TypeError, ValueError):
198-
wants_converter = False
199-
200-
if wants_converter:
201-
if converter is None:
202-
converter = _default_converter()
203-
return from_json_hook(value, converter)
204-
return from_json_hook(value)
205-
206-
207-
def _default_converter() -> Any:
208-
# Lazy import to avoid a load-time cycle: ``serialization`` imports this
209-
# module at import time, but by the time a hook actually runs both modules
210-
# are fully initialized.
211-
from durabletask.serialization import DEFAULT_DATA_CONVERTER
212-
return DEFAULT_DATA_CONVERTER
213-
214-
215-
def _coerce_generic(value: Any, expected_type: Any, origin: Any, converter: Any | None = None) -> Any:
149+
def _coerce_generic(value: Any, expected_type: Any, origin: Any) -> Any:
216150
args = typing.get_args(expected_type)
217151
if origin is typing.Union or origin is types.UnionType:
218152
# If the value already matches a member type, keep it as-is.
219153
non_none = [a for a in args if a is not type(None)]
220154
for arg in non_none:
221-
# An ``Any`` member imposes no constraint (and ``isinstance(x, Any)``
222-
# raises), so the value is acceptable as-is.
223-
if arg is typing.Any:
224-
return value
225155
if isinstance(arg, type) and isinstance(value, arg):
226156
return value
227157
# ``Optional[T]`` (exactly one non-None member): coerce to that member.
228158
# For a genuine multi-member ``Union`` where the value matched none of
229159
# the members, leave it untouched rather than guessing the first arg --
230160
# forcing a coercion there can silently mis-construct the wrong type.
231161
if len(non_none) == 1:
232-
return coerce_to_type(value, non_none[0], converter)
162+
return coerce_to_type(value, non_none[0])
233163
return value
234164
if origin in (list, Sequence) and isinstance(value, list):
235165
elem_type = args[0] if args else None
236-
return [coerce_to_type(item, elem_type, converter) for item in cast(list[Any], value)]
166+
return [coerce_to_type(item, elem_type) for item in cast(list[Any], value)]
237167
# Other generics (dict, tuple, ...) are returned as parsed JSON.
238168
return value
239169

240170

241-
def _build_dataclass(cls: Any, data: dict[str, Any], converter: Any | None = None) -> Any:
171+
def _build_dataclass(cls: Any, data: dict[str, Any]) -> Any:
242172
"""Construct a dataclass from its dict payload, recursing into typed fields."""
243173
try:
244174
hints = typing.get_type_hints(cls)
@@ -249,5 +179,5 @@ def _build_dataclass(cls: Any, data: dict[str, Any], converter: Any | None = Non
249179
if field.name not in data:
250180
continue
251181
field_type = hints.get(field.name)
252-
kwargs[field.name] = coerce_to_type(data[field.name], field_type, converter)
182+
kwargs[field.name] = coerce_to_type(data[field.name], field_type)
253183
return cls(**kwargs)

0 commit comments

Comments
 (0)