Fix serialization of unions with nested PEP 695 type aliases#331
Fix serialization of unions with nested PEP 695 type aliases#331PraneelBhatia wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR improves Mashumaro’s support for PEP 695 TypeAliasType when aliases are used inside Union/| types (including parameterized aliases), and adds regression tests covering dict serialization, omit-none behavior, and codecs.
Changes:
- Add
resolve_type_alias_type()helper to centralizeTypeAliasTypeunwrapping (including parameter substitution for parameterized aliases). - Apply alias resolution in pack/unpack union handling and in special typing primitive paths.
- Add tests reproducing issue #330 and additional union+alias scenarios.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| tests/test_pep_695.py | Adds regression tests for TypeAliasType nested in unions (incl. omit-none flag + codecs). |
| mashumaro/core/meta/types/unpack.py | Resolves type aliases before selecting union unpackers and consolidates alias handling. |
| mashumaro/core/meta/types/pack.py | Resolves type aliases before selecting union packers and consolidates alias handling. |
| mashumaro/core/meta/helpers.py | Introduces resolve_type_alias_type() helper for reuse across pack/unpack. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def resolve_type_alias_type(typ: Type) -> Type: | ||
| while True: | ||
| if is_type_alias_type(typ): | ||
| typ = typ.__value__ | ||
| elif is_type_alias_type(get_type_origin(typ)): | ||
| origin = get_type_origin(typ) | ||
| type_params = getattr(origin, "__type_params__", ()) | ||
| args = get_args(typ) | ||
| param_map = dict(zip(type_params, args)) | ||
| typ = substitute_type_params(origin.__value__, param_map) | ||
| else: | ||
| return typ |
There was a problem hiding this comment.
Added cycle protection in 829f7c8: the resolver tracks seen values and bounds the number of resolution steps, raising TypeError: Cannot resolve recursive type alias ... for both exact cycles (type A = B; type B = A) and non-repeating chains (type G[T] = G[list[T]]). The seen set tracks values rather than id()s because each substitution builds a new (equal) object for parameterized cycles; is_hashable() guards unhashable values and the step bound is the backstop. Raising a targeted error seemed more useful than returning the unresolved alias, which would just push the non-termination into the packer/unpacker dispatch. Tests cover both shapes, including at dataclass creation time.
| type_params = getattr(origin, "__type_params__", ()) | ||
| args = get_args(typ) | ||
| param_map = dict(zip(type_params, args)) | ||
| typ = substitute_type_params(origin.__value__, param_map) |
There was a problem hiding this comment.
Replaced the silent truncation in 829f7c8: missing trailing parameters now fall back to their PEP 696 defaults when defined (TypeAliasType subscription doesn't auto-fill defaults, so Pair[int] with TypeVar("V", default=str) previously leaked an unsubstituted ~V into the resolved type), and a genuine mismatch raises TypeError: Too few/Too many arguments for ...; actual N, expected M in the same format as the existing generics arity check. The check is skipped for TypeVarTuple/ParamSpec parameters since their subscription arity is flexible. One behavior note: invalid annotations like Pair[int] without a default used to serialize with a half-substituted value and now fail at class creation — happy to relax that if you'd rather keep them lenient.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #331 +/- ##
==========================================
- Coverage 99.92% 99.91% -0.01%
==========================================
Files 101 101
Lines 12007 12131 +124
==========================================
+ Hits 11998 12121 +123
- Misses 9 10 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Fixes #330
When a union contains a PEP 695
typealias as one of its members, serializer generation crashed withTypeError: issubclass() arg 1 must be a class, becausepack_unionpassed the rawTypeAliasTypeobject toissubclass(get_type_origin(type_arg), Collection). The deserialization path had the symmetric problem:UnionUnpackerBuildergenerated type-match conditions from the alias's__name__, which raisedNameErrorat decode time.Changes:
resolve_type_alias_type()helper tomashumaro/core/meta/helpers.pythat fully resolvesTypeAliasTypeobjects, including parameterized aliases (type ListOf[T] = list[T]) viasubstitute_type_params.pack_unionandUnionUnpackerBuilder._add_body.pack_special_typing_primitiveandunpack_special_typing_primitiveto use the same helper (behavior unchanged).TO_DICT_ADD_OMIT_NONE_FLAGvariant with eager and lazy compilation, codecs, and parameterized aliases as union members. All fail before the fix.