Skip to content

Commit

Permalink
Add support for generic TypedDict following python/cpython#89026
Browse files Browse the repository at this point in the history
  • Loading branch information
Fatal1ty committed Jan 22, 2023
1 parent 18735dd commit 985e91a
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 4 deletions.
10 changes: 8 additions & 2 deletions mashumaro/core/meta/types/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,13 @@ def pack_named_tuple(spec: ValueSpec) -> Expression:


def pack_typed_dict(spec: ValueSpec) -> Expression:
annotations = 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 spec.origin_type.__annotations__.items()
}
all_keys = list(annotations.keys())
required_keys = getattr(spec.type, "__required_keys__", all_keys)
optional_keys = getattr(spec.type, "__optional_keys__", [])
Expand Down Expand Up @@ -637,7 +643,7 @@ def inner_expr(
f'{{{inner_expr(0, "key")}: {inner_expr(1, v_type=int)} '
f"for key, value in {spec.expression}.items()}}"
)
elif is_typed_dict(spec.type):
elif is_typed_dict(spec.origin_type):
return pack_typed_dict(spec)
elif ensure_generic_mapping(spec, args, typing.Mapping):
return (
Expand Down
10 changes: 8 additions & 2 deletions mashumaro/core/meta/types/unpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,13 @@ def unpack_named_tuple(spec: ValueSpec) -> Expression:


def unpack_typed_dict(spec: ValueSpec) -> Expression:
annotations = 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 spec.origin_type.__annotations__.items()
}
all_keys = list(annotations.keys())
required_keys = getattr(spec.type, "__required_keys__", all_keys)
optional_keys = getattr(spec.type, "__optional_keys__", [])
Expand Down Expand Up @@ -786,7 +792,7 @@ def inner_expr(
f"{inner_expr(1, v_type=int)} "
f"for key, value in {spec.expression}.items()}})"
)
elif is_typed_dict(spec.type):
elif is_typed_dict(spec.origin_type):
return unpack_typed_dict(spec)
elif ensure_generic_mapping(spec, args, typing.Mapping):
return (
Expand Down
5 changes: 5 additions & 0 deletions tests/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ class TypedDictOptionalKeysWithOptional(TypedDict, total=False):
y: float


class GenericTypedDict(TypedDict, Generic[T]):
x: T
y: int


class MyNamedTuple(NamedTuple):
i: int
f: float
Expand Down
21 changes: 21 additions & 0 deletions tests/test_data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
DataClassWithoutMixin,
GenericSerializableList,
GenericSerializableTypeDataClass,
GenericTypedDict,
MutableString,
MyDataClass,
MyDataClassWithUnion,
Expand Down Expand Up @@ -1346,6 +1347,26 @@ class DataClass(DataClassDictMixin):
assert obj.to_dict()


def test_unbound_generic_typed_dict():
@dataclass
class DataClass(DataClassDictMixin):
x: GenericTypedDict

obj = DataClass({"x": "2023-01-22", "y": 42})
assert DataClass.from_dict({"x": {"x": "2023-01-22", "y": "42"}}) == obj
assert obj.to_dict() == {"x": {"x": "2023-01-22", "y": 42}}


def test_bound_generic_typed_dict():
@dataclass
class DataClass(DataClassDictMixin):
x: GenericTypedDict[date]

obj = DataClass({"x": date(2023, 1, 22), "y": 42})
assert DataClass.from_dict({"x": {"x": "2023-01-22", "y": "42"}}) == obj
assert obj.to_dict() == {"x": {"x": "2023-01-22", "y": 42}}


def test_dataclass_with_init_false_field():
@dataclass
class DataClass(DataClassDictMixin):
Expand Down

0 comments on commit 985e91a

Please sign in to comment.