fix: round-trip serialization of Ellipsis in variadic tuple and Callable types#12062
Merged
anakin87 merged 2 commits intoJul 21, 2026
Merged
Conversation
…ble types serialize_type rendered the `...` in types like tuple[int, ...] and Callable[..., X] as the literal string "Ellipsis", which deserialize_type then refused as a non-type builtin. So a component using such a type (for example OutputAdapter(output_type=tuple[int, ...])) could be written by Pipeline.dumps() but failed to load back with Pipeline.loads(). Serialize Ellipsis to "..." (the literal Python itself uses when rendering these types) and resolve it, together with None and NoneType, before the generic and module-path handling in deserialize_type. The legacy "Ellipsis" literal is still accepted so pipelines serialized by older versions keep loading.
|
@winklemad is attempting to deploy a commit to the deepset Team on Vercel. A member of the Team first needs to authorize it. |
Contributor
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||
anakin87
requested changes
Jul 20, 2026
anakin87
left a comment
Member
There was a problem hiding this comment.
Thank you, the fix looks good!
I only found two minor opportunities to improve this PR...
| # "..." would otherwise be read as a module path, and the None/Ellipsis singletons would be refused | ||
| # by the builtin type gate. "Ellipsis" is also accepted so that types serialized by older versions | ||
| # (which emitted the literal "Ellipsis") can still be read back. | ||
| special_literals: dict[str, Any] = {"None": None, "NoneType": NoneType, "...": ..., "Ellipsis": ...} |
Member
There was a problem hiding this comment.
nitpick: I'd use a _SPECIAL_LITERALS constant defined on the module
Comment on lines
+253
to
+272
| def test_output_type_serialization_ellipsis(): | ||
| # The `...` (Ellipsis) singleton is serialized to the literal "..." Python itself uses when rendering | ||
| # variadic tuples and Callable, not to str(Ellipsis) == "Ellipsis" (which is not a type). | ||
| assert serialize_type(tuple[int, ...]) == "tuple[int, ...]" | ||
| assert serialize_type(Tuple[int, ...]) == "typing.Tuple[int, ...]" | ||
| assert serialize_type(Callable[..., int]) == "typing.Callable[..., int]" | ||
|
|
||
|
|
||
| def test_output_type_round_trip_variadic_tuple_and_callable_ellipsis(): | ||
| # Regression: `...` used to be serialized as "Ellipsis", which deserialize_type then rejected as a | ||
| # non-type builtin, so a type Haystack serialized itself could not be read back. | ||
| for type_ in [ | ||
| tuple[int, ...], | ||
| tuple[str, ...], | ||
| tuple[dict[str, int], ...], | ||
| Tuple[int, ...], | ||
| Callable[..., int], | ||
| Callable[..., str], | ||
| ]: | ||
| assert deserialize_type(serialize_type(type_)) == type_ |
Member
There was a problem hiding this comment.
Suggested change
| def test_output_type_serialization_ellipsis(): | |
| # The `...` (Ellipsis) singleton is serialized to the literal "..." Python itself uses when rendering | |
| # variadic tuples and Callable, not to str(Ellipsis) == "Ellipsis" (which is not a type). | |
| assert serialize_type(tuple[int, ...]) == "tuple[int, ...]" | |
| assert serialize_type(Tuple[int, ...]) == "typing.Tuple[int, ...]" | |
| assert serialize_type(Callable[..., int]) == "typing.Callable[..., int]" | |
| def test_output_type_round_trip_variadic_tuple_and_callable_ellipsis(): | |
| # Regression: `...` used to be serialized as "Ellipsis", which deserialize_type then rejected as a | |
| # non-type builtin, so a type Haystack serialized itself could not be read back. | |
| for type_ in [ | |
| tuple[int, ...], | |
| tuple[str, ...], | |
| tuple[dict[str, int], ...], | |
| Tuple[int, ...], | |
| Callable[..., int], | |
| Callable[..., str], | |
| ]: | |
| assert deserialize_type(serialize_type(type_)) == type_ |
Let's add
pytest.param("typing.Callable[..., int]", Callable[..., int]),
pytest.param("typing.Callable[..., str]", Callable[..., str]),to TYPING_AND_TYPE_TESTS, so that the above-mentioned tests become fully redundant and we can remove them.
…a round-trip params Move the special-literal lookup table in deserialize_type to a module-level _SPECIAL_LITERALS constant, and add typing.Callable[..., int] / [..., str] to TYPING_AND_TYPE_TESTS so the variadic-tuple and Callable ellipsis cases are exercised by the shared serialize/deserialize round-trip tests. The two standalone ellipsis tests are now redundant and removed.
Contributor
Author
|
Done, thanks for the review!
|
anakin87
self-requested a review
July 21, 2026 07:37
anakin87
enabled auto-merge (squash)
July 21, 2026 07:37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
serialize_typerendered the...in types liketuple[int, ...]andCallable[..., X]as the literal string"Ellipsis"(it fell through tostr(Ellipsis)), whichdeserialize_typethen rejected — the"Ellipsis"token hits the builtin-type gate and is correctly refused sinceEllipsisisn't a type.The result is a serialization round-trip break: any component given a variadic-tuple or
Callable[..., X]type — e.g.OutputAdapter(output_type=tuple[int, ...]),ConditionalRouter/MetadataRouterroute types,ListJoiner/BranchJoiner,LLMEvaluator, an AgentStateschema — produces a pipeline thatPipeline.dumps()writes butPipeline.loads()hard-rejects.Fix:
serialize_typeemits"..."(the literal Python itself uses) forEllipsis, anddeserialize_typeresolves the non-type singletons (None,NoneType,..., plus the legacy"Ellipsis") up front — before the module-path branch, since the dots in"..."would otherwise be read as a module path. Covers both variadic tuples andCallable[..., X]; legacy"Ellipsis"payloads still load. Added round-trip regression tests for variadic tuples and Callable-with-Ellipsis.