diff --git a/mashumaro/core/meta/types/pack.py b/mashumaro/core/meta/types/pack.py index 68e560de..e380d6f0 100644 --- a/mashumaro/core/meta/types/pack.py +++ b/mashumaro/core/meta/types/pack.py @@ -494,7 +494,13 @@ def pack_tuple(spec: ValueSpec, args: Tuple[Type, ...]) -> Expression: def pack_named_tuple(spec: ValueSpec) -> Expression: - annotations = getattr(spec.type, "__annotations__", {}) + resolved = resolve_type_params(spec.origin_type, get_args(spec.type))[ + spec.origin_type + ] + annotations = { + k: resolved.get(v, v) + for k, v in getattr(spec.origin_type, "__annotations__", {}).items() + } fields = getattr(spec.type, "_fields", ()) packers = [] as_dict = spec.builder.get_config().namedtuple_as_dict @@ -619,7 +625,7 @@ def inner_expr( elif issubclass(spec.origin_type, str): return spec.expression elif issubclass(spec.origin_type, Tuple): # type: ignore - if is_named_tuple(spec.type): + if is_named_tuple(spec.origin_type): return pack_named_tuple(spec) elif ensure_generic_collection(spec): return pack_tuple(spec, args) diff --git a/mashumaro/core/meta/types/unpack.py b/mashumaro/core/meta/types/unpack.py index f99a3ea4..11f89500 100644 --- a/mashumaro/core/meta/types/unpack.py +++ b/mashumaro/core/meta/types/unpack.py @@ -580,7 +580,13 @@ def unpack_tuple(spec: ValueSpec, args: Tuple[Type, ...]) -> Expression: def unpack_named_tuple(spec: ValueSpec) -> Expression: - annotations = getattr(spec.type, "__annotations__", {}) + resolved = resolve_type_params(spec.origin_type, get_args(spec.type))[ + spec.origin_type + ] + annotations = { + k: resolved.get(v, v) + for k, v in getattr(spec.origin_type, "__annotations__", {}).items() + } fields = getattr(spec.type, "_fields", ()) defaults = getattr(spec.type, "_field_defaults", {}) unpackers = [] @@ -757,7 +763,7 @@ def inner_expr( f"for value in {spec.expression}])" ) elif issubclass(spec.origin_type, Tuple): # type: ignore - if is_named_tuple(spec.type): + if is_named_tuple(spec.origin_type): return unpack_named_tuple(spec) elif ensure_generic_collection(spec): return unpack_tuple(spec, args) diff --git a/tests/entities.py b/tests/entities.py index f3512a7a..60905042 100644 --- a/tests/entities.py +++ b/tests/entities.py @@ -3,16 +3,7 @@ from datetime import date, datetime from enum import Enum, Flag, IntEnum, IntFlag from os import PathLike -from typing import ( - Any, - Generic, - List, - NamedTuple, - NewType, - Optional, - TypeVar, - Union, -) +from typing import Any, Generic, List, NewType, Optional, TypeVar, Union try: from enum import StrEnum @@ -22,7 +13,7 @@ class StrEnum(str, Enum): pass -from typing_extensions import TypedDict +from typing_extensions import NamedTuple, TypedDict from mashumaro import DataClassDictMixin from mashumaro.config import TO_DICT_ADD_OMIT_NONE_FLAG, BaseConfig @@ -287,4 +278,9 @@ class MyNamedTupleWithOptional(NamedTuple): ) +class GenericNamedTuple(NamedTuple, Generic[T]): + x: T + y: int + + MyDatetimeNewType = NewType("MyDatetimeNewType", datetime) diff --git a/tests/test_data_types.py b/tests/test_data_types.py index 739ec09b..4bb5f92a 100644 --- a/tests/test_data_types.py +++ b/tests/test_data_types.py @@ -62,6 +62,7 @@ from .entities import ( CustomPath, DataClassWithoutMixin, + GenericNamedTuple, GenericSerializableList, GenericSerializableTypeDataClass, GenericTypedDict, @@ -1319,6 +1320,26 @@ class DataClass(DataClassDictMixin): assert DataClass().to_dict() == {"x": [None, 7]} +def test_unbound_generic_named_tuple(): + @dataclass + class DataClass(DataClassDictMixin): + x: GenericNamedTuple + + obj = DataClass(GenericNamedTuple("2023-01-22", 42)) + assert DataClass.from_dict({"x": ["2023-01-22", "42"]}) == obj + assert obj.to_dict() == {"x": ["2023-01-22", 42]} + + +def test_bound_generic_named_tuple(): + @dataclass + class DataClass(DataClassDictMixin): + x: GenericNamedTuple[date] + + obj = DataClass(GenericNamedTuple(date(2023, 1, 22), 42)) + assert DataClass.from_dict({"x": ["2023-01-22", "42"]}) == obj + assert obj.to_dict() == {"x": ["2023-01-22", 42]} + + def test_typed_dict_required_keys_with_optional(): @dataclass class DataClass(DataClassDictMixin):