Skip to content

Commit 6c5c5fd

Browse files
committed
Align DataConverter with Go Client
1 parent 884c64f commit 6c5c5fd

File tree

4 files changed

+42
-45
lines changed

4 files changed

+42
-45
lines changed

cadence/_internal/activity/_activity_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async def _report_failure(self, task: PollForActivityTaskResponse, error: Except
5757
_logger.exception('Exception reporting activity failure')
5858

5959
async def _report_success(self, task: PollForActivityTaskResponse, result: Any):
60-
as_payload = await self._data_converter.to_data(result)
60+
as_payload = await self._data_converter.to_data([result])
6161

6262
try:
6363
await self._client.worker_stub.RespondActivityTaskCompleted(RespondActivityTaskCompletedRequest(

cadence/data_converter.py

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from json import JSONDecoder
66
from msgspec import json, convert
77

8+
_SPACE = ' '.encode()
89

910
class DataConverter(Protocol):
1011

@@ -19,33 +20,24 @@ async def to_data(self, values: List[Any]) -> Payload:
1920
class DefaultDataConverter(DataConverter):
2021
def __init__(self) -> None:
2122
self._encoder = json.Encoder()
22-
self._decoder = json.Decoder()
23-
self._fallback_decoder = JSONDecoder(strict=False)
23+
# Need to use std lib decoder in order to decode the custom whitespace delimited data format
24+
self._decoder = JSONDecoder(strict=False)
2425

2526

2627
async def from_data(self, payload: Payload, type_hints: List[Type | None]) -> List[Any]:
2728
if not payload.data:
2829
return DefaultDataConverter._convert_into([], type_hints)
2930

30-
if len(type_hints) > 1:
31-
payload_str = payload.data.decode()
32-
# Handle payloads from the Go client, which are a series of json objects rather than a json array
33-
if not payload_str.startswith("["):
34-
return self._decode_whitespace_delimited(payload_str, type_hints)
35-
else:
36-
as_list = self._decoder.decode(payload_str)
37-
return DefaultDataConverter._convert_into(as_list, type_hints)
38-
39-
as_value = self._decoder.decode(payload.data)
40-
return DefaultDataConverter._convert_into([as_value], type_hints)
31+
payload_str = payload.data.decode()
4132

33+
return self._decode_whitespace_delimited(payload_str, type_hints)
4234

4335
def _decode_whitespace_delimited(self, payload: str, type_hints: List[Type | None]) -> List[Any]:
4436
results: List[Any] = []
4537
start, end = 0, len(payload)
4638
while start < end and len(results) < len(type_hints):
4739
remaining = payload[start:end]
48-
(value, value_end) = self._fallback_decoder.raw_decode(remaining)
40+
(value, value_end) = self._decoder.raw_decode(remaining)
4941
start += value_end + 1
5042
results.append(value)
5143

@@ -76,10 +68,11 @@ def _get_default(type_hint: Type) -> Any:
7668

7769

7870
async def to_data(self, values: List[Any]) -> Payload:
79-
data_value = values
80-
# Don't wrap single values in a json array
81-
if len(values) == 1:
82-
data_value = values[0]
71+
result = bytearray()
72+
for index, value in enumerate(values):
73+
self._encoder.encode_into(value, result, -1)
74+
if index < len(values) - 1:
75+
result += _SPACE
8376

84-
return Payload(data=self._encoder.encode(data_value))
77+
return Payload(data=bytes(result))
8578

tests/cadence/_internal/activity/test_activity_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ async def activity_fn(first: str, second: str):
8282

8383
executor = ActivityExecutor(client, 'task_list', 'identity', 1, reg.get_activity)
8484

85-
await executor.execute(fake_task("activity_type", '["hello", "world"]'))
85+
await executor.execute(fake_task("activity_type", '"hello" "world"'))
8686

8787
worker_stub.RespondActivityTaskCompleted.assert_called_once_with(RespondActivityTaskCompletedRequest(
8888
task_token=b'task_token',

tests/cadence/data_converter_test.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,28 @@ class _TestDataClass:
2323
'"Hello" "world"', [str, str], ["Hello", "world"], id="space delimited"
2424
),
2525
pytest.param(
26-
'["Hello", "world"]', [str, str], ["Hello", "world"], id="json array"
26+
"1", [int, int], [1, 0], id="ints"
2727
),
2828
pytest.param(
29-
"[1]", [int, int], [1, 0], id="ints"
29+
"1.5", [float, float], [1.5, 0.0], id="floats"
3030
),
3131
pytest.param(
32-
"[1.5]", [float, float], [1.5, 0.0], id="floats"
32+
"true", [bool, bool], [True, False], id="bools"
3333
),
3434
pytest.param(
35-
"[true]", [bool, bool], [True, False], id="bools"
35+
'{"foo": "hello world", "bar": 42, "baz": {"bar": 43}}', [_TestDataClass, _TestDataClass], [_TestDataClass("hello world", 42, _TestDataClass(bar=43)), None], id="data classes"
3636
),
3737
pytest.param(
38-
'[{"foo": "hello world", "bar": 42, "baz": {"bar": 43}}]', [_TestDataClass, _TestDataClass], [_TestDataClass("hello world", 42, _TestDataClass(bar=43)), None], id="data classes"
38+
'{"foo": "hello world"}', [dict, dict], [{"foo": "hello world"}, None], id="dicts"
3939
),
4040
pytest.param(
41-
'[{"foo": "hello world"}]', [dict, dict], [{"foo": "hello world"}, None], id="dicts"
41+
'{"foo": 52}', [dict[str, int], dict], [{"foo": 52}, None], id="generic dicts"
4242
),
4343
pytest.param(
44-
'[{"foo": 52}]', [dict[str, int], dict], [{"foo": 52}, None], id="generic dicts"
44+
'["hello"]', [list[str], list[str]], [["hello"], None], id="lists"
4545
),
4646
pytest.param(
47-
'[["hello"]]', [list[str], list[str]], [["hello"], None], id="lists"
48-
),
49-
pytest.param(
50-
'[["hello"]]', [set[str], set[str]], [{"hello"}, None], id="sets"
47+
'["hello"]', [set[str], set[str]], [{"hello"}, None], id="sets"
5148
),
5249
pytest.param(
5350
'["hello", "world"]', [list[str]], [["hello", "world"]], id="list"
@@ -56,26 +53,21 @@ class _TestDataClass:
5653
'{"foo": "bar"} {"bar": 100} ["hello"] "world"', [_TestDataClass, _TestDataClass, list[str], str],
5754
[_TestDataClass(foo="bar"), _TestDataClass(bar=100), ["hello"], "world"], id="space delimited mix"
5855
),
59-
pytest.param(
60-
'[{"foo": "bar"},{"bar": 100},["hello"],"world"]', [_TestDataClass, _TestDataClass, list[str], str],
61-
[_TestDataClass(foo="bar"), _TestDataClass(bar=100), ["hello"], "world"], id="json array mix"
62-
),
6356
pytest.param(
6457
"", [], [], id="no input expected"
6558
),
6659
pytest.param(
6760
"", [str], [None], id="no input unexpected"
6861
),
6962
pytest.param(
70-
'["hello world", {"foo":"bar"}, 7]', [None, None, None], ["hello world", {"foo":"bar"}, 7], id="no type hints"
63+
'"hello world" {"foo":"bar"} 7', [None, None, None], ["hello world", {"foo":"bar"}, 7], id="no type hints"
7164
),
7265
pytest.param(
7366
'"hello" "world" "goodbye"', [str, str], ["hello", "world"],
7467
id="extra content"
7568
),
7669
]
7770
)
78-
@pytest.mark.asyncio
7971
async def test_data_converter_from_data(json: str, types: list[Type], expected: list[Any]) -> None:
8072
converter = DefaultDataConverter()
8173
actual = await converter.from_data(Payload(data=json.encode()), types)
@@ -88,18 +80,30 @@ async def test_data_converter_from_data(json: str, types: list[Type], expected:
8880
["hello world"], '"hello world"', id="happy path"
8981
),
9082
pytest.param(
91-
["hello", "world"], '["hello", "world"]', id="multiple values"
83+
["hello", "world"], '"hello" "world"', id="multiple values"
84+
),
85+
pytest.param(
86+
[[["hello"]], ["world"]], '[["hello"]] ["world"]', id="lists"
87+
),
88+
pytest.param(
89+
[1, 2, 10], '1 2 10', id="numeric values"
90+
),
91+
pytest.param(
92+
[True, False], 'true false', id="bool values"
93+
),
94+
pytest.param(
95+
[{'foo': 'foo', 'bar': 20}], '{"bar":20,"foo":"foo"}', id="dict values"
96+
),
97+
pytest.param(
98+
[{'foo', 'bar'}], '["bar","foo"]', id="set values"
9299
),
93100
pytest.param(
94-
[_TestDataClass()], '{"foo": "foo", "bar": -1, "baz": null}', id="data classes"
101+
[_TestDataClass()], '{"foo":"foo","bar":-1,"baz":null}', id="data classes"
95102
),
96103
]
97104
)
98-
@pytest.mark.asyncio
99105
async def test_data_converter_to_data(values: list[Any], expected: str) -> None:
100106
converter = DefaultDataConverter()
107+
converter._encoder = json.Encoder(order='deterministic')
101108
actual = await converter.to_data(values)
102-
# Parse both rather than trying to compare strings
103-
actual_parsed = json.decode(actual.data)
104-
expected_parsed = json.decode(expected)
105-
assert expected_parsed == actual_parsed
109+
assert actual.data.decode() == expected

0 commit comments

Comments
 (0)