Skip to content

Commit 2574517

Browse files
ATOM00bluesjrl
andauthored
fix: keep NoneType argument when serializing typing generics (#11368)
Co-authored-by: ATOM00blue <219721791+ATOM00blue@users.noreply.github.com> Co-authored-by: Sebastian Husch Lee <10526848+sjrl@users.noreply.github.com>
1 parent 3611929 commit 2574517

3 files changed

Lines changed: 38 additions & 1 deletion

File tree

haystack/utils/type_serialization.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,14 @@ def serialize_type(target: Any) -> str:
7575
# This avoids issues with Python's internal cache, where List[Union[str, int]] and List[str | int] are treated
7676
# as the same key. GenericAlias (builtins like list[...]) can keep the PEP 604 syntax.
7777
is_typing_generic = not isinstance(target, GenericAlias)
78+
# Optional[X] is normalized by Python to Union[X, None]; the trailing None is already implied by the
79+
# "Optional" name, so we drop it. For any other generic (e.g. Dict[str, None], Tuple[int, None] or a
80+
# Union with more than two members) NoneType is a regular argument and must be kept.
81+
skip_nonetype = name == "Optional"
7882
args_str = ", ".join(
7983
serialize_type(Union[tuple(get_args(a))] if is_typing_generic and isinstance(a, UnionType) else a) # noqa: UP007
8084
for a in args
81-
if a is not NoneType
85+
if not (skip_nonetype and a is NoneType)
8286
)
8387
return f"{module_name}.{name}[{args_str}]" if module_name else f"{name}[{args_str}]"
8488

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed ``serialize_type`` dropping ``NoneType`` when it appears as a regular argument of a ``typing`` generic.
5+
Types such as ``typing.Dict[str, None]``, ``typing.Tuple[int, None]``, ``typing.List[None]`` and unions with more
6+
than two members like ``typing.Union[str, int, None]`` were serialized incorrectly (for example to
7+
``typing.Dict[str]``), which produced malformed type strings that could not be deserialized again.
8+
``typing.Optional[X]`` is still serialized as ``typing.Optional[X]`` without a redundant trailing ``None``.

test/utils/test_type_serialization.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,31 @@ def test_output_type_deserialization_nested():
195195
assert deserialize_type("list[dict[str, int] | None]") == list[Union[dict[str, int], None]]
196196

197197

198+
def test_output_type_serialization_typing_generic_with_nonetype():
199+
# NoneType used as a regular argument of a typing generic (not the implicit None of Optional)
200+
# must be kept, otherwise the serialized type is malformed (e.g. "typing.Dict[str]") or loses information.
201+
assert serialize_type(Dict[str, type(None)]) == "typing.Dict[str, None]"
202+
assert serialize_type(Dict[type(None), str]) == "typing.Dict[None, str]"
203+
assert serialize_type(Tuple[int, type(None)]) == "typing.Tuple[int, None]"
204+
assert serialize_type(List[type(None)]) == "typing.List[None]"
205+
# A Union with more than two members that includes None must keep None as well.
206+
assert serialize_type(Union[str, int, None]) == "typing.Union[str, int, None]"
207+
# Optional must still be serialized without a redundant trailing None.
208+
assert serialize_type(Optional[str]) == "typing.Optional[str]"
209+
210+
211+
def test_output_type_round_trip_typing_generic_with_nonetype():
212+
for type_ in [
213+
Dict[str, type(None)],
214+
Dict[type(None), str],
215+
Tuple[int, type(None)],
216+
List[type(None)],
217+
Union[str, int, None],
218+
Optional[str],
219+
]:
220+
assert deserialize_type(serialize_type(type_)) == type_
221+
222+
198223
def test_output_type_serialization_haystack_dataclasses():
199224
# typing
200225
# Answer

0 commit comments

Comments
 (0)