diff --git a/CHANGELOG.md b/CHANGELOG.md index ee28a3bbf3..56f3c8dc56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,9 @@ to include examples, links to docs, or any other relevant information. ### Fixed +- Restore `frozenset` values when decoding JSON payloads with a `frozenset` type hint, + including nested frozen sets. + - Ordinary absolute imports of already-loaded modules in sandboxed workflows no longer go through importlib's module locks, fixing intermittent `Failed validating workflow` errors on Python 3.10 caused by a `KeyError` in `importlib._bootstrap._ModuleLock.acquire` when a garbage-collection diff --git a/temporalio/converter/_payload_converter.py b/temporalio/converter/_payload_converter.py index 00c63e340d..a32de04094 100644 --- a/temporalio/converter/_payload_converter.py +++ b/temporalio/converter/_payload_converter.py @@ -1086,11 +1086,13 @@ def value_to_type( ret_list.append(value_to_type(arg_type, item, custom_converters)) except Exception as err: raise TypeError(f"Failed converting {hint} index {i}") from err - # If tuple, set, or deque convert back to that type + # If tuple, set, frozenset, or deque convert back to that type if origin is tuple: return tuple(ret_list) elif origin is set: return set(ret_list) + elif origin is frozenset: + return frozenset(ret_list) elif origin is collections.deque: return collections.deque(ret_list) return ret_list diff --git a/tests/test_converter.py b/tests/test_converter.py index 4c64e25ba4..8444ae0ba1 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -351,6 +351,28 @@ class MyPydanticClass(pydantic.BaseModel): # type: ignore[reportUnreachable] baz: UUID | None = None +@pytest.mark.parametrize( + "hint", + [frozenset, frozenset[int], typing_extensions.FrozenSet[int]], # type:ignore[reportDeprecated] +) +@pytest.mark.parametrize("value", [frozenset(), frozenset({1, 2})]) +def test_json_frozenset_round_trip(hint: Any, value: frozenset[int]): + converter = JSONPlainPayloadConverter() + payload = converter.to_payload(value) + assert payload is not None + converted = converter.from_payload(payload, hint) + assert isinstance(converted, frozenset) + assert converted == value + + +def test_json_nested_frozenset_round_trip(): + converter = JSONPlainPayloadConverter() + value = {frozenset({1, 2}), frozenset({3})} + payload = converter.to_payload(value) + assert payload is not None + assert converter.from_payload(payload, set[frozenset[int]]) == value + + def test_json_type_hints(): converter = JSONPlainPayloadConverter() @@ -439,6 +461,7 @@ def fail(hint: Any, value: Any) -> None: ok(deque[int], deque([5, 6])) ok(Sequence[int], [5, 6]) fail(list[int], [1, 2, "3"]) + fail(frozenset[int], [1, 2, "3"]) # Dict-like ok(dict[str, MyDataClass], {"foo": MyDataClass("foo", 5, SerializableEnum.FOO)})