From 17a7866456d0e07bb999695ae50f9f28c5913d4e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 11 May 2024 17:38:28 +0100 Subject: [PATCH] Make detection of TypeVar defaults robust to the CPython PEP-696 implementation --- src/cattrs/gen/_generics.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/cattrs/gen/_generics.py b/src/cattrs/gen/_generics.py index 877393b8..069c48c8 100644 --- a/src/cattrs/gen/_generics.py +++ b/src/cattrs/gen/_generics.py @@ -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) @@ -35,10 +59,7 @@ 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 @@ -46,9 +67,7 @@ def generate_mapping(cl: type, old_mapping: dict[str, type] = {}) -> dict[str, t 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: