Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion haystack/utils/type_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,14 @@ def serialize_type(target: Any) -> str:
# This avoids issues with Python's internal cache, where List[Union[str, int]] and List[str | int] are treated
# as the same key. GenericAlias (builtins like list[...]) can keep the PEP 604 syntax.
is_typing_generic = not isinstance(target, GenericAlias)
# Optional[X] is normalized by Python to Union[X, None]; the trailing None is already implied by the
# "Optional" name, so we drop it. For any other generic (e.g. Dict[str, None], Tuple[int, None] or a
# Union with more than two members) NoneType is a regular argument and must be kept.
skip_nonetype = name == "Optional"
args_str = ", ".join(
serialize_type(Union[tuple(get_args(a))] if is_typing_generic and isinstance(a, UnionType) else a) # noqa: UP007
for a in args
if a is not NoneType
if not (skip_nonetype and a is NoneType)
Comment thread
sjrl marked this conversation as resolved.
)
return f"{module_name}.{name}[{args_str}]" if module_name else f"{name}[{args_str}]"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
fixes:
- |
Fixed `serialize_type` dropping `NoneType` when it appears as a regular argument of a `typing` generic.
Comment thread
sjrl marked this conversation as resolved.
Outdated
Types such as `typing.Dict[str, None]`, `typing.Tuple[int, None]`, `typing.List[None]` and unions with more
than two members like `typing.Union[str, int, None]` were serialized incorrectly (for example to
`typing.Dict[str]`), which produced malformed type strings that could not be deserialized again.
`typing.Optional[X]` is still serialized as `typing.Optional[X]` without a redundant trailing `None`.
Comment thread
sjrl marked this conversation as resolved.
Outdated
25 changes: 25 additions & 0 deletions test/utils/test_type_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,31 @@ def test_output_type_deserialization_nested():
assert deserialize_type("list[dict[str, int] | None]") == list[Union[dict[str, int], None]]


def test_output_type_serialization_typing_generic_with_nonetype():
# NoneType used as a regular argument of a typing generic (not the implicit None of Optional)
# must be kept, otherwise the serialized type is malformed (e.g. "typing.Dict[str]") or loses information.
assert serialize_type(Dict[str, type(None)]) == "typing.Dict[str, None]"
assert serialize_type(Dict[type(None), str]) == "typing.Dict[None, str]"
assert serialize_type(Tuple[int, type(None)]) == "typing.Tuple[int, None]"
assert serialize_type(List[type(None)]) == "typing.List[None]"
# A Union with more than two members that includes None must keep None as well.
assert serialize_type(Union[str, int, None]) == "typing.Union[str, int, None]"
# Optional must still be serialized without a redundant trailing None.
assert serialize_type(Optional[str]) == "typing.Optional[str]"


def test_output_type_round_trip_typing_generic_with_nonetype():
for type_ in [
Dict[str, type(None)],
Dict[type(None), str],
Tuple[int, type(None)],
List[type(None)],
Union[str, int, None],
Optional[str],
]:
assert deserialize_type(serialize_type(type_)) == type_


def test_output_type_serialization_haystack_dataclasses():
# typing
# Answer
Expand Down
Loading