1from __future__ import annotations
2
3from typing import TypeVar
4
5from .._compat import get_args, get_origin, is_generic
6
7
8def _tvar_has_default(tvar) -> bool:
9 """Does `tvar` have a default?
10
11 In CPython 3.13+ and typing_extensions>=4.12.0:
12 - TypeVars have a `no_default()` method for detecting
13 if a TypeVar has a default
14 - TypeVars with `default=None` have `__default__` set to `None`
15 - TypeVars with no `default` parameter passed
16 have `__default__` set to `typing(_extensions).NoDefault
17
18 On typing_exensions<4.12.0:
19 - TypeVars do not have a `no_default()` method for detecting
20 if a TypeVar has a default
21 - TypeVars with `default=None` have `__default__` set to `NoneType`
22 - TypeVars with no `default` parameter passed
23 have `__default__` set to `typing(_extensions).NoDefault
24 """
25 try:
26 return tvar.has_default()
27 except AttributeError:
28 # compatibility for typing_extensions<4.12.0
29 return getattr(tvar, "__default__", None) is not None
30
31
32def generate_mapping(cl: type, old_mapping: dict[str, type] = {}) -> dict[str, type]:
33 """Generate a mapping of typevars to actual types for a generic class."""
34 mapping = dict(old_mapping)
35
36 origin = get_origin(cl)
37
38 if origin is not None:
39 # To handle the cases where classes in the typing module are using
40 # the GenericAlias structure but aren't a Generic and hence
41 # end up in this function but do not have an `__parameters__`
42 # attribute. These classes are interface types, for example
43 # `typing.Hashable`.
44 parameters = getattr(get_origin(cl), "__parameters__", None)
45 if parameters is None:
46 return dict(old_mapping)
47
48 for p, t in zip(parameters, get_args(cl)):
49 if isinstance(t, TypeVar):
50 continue
51 mapping[p.__name__] = t
52
53 elif is_generic(cl):
54 # Origin is None, so this may be a subclass of a generic class.
55 orig_bases = cl.__orig_bases__
56 for base in orig_bases:
57 if not hasattr(base, "__args__"):
58 continue
59 base_args = base.__args__
60 if hasattr(base.__origin__, "__parameters__"):
61 base_params = base.__origin__.__parameters__
62 elif any(_tvar_has_default(base_arg) for base_arg in base_args):
63 # TypeVar with a default e.g. PEP 696
64 # https://www.python.org/dev/peps/pep-0696/
65 # Extract the defaults for the TypeVars and insert
66 # them into the mapping
67 mapping_params = [
68 (base_arg, base_arg.__default__)
69 for base_arg in base_args
70 if _tvar_has_default(base_arg)
71 ]
72 base_params, base_args = zip(*mapping_params)
73 else:
74 continue
75
76 for param, arg in zip(base_params, base_args):
77 mapping[param.__name__] = arg
78
79 return mapping