Skip to content

Commit 2b10bcb

Browse files
Add cattrs support for TypeVar with default (PEP696) (#512)
* Add support for TypeVar with default (PEP696) * Add changelog entry * Fix test
1 parent 04e09aa commit 2b10bcb

File tree

3 files changed

+72
-2
lines changed

3 files changed

+72
-2
lines changed

HISTORY.md

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ can now be used as decorators and have gained new features.
3333
([#426](https://github.com/python-attrs/cattrs/issues/426) [#477](https://github.com/python-attrs/cattrs/pull/477))
3434
- Add support for [PEP 695](https://peps.python.org/pep-0695/) type aliases.
3535
([#452](https://github.com/python-attrs/cattrs/pull/452))
36+
- Add support for [PEP 696](https://peps.python.org/pep-0696/) `TypeVar`s with defaults.
37+
([#512](https://github.com/python-attrs/cattrs/pull/512))
3638
- Add support for named tuples with type metadata ([`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)).
3739
([#425](https://github.com/python-attrs/cattrs/issues/425) [#491](https://github.com/python-attrs/cattrs/pull/491))
3840
- The `include_subclasses` strategy now fetches the member hooks from the converter (making use of converter defaults) if overrides are not provided, instead of generating new hooks with no overrides.

src/cattrs/gen/_generics.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,27 @@ def generate_mapping(cl: type, old_mapping: dict[str, type] = {}) -> dict[str, t
3333
if not hasattr(base, "__args__"):
3434
continue
3535
base_args = base.__args__
36-
if not hasattr(base.__origin__, "__parameters__"):
36+
if hasattr(base.__origin__, "__parameters__"):
37+
base_params = base.__origin__.__parameters__
38+
elif any(
39+
getattr(base_arg, "__default__", None) is not None
40+
for base_arg in base_args
41+
):
42+
# TypeVar with a default e.g. PEP 696
43+
# https://www.python.org/dev/peps/pep-0696/
44+
# Extract the defaults for the TypeVars and insert
45+
# them into the mapping
46+
mapping_params = [
47+
(base_arg, base_arg.__default__)
48+
for base_arg in base_args
49+
# Note: None means no default was provided, since
50+
# TypeVar("T", default=None) sets NoneType as the default
51+
if getattr(base_arg, "__default__", None) is not None
52+
]
53+
base_params, base_args = zip(*mapping_params)
54+
else:
3755
continue
38-
base_params = base.__origin__.__parameters__
56+
3957
for param, arg in zip(base_params, base_args):
4058
mapping[param.__name__] = arg
4159

tests/test_generics_696.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Tests for generics under PEP 696 (type defaults)."""
2+
from typing import Generic
3+
4+
import pytest
5+
from attrs import define, fields
6+
from typing_extensions import TypeVar
7+
8+
from cattrs.errors import StructureHandlerNotFoundError
9+
from cattrs.gen import generate_mapping
10+
11+
T = TypeVar("T")
12+
TD = TypeVar("TD", default=str)
13+
14+
15+
def test_structure_typevar_default(genconverter):
16+
"""Generics with defaulted TypeVars work."""
17+
18+
@define
19+
class C(Generic[T]):
20+
a: T
21+
22+
c_mapping = generate_mapping(C)
23+
atype = fields(C).a.type
24+
assert atype.__name__ not in c_mapping
25+
26+
with pytest.raises(StructureHandlerNotFoundError):
27+
# Missing type for generic argument
28+
genconverter.structure({"a": "1"}, C)
29+
30+
c_mapping = generate_mapping(C[str])
31+
atype = fields(C[str]).a.type
32+
assert c_mapping[atype.__name__] == str
33+
34+
assert genconverter.structure({"a": "1"}, C[str]) == C("1")
35+
36+
@define
37+
class D(Generic[TD]):
38+
a: TD
39+
40+
d_mapping = generate_mapping(D)
41+
atype = fields(D).a.type
42+
assert d_mapping[atype.__name__] == str
43+
44+
# Defaults to string
45+
assert d_mapping[atype.__name__] == str
46+
assert genconverter.structure({"a": "1"}, D) == D("1")
47+
48+
# But allows other types
49+
assert genconverter.structure({"a": "1"}, D[str]) == D("1")
50+
assert genconverter.structure({"a": 1}, D[int]) == D(1)

0 commit comments

Comments
 (0)