Skip to content
Merged
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
33 changes: 26 additions & 7 deletions src/cattrs/gen/_generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@
from .._compat import get_args, get_origin, is_generic


def _tvar_has_default(tvar) -> bool:
"""Does `tvar` have a default?

In CPython 3.13+ and typing_extensions>=4.12.0:
- TypeVars have a `no_default()` method for detecting
if a TypeVar has a default
- TypeVars with `default=None` have `__default__` set to `None`
- TypeVars with no `default` parameter passed
have `__default__` set to `typing(_extensions).NoDefault

On typing_exensions<4.12.0:
- TypeVars do not have a `no_default()` method for detecting
if a TypeVar has a default
- TypeVars with `default=None` have `__default__` set to `NoneType`
- TypeVars with no `default` parameter passed
have `__default__` set to `typing(_extensions).NoDefault
"""
try:
return tvar.has_default()
except AttributeError:
# compatibility for typing_extensions<4.12.0
return getattr(tvar, "__default__", None) is not None


def generate_mapping(cl: type, old_mapping: dict[str, type] = {}) -> dict[str, type]:
"""Generate a mapping of typevars to actual types for a generic class."""
mapping = dict(old_mapping)
Expand Down Expand Up @@ -35,20 +59,15 @@ def generate_mapping(cl: type, old_mapping: dict[str, type] = {}) -> dict[str, t
base_args = base.__args__
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
):
elif any(_tvar_has_default(base_arg) 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
if _tvar_has_default(base_arg)
]
base_params, base_args = zip(*mapping_params)
else:
Expand Down