Skip to content

Commit a98ccc0

Browse files
committed
No more silent fallbacks to JsonDataConverter
1 parent 8db4510 commit a98ccc0

10 files changed

Lines changed: 68 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,23 @@ DEPRECATED
6565
`JsonDataConverter`) instead. The functions continue to work for backwards
6666
compatibility.
6767

68-
BREAKING CHANGES (type-level only — no runtime impact for typical users)
68+
BREAKING CHANGES (no runtime impact for typical users)
6969

70-
These changes do not alter runtime behavior, but because the package ships
71-
`py.typed`, consumers running strict type checkers (pyright/mypy) — or
72-
subclassing the public abstract types — may need to update their code:
70+
Most of these are type-level only: because the package ships `py.typed`,
71+
consumers running strict type checkers (pyright/mypy) — or subclassing the
72+
public abstract types — may need to update their code. The constructor change
73+
below also affects callers who *directly* construct the named classes, which is
74+
uncommon since they are normally handed to you by the SDK.
7375

7476
- `OrchestrationContext.call_activity`, `call_sub_orchestrator`, `call_entity`,
7577
and `wait_for_external_event` gained new keyword-only parameters
7678
(`return_type` / `data_type`). Subclasses overriding these methods should add
7779
the parameter to match the base signature.
80+
- `EntityContext` and `EntityMetadata` (and its `from_entity_metadata` /
81+
`from_entity_response` factories) now require a `data_converter` argument.
82+
These objects are normally constructed by the SDK — you receive an
83+
`EntityContext` in an entity function and an `EntityMetadata` from the client —
84+
so this only affects code that constructs them directly.
7885

7986
## v1.6.0
8087

durabletask-azuremanaged/durabletask/azuremanaged/client.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
)
2121
import durabletask.internal.shared as shared
2222
from durabletask.payload.store import PayloadStore
23+
from durabletask.serialization import DataConverter
2324

2425

2526
# Client class used for Durable Task Scheduler (DTS)
@@ -35,6 +36,7 @@ def __init__(self, *,
3536
resiliency_options: GrpcClientResiliencyOptions | None = None,
3637
default_version: str | None = None,
3738
payload_store: PayloadStore | None = None,
39+
data_converter: DataConverter | None = None,
3840
log_handler: logging.Handler | None = None,
3941
log_formatter: logging.Formatter | None = None):
4042

@@ -59,7 +61,8 @@ def __init__(self, *,
5961
channel_options=channel_options,
6062
resiliency_options=resiliency_options,
6163
default_version=default_version,
62-
payload_store=payload_store)
64+
payload_store=payload_store,
65+
data_converter=data_converter)
6366

6467

6568
# Async client class used for Durable Task Scheduler (DTS)
@@ -113,6 +116,7 @@ def __init__(self, *,
113116
resiliency_options: GrpcClientResiliencyOptions | None = None,
114117
default_version: str | None = None,
115118
payload_store: PayloadStore | None = None,
119+
data_converter: DataConverter | None = None,
116120
log_handler: logging.Handler | None = None,
117121
log_formatter: logging.Formatter | None = None):
118122

@@ -137,4 +141,5 @@ def __init__(self, *,
137141
channel_options=channel_options,
138142
resiliency_options=resiliency_options,
139143
default_version=default_version,
140-
payload_store=payload_store)
144+
payload_store=payload_store,
145+
data_converter=data_converter)

durabletask-azuremanaged/durabletask/azuremanaged/worker.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919
import durabletask.internal.shared as shared
2020
from durabletask.payload.store import PayloadStore
21+
from durabletask.serialization import DataConverter
2122
from durabletask.worker import ConcurrencyOptions, TaskHubGrpcWorker
2223

2324

@@ -81,6 +82,7 @@ def __init__(self, *,
8182
resiliency_options: GrpcWorkerResiliencyOptions | None = None,
8283
concurrency_options: ConcurrencyOptions | None = None,
8384
payload_store: PayloadStore | None = None,
85+
data_converter: DataConverter | None = None,
8486
log_handler: logging.Handler | None = None,
8587
log_formatter: logging.Formatter | None = None):
8688

@@ -110,5 +112,6 @@ def __init__(self, *,
110112
concurrency_options=concurrency_options,
111113
# DTS natively supports long timers so chunking is unnecessary
112114
maximum_timer_interval=None,
113-
payload_store=payload_store
115+
payload_store=payload_store,
116+
data_converter=data_converter
114117
)

durabletask/entities/entity_context.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@
1616

1717
class EntityContext:
1818
def __init__(self, orchestration_id: str, operation: str, state: StateShim,
19-
entity_id: EntityInstanceId, data_converter: "DataConverter | None" = None):
19+
entity_id: EntityInstanceId, data_converter: "DataConverter"):
2020
self._orchestration_id = orchestration_id
2121
self._operation = operation
2222
self._state = state
2323
self._entity_id = entity_id
24-
if data_converter is None:
25-
from durabletask.serialization import JsonDataConverter
26-
data_converter = JsonDataConverter()
2724
self._data_converter = data_converter
2825

2926
@property

durabletask/entities/entity_metadata.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __init__(self,
3636
locked_by: str,
3737
includes_state: bool,
3838
state: Any | None,
39-
data_converter: "DataConverter | None" = None):
39+
data_converter: "DataConverter"):
4040
"""Initializes a new instance of the EntityMetadata class.
4141
4242
Args:
@@ -48,20 +48,17 @@ def __init__(self,
4848
self._locked_by = locked_by
4949
self.includes_state = includes_state
5050
self._state = state
51-
if data_converter is None:
52-
from durabletask.serialization import JsonDataConverter
53-
data_converter = JsonDataConverter()
5451
self._data_converter = data_converter
5552

5653
@staticmethod
5754
def from_entity_response(entity_response: pb.GetEntityResponse, includes_state: bool,
58-
data_converter: "DataConverter | None" = None):
55+
data_converter: "DataConverter"):
5956
return EntityMetadata.from_entity_metadata(
6057
entity_response.entity, includes_state, data_converter)
6158

6259
@staticmethod
6360
def from_entity_metadata(entity: pb.EntityMetadata, includes_state: bool,
64-
data_converter: "DataConverter | None" = None):
61+
data_converter: "DataConverter"):
6562
try:
6663
entity_id = EntityInstanceId.parse(entity.instanceId)
6764
except ValueError:

durabletask/internal/entity_state_shim.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,8 @@ class StateShim:
3636
is written back with :meth:`set_state`.
3737
"""
3838

39-
def __init__(self, start_state: Any, data_converter: "DataConverter | None" = None,
39+
def __init__(self, start_state: Any, data_converter: "DataConverter",
4040
*, is_serialized: bool = False):
41-
if data_converter is None:
42-
from durabletask.serialization import JsonDataConverter
43-
data_converter = JsonDataConverter()
4441
self._data_converter = data_converter
4542
# The state is normalized to its serialized string form. ``is_serialized``
4643
# marks ``start_state`` as a raw payload already off the wire (stored

examples/custom_data_converter/src/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def main() -> None:
7575
if state and state.runtime_status == client.OrchestrationStatus.COMPLETED:
7676
# ``get_output(Receipt)`` reconstructs the typed, validated result.
7777
receipt = state.get_output(Receipt)
78+
assert receipt is not None
7879
print("Orchestration completed. Typed receipt:")
7980
print(f" customer = {receipt.customer}")
8081
print(f" total = {receipt.total}")

tests/durabletask/test_entity_executor.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def _make_executor(*entity_args) -> _EntityExecutor:
2222
def _execute(executor, entity_name, operation, encoded_input=None):
2323
"""Helper to execute an entity operation."""
2424
entity_id = entities.EntityInstanceId(entity_name, "test-key")
25-
state = StateShim(None)
25+
state = StateShim(None, JsonDataConverter())
2626
return executor.execute("test-orchestration", entity_id, operation, state, encoded_input)
2727

2828

@@ -75,7 +75,7 @@ def get(self):
7575
entity_id = entities.EntityInstanceId("Counter", "test-key")
7676

7777
# set requires input
78-
state = StateShim(None)
78+
state = StateShim(None, JsonDataConverter())
7979
executor.execute("test-orch", entity_id, "set", state, "10")
8080
state.commit()
8181

@@ -127,7 +127,7 @@ def counter(ctx: entities.EntityContext, input):
127127

128128
executor = _make_executor(counter)
129129
entity_id = entities.EntityInstanceId("counter", "test-key")
130-
state = StateShim(None)
130+
state = StateShim(None, JsonDataConverter())
131131

132132
executor.execute("test-orch", entity_id, "set", state, "42")
133133
state.commit()
@@ -140,19 +140,19 @@ class TestStateShimCoercion:
140140
"""Tests for StateShim.get_state type coercion via the data converter."""
141141

142142
def test_get_state_none_returns_default(self):
143-
state = StateShim(None)
143+
state = StateShim(None, JsonDataConverter())
144144
assert state.get_state(int, 0) == 0
145145

146146
def test_get_state_none_without_default_returns_none(self):
147-
state = StateShim(None)
147+
state = StateShim(None, JsonDataConverter())
148148
assert state.get_state(int) is None
149149

150150
def test_get_state_passes_through_matching_type(self):
151-
state = StateShim(5)
151+
state = StateShim(5, JsonDataConverter())
152152
assert state.get_state(int) == 5
153153

154154
def test_get_state_constructor_coercion(self):
155-
state = StateShim("5")
155+
state = StateShim("5", JsonDataConverter())
156156
assert state.get_state(int) == 5
157157

158158
def test_get_state_coerces_dataclass(self):
@@ -163,7 +163,7 @@ class Counter:
163163
value: int
164164

165165
# State is stored as a plain dict (as it would be after from_json).
166-
state = StateShim({"value": 7})
166+
state = StateShim({"value": 7}, JsonDataConverter())
167167
result = state.get_state(Counter)
168168
assert isinstance(result, Counter)
169169
assert result.value == 7
@@ -177,7 +177,7 @@ def __init__(self, n: int):
177177
def from_json(cls, data):
178178
return cls(data["n"])
179179

180-
state = StateShim({"n": 3})
180+
state = StateShim({"n": 3}, JsonDataConverter())
181181
result = state.get_state(Wrapped)
182182
assert isinstance(result, Wrapped)
183183
assert result.n == 3
@@ -187,7 +187,7 @@ def test_get_state_invalid_coercion_raises(self):
187187
# restoring the pre-existing strict contract for entity state access.
188188
import pytest
189189

190-
state = StateShim("not-an-int")
190+
state = StateShim("not-an-int", JsonDataConverter())
191191
with pytest.raises(TypeError):
192192
state.get_state(int)
193193

@@ -197,7 +197,7 @@ class TestStateShimDeferredDeserialization:
197197

198198
def test_constructor_does_not_deserialize_serialized_state(self):
199199
# A serialized payload is held verbatim until read, not eagerly parsed.
200-
state = StateShim('{"value": 7}', is_serialized=True)
200+
state = StateShim('{"value": 7}', JsonDataConverter(), is_serialized=True)
201201
assert state._current_state == '{"value": 7}'
202202

203203
def test_get_state_defers_deserialization_with_type(self):
@@ -207,13 +207,13 @@ def test_get_state_defers_deserialization_with_type(self):
207207
class Counter:
208208
value: int
209209

210-
state = StateShim('{"value": 7}', is_serialized=True)
210+
state = StateShim('{"value": 7}', JsonDataConverter(), is_serialized=True)
211211
result = state.get_state(Counter)
212212
assert isinstance(result, Counter)
213213
assert result.value == 7
214214

215215
def test_get_state_no_type_returns_parsed_value(self):
216-
state = StateShim('{"value": 7}', is_serialized=True)
216+
state = StateShim('{"value": 7}', JsonDataConverter(), is_serialized=True)
217217
assert state.get_state() == {"value": 7}
218218

219219
def test_deferred_deserialization_passes_raw_string_to_converter(self):
@@ -247,11 +247,11 @@ def coerce(self, value, target_type=None):
247247
def test_encode_state_passes_through_unmodified_payload(self):
248248
# An unread/unmodified serialized payload is returned verbatim, never
249249
# re-serialized (which would double-encode the JSON string).
250-
state = StateShim('{"value": 7}', is_serialized=True)
250+
state = StateShim('{"value": 7}', JsonDataConverter(), is_serialized=True)
251251
assert state.encode_state() == '{"value": 7}'
252252

253253
def test_reading_does_not_trigger_double_encoding(self):
254-
state = StateShim('{"value": 7}', is_serialized=True)
254+
state = StateShim('{"value": 7}', JsonDataConverter(), is_serialized=True)
255255
# Reading (even with a type) must not turn the payload into a live value
256256
# that would be re-serialized into a JSON-encoded string.
257257
state.get_state()
@@ -261,31 +261,31 @@ def test_reading_does_not_trigger_double_encoding(self):
261261
assert json.loads(encoded) == {"value": 7}
262262

263263
def test_encode_state_serializes_live_value_after_set_state(self):
264-
state = StateShim('{"value": 7}', is_serialized=True)
264+
state = StateShim('{"value": 7}', JsonDataConverter(), is_serialized=True)
265265
state.set_state({"value": 8})
266266
encoded = state.encode_state()
267267
assert json.loads(encoded) == {"value": 8}
268268

269269
def test_encode_state_none_when_state_is_none(self):
270-
state = StateShim(None, is_serialized=True)
270+
state = StateShim(None, JsonDataConverter(), is_serialized=True)
271271
assert state.encode_state() is None
272272

273273
def test_commit_preserves_unmodified_payload(self):
274-
state = StateShim('{"value": 7}', is_serialized=True)
274+
state = StateShim('{"value": 7}', JsonDataConverter(), is_serialized=True)
275275
state.commit()
276276
# After commit, the (unmodified) state still round-trips without
277277
# double-encoding.
278278
assert state.encode_state() == '{"value": 7}'
279279

280280
def test_rollback_restores_unmodified_payload(self):
281-
state = StateShim('{"value": 7}', is_serialized=True)
281+
state = StateShim('{"value": 7}', JsonDataConverter(), is_serialized=True)
282282
state.commit()
283283
state.set_state({"value": 99})
284284
state.rollback()
285285
assert state.encode_state() == '{"value": 7}'
286286

287287
def test_falsy_serialized_state_is_not_dropped(self):
288288
# A serialized falsy value (e.g. 0) is preserved, not treated as cleared.
289-
state = StateShim("0", is_serialized=True)
289+
state = StateShim("0", JsonDataConverter(), is_serialized=True)
290290
assert state.get_state(int) == 0
291291
assert state.encode_state() == "0"

0 commit comments

Comments
 (0)