Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cattrs support for TypeVar with default (PEP696) #512

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ can now be used as decorators and have gained new features.
([#426](https://github.com/python-attrs/cattrs/issues/426) [#477](https://github.com/python-attrs/cattrs/pull/477))
- Add support for [PEP 695](https://peps.python.org/pep-0695/) type aliases.
([#452](https://github.com/python-attrs/cattrs/pull/452))
- Add support for [PEP 696](https://peps.python.org/pep-0696/) `TypeVar`s with defaults.
([#512](https://github.com/python-attrs/cattrs/pull/512))
- Add support for named tuples with type metadata ([`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)).
([#425](https://github.com/python-attrs/cattrs/issues/425) [#491](https://github.com/python-attrs/cattrs/pull/491))
- 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.
Expand Down
22 changes: 20 additions & 2 deletions src/cattrs/gen/_generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,27 @@ def generate_mapping(cl: type, old_mapping: dict[str, type] = {}) -> dict[str, t
if not hasattr(base, "__args__"):
continue
base_args = base.__args__
if not hasattr(base.__origin__, "__parameters__"):
if hasattr(base.__origin__, "__parameters__"):
base_params = base.__origin__.__parameters__
elif any(
getattr(base_arg, "__default__", None) is not None
for base_arg in base_args
):
# TypeVar with a default e.g. PEP 696
# https://www.python.org/dev/peps/pep-0696/
# Extract the defaults for the TypeVars and insert
# them into the mapping
mapping_params = [
(base_arg, base_arg.__default__)
for base_arg in base_args
# Note: None means no default was provided, since
# TypeVar("T", default=None) sets NoneType as the default
if getattr(base_arg, "__default__", None) is not None
]
base_params, base_args = zip(*mapping_params)
else:
continue
base_params = base.__origin__.__parameters__

for param, arg in zip(base_params, base_args):
mapping[param.__name__] = arg

Expand Down
50 changes: 50 additions & 0 deletions tests/test_generics_696.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Tests for generics under PEP 696 (type defaults)."""
from typing import Generic

import pytest
from attrs import define, fields
from typing_extensions import TypeVar

from cattrs.errors import StructureHandlerNotFoundError
from cattrs.gen import generate_mapping

T = TypeVar("T")
TD = TypeVar("TD", default=str)


def test_structure_typevar_default(genconverter):
"""Generics with defaulted TypeVars work."""

@define
class C(Generic[T]):
a: T

c_mapping = generate_mapping(C)
atype = fields(C).a.type
assert atype.__name__ not in c_mapping

with pytest.raises(StructureHandlerNotFoundError):
# Missing type for generic argument
genconverter.structure({"a": "1"}, C)

c_mapping = generate_mapping(C[str])
atype = fields(C[str]).a.type
assert c_mapping[atype.__name__] == str

assert genconverter.structure({"a": "1"}, C[str]) == C("1")

@define
class D(Generic[TD]):
a: TD

d_mapping = generate_mapping(D)
atype = fields(D).a.type
assert d_mapping[atype.__name__] == str

# Defaults to string
assert d_mapping[atype.__name__] == str
assert genconverter.structure({"a": "1"}, D) == D("1")

# But allows other types
assert genconverter.structure({"a": "1"}, D[str]) == D("1")
assert genconverter.structure({"a": 1}, D[int]) == D(1)
Loading