Skip to content

Commit 56b54ec

Browse files
committed
type aliases: support generics
1 parent 5c5876e commit 56b54ec

File tree

7 files changed

+125
-86
lines changed

7 files changed

+125
-86
lines changed

pdm.lock

+43-46
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cattrs/_compat.py

-26
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
Optional,
2626
Protocol,
2727
Tuple,
28-
TypedDict,
2928
Union,
3029
_AnnotatedAlias,
3130
_GenericAlias,
@@ -53,12 +52,9 @@
5352
"fields_dict",
5453
"ExceptionGroup",
5554
"ExtensionsTypedDict",
56-
"get_type_alias_base",
5755
"has",
58-
"is_type_alias",
5956
"is_typeddict",
6057
"TypeAlias",
61-
"TypedDict",
6258
]
6359

6460
try:
@@ -112,20 +108,6 @@ def is_typeddict(cls: Any):
112108
return _is_typeddict(getattr(cls, "__origin__", cls))
113109

114110

115-
def is_type_alias(type: Any) -> bool:
116-
"""Is this a PEP 695 type alias?"""
117-
return False
118-
119-
120-
def get_type_alias_base(type: Any) -> Any:
121-
"""
122-
What is this a type alias of?
123-
124-
Works only on 3.12+.
125-
"""
126-
return type.__value__
127-
128-
129111
def has(cls):
130112
return hasattr(cls, "__attrs_attrs__") or hasattr(cls, "__dataclass_fields__")
131113

@@ -273,14 +255,6 @@ def is_tuple(type):
273255
)
274256

275257

276-
if sys.version_info >= (3, 12):
277-
from typing import TypeAliasType
278-
279-
def is_type_alias(type: Any) -> bool:
280-
"""Is this a PEP 695 type alias?"""
281-
return isinstance(type, TypeAliasType)
282-
283-
284258
if sys.version_info >= (3, 10):
285259

286260
def is_union_type(obj):

src/cattrs/converters.py

+6-11
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
get_final_base,
3131
get_newtype_base,
3232
get_origin,
33-
get_type_alias_base,
3433
has,
3534
has_with_generic,
3635
is_annotated,
@@ -48,7 +47,6 @@
4847
is_protocol,
4948
is_sequence,
5049
is_tuple,
51-
is_type_alias,
5250
is_typeddict,
5351
is_union_type,
5452
signature,
@@ -92,6 +90,11 @@
9290
from .gen.typeddicts import make_dict_structure_fn as make_typeddict_dict_struct_fn
9391
from .gen.typeddicts import make_dict_unstructure_fn as make_typeddict_dict_unstruct_fn
9492
from .literals import is_literal_containing_enums
93+
from .typealiases import (
94+
get_type_alias_base,
95+
is_type_alias,
96+
type_alias_structure_factory,
97+
)
9598
from .types import SimpleStructureHook
9699

97100
__all__ = ["UnstructureStrategy", "BaseConverter", "Converter", "GenConverter"]
@@ -259,7 +262,7 @@ def __init__(
259262
),
260263
(is_generic_attrs, self._gen_structure_generic, True),
261264
(lambda t: get_newtype_base(t) is not None, self._structure_newtype),
262-
(is_type_alias, self._find_type_alias_structure_hook, True),
265+
(is_type_alias, type_alias_structure_factory, "extended"),
263266
(
264267
lambda t: get_final_base(t) is not None,
265268
self._structure_final_factory,
@@ -699,14 +702,6 @@ def _structure_newtype(self, val: UnstructuredValue, type) -> StructuredValue:
699702
base = get_newtype_base(type)
700703
return self.get_structure_hook(base)(val, base)
701704

702-
def _find_type_alias_structure_hook(self, type: Any) -> StructureHook:
703-
base = get_type_alias_base(type)
704-
res = self.get_structure_hook(base)
705-
if res == self._structure_call:
706-
# we need to replace the type arg of `structure_call`
707-
return lambda v, _, __base=base: __base(v)
708-
return lambda v, _, __base=base: res(v, __base)
709-
710705
def _structure_final_factory(self, type):
711706
base = get_final_base(type)
712707
res = self.get_structure_hook(base)

src/cattrs/gen/typeddicts.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import re
44
import sys
5-
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
5+
from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, TypeVar
66

77
from attrs import NOTHING, Attribute
88
from typing_extensions import _TypedDictMeta
@@ -20,7 +20,6 @@ def get_annots(cl) -> dict[str, Any]:
2020

2121

2222
from .._compat import (
23-
TypedDict,
2423
get_full_type_hints,
2524
get_notrequired_base,
2625
get_origin,

src/cattrs/typealiases.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Utilities for type aliases."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from typing import TYPE_CHECKING, Any
7+
8+
from ._compat import is_generic
9+
from ._generics import deep_copy_with
10+
from .dispatch import StructureHook
11+
from .gen._generics import generate_mapping
12+
13+
if TYPE_CHECKING:
14+
from .converters import BaseConverter
15+
16+
__all__ = ["is_type_alias", "get_type_alias_base", "type_alias_structure_factory"]
17+
18+
if sys.version_info >= (3, 12):
19+
from types import GenericAlias
20+
from typing import TypeAliasType
21+
22+
def is_type_alias(type: Any) -> bool:
23+
"""Is this a PEP 695 type alias?"""
24+
return isinstance(
25+
type.__origin__ if type.__class__ is GenericAlias else type, TypeAliasType
26+
)
27+
28+
else:
29+
30+
def is_type_alias(type: Any) -> bool:
31+
"""Is this a PEP 695 type alias?"""
32+
return False
33+
34+
35+
def get_type_alias_base(type: Any) -> Any:
36+
"""
37+
What is this a type alias of?
38+
39+
Works only on 3.12+.
40+
"""
41+
return type.__value__
42+
43+
44+
def type_alias_structure_factory(type: Any, converter: BaseConverter) -> StructureHook:
45+
base = get_type_alias_base(type)
46+
if is_generic(type):
47+
mapping = generate_mapping(type)
48+
if base.__name__ in mapping:
49+
# Probably just type T = T
50+
base = mapping[base.__name__]
51+
else:
52+
base = deep_copy_with(base, mapping)
53+
res = converter.get_structure_hook(base)
54+
if res == converter._structure_call:
55+
# we need to replace the type arg of `structure_call`
56+
return lambda v, _, __base=base: __base(v)
57+
return lambda v, _, __base=base: res(v, __base)

tests/test_generics_695.py

+16
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,19 @@ def structure_testclass(val, type):
8989

9090
type TestAlias = TestClass
9191
assert converter.structure(None, TestAlias) is TestClass
92+
93+
94+
def test_generic_type_alias(converter: BaseConverter):
95+
"""Generic type aliases work.
96+
97+
See https://docs.python.org/3/reference/compound_stmts.html#generic-type-aliases
98+
for details.
99+
"""
100+
101+
type Gen1[T] = T
102+
103+
assert converter.structure("1", Gen1[int]) == 1
104+
105+
type Gen2[K, V] = dict[K, V]
106+
107+
assert converter.structure({"a": "1"}, Gen2[str, int]) == {"a": 1}

tests/test_v.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
Optional,
99
Sequence,
1010
Tuple,
11+
TypedDict,
1112
)
1213

1314
from attrs import Factory, define, field
1415
from pytest import fixture, raises
1516

1617
from cattrs import Converter, transform_error
17-
from cattrs._compat import Mapping, TypedDict
18+
from cattrs._compat import Mapping
1819
from cattrs.errors import IterableValidationError
1920
from cattrs.gen import make_dict_structure_fn
2021
from cattrs.v import format_exception

0 commit comments

Comments
 (0)