11# type: ignore
22import collections
33import sys
4+ import types
45import typing
5- from typing import _AnnotatedAlias # noqa: F401
6+ from typing import _AnnotatedAlias , _GenericAlias # noqa: F401
67
78try :
89 from typing_extensions import get_type_hints as _get_type_hints
@@ -22,6 +23,8 @@ def get_type_hints(obj):
2223 return _get_type_hints (obj , include_extras = True )
2324
2425
26+ PY_312PLUS = sys .version_info >= (3 , 12 )
27+
2528# The `is_class` argument was new in 3.11, but was backported to 3.9 and 3.10.
2629# It's _likely_ to be available for 3.9/3.10, but may not be. Easiest way to
2730# check is to try it and see. This check can be removed when we drop support
@@ -79,23 +82,42 @@ def _apply_params(obj, mapping):
7982def _get_class_mro_and_typevar_mappings (obj ):
8083 mapping = {}
8184
82- if isinstance (obj , type ):
85+ # in Python 3.10 a natively produced 'types.GenericAlias' (e.g. 'list[int]', or the
86+ # 'Base[int]' produced when a 'Generic' subclass inherits a builtin's
87+ # '__class_getitem__') satisfies 'isinstance(_, type)', unlike on 3.11+. we still
88+ # want to treat those as parametrised aliases, not bare classes
89+ if isinstance (obj , type ) and not isinstance (obj , types .GenericAlias ):
8390 cls = obj
8491 else :
8592 cls = obj .__origin__
8693
8794 def inner (c , scope ):
88- if isinstance (c , type ):
95+ if isinstance (c , type ) and not isinstance ( c , types . GenericAlias ) :
8996 cls = c
9097 new_scope = {}
9198 else :
92- cls = getattr ( c , "__origin__" , None )
99+ cls = typing . get_origin ( c )
93100 if cls in (None , object , typing .Generic ) or cls in mapping :
94101 return
95- params = cls .__parameters__
96- args = tuple (_apply_params (a , scope ) for a in c .__args__ )
97- assert len (params ) == len (args )
98- mapping [cls ] = new_scope = dict (zip (params , args ))
102+
103+ if hasattr (cls , "__parameters__" ):
104+ # 'cls' carries its own type vars. This covers both ordinary
105+ # 'typing._GenericAlias' bases and the 'types.GenericAlias' that
106+ # get produced when a user-defined 'Generic' subclass inherits a
107+ # builtin's '__class_getitem__' (e.g. 'class Base(Mapping[str, T])',
108+ # whose 'Base[...]' is a 'types.GenericAlias'). Map cls's own
109+ # type vars onto the resolved args, applying '_apply_params' so any
110+ # outer bindings in 'scope' (e.g. 'U -> int') are propagated.
111+ params = cls .__parameters__
112+ args = tuple (_apply_params (a , scope ) for a in typing .get_args (c ))
113+ assert len (params ) == len (args )
114+ new_scope = dict (zip (params , args ))
115+ else :
116+ # a true built-in generic (e.g. 'collections.abc.Mapping[str, T]')
117+ # whose '__origin__' has no '__parameters__'; the unresolved type
118+ # vars and args live on the alias itself, not the origin.
119+ new_scope = dict (zip (c .__parameters__ , typing .get_args (c )))
120+ mapping [cls ] = new_scope
99121
100122 if issubclass (cls , typing .Generic ):
101123 bases = getattr (cls , "__orig_bases__" , cls .__bases__ )
@@ -133,6 +155,11 @@ def get_class_annotations(obj):
133155
134156 mapping = typevar_mappings .get (cls )
135157 cls_locals = dict (vars (cls ))
158+
159+ if PY_312PLUS :
160+ # resolve type parameters (e.g. class Foo[T]: pass)
161+ cls_locals .update ({p .__name__ : p for p in cls .__type_params__ })
162+
136163 try :
137164 cls_module = cls .__module__
138165 except AttributeError :
@@ -315,3 +342,39 @@ def inner(obj):
315342def rebuild (cls , kwargs ):
316343 """Used to unpickle Structs with keyword-only fields"""
317344 return cls (** kwargs )
345+
346+
347+ def convert_generic_alias (origin , args ):
348+ # subscribed typing._GenericAlias instances are cached within the typing module
349+ # we make use of this fact, by storing a __msgspec_cache__ attribute on the
350+ # subscribed instance. only subscribed types are cached, so
351+ # 'typing._GenericAlias(list, int) is typing._GenericAlias(list, int)' would be
352+ # false.
353+ # to achieve the same behaviour when re-creating a typing._GenericAlias from a
354+ # types.GenericAlias, we first construct a temporary *unbound*
355+ # typing._GenericAlias, on which we then call __getattr__. effectively doing
356+ # typing._GenericAlias(list, T)[int], for which
357+ # 'typing._GenericAlias(list, T)[int] is typing._GenericAlias(list, T)[int]'
358+ # holds true
359+ try :
360+ params = origin .__parameters__
361+ except AttributeError :
362+ if not isinstance (origin , type ):
363+ # a special form such as 'typing.Literal', whose args are values rather than
364+ # type parameters. Ordinary subscription yields the canonical, interned
365+ # alias of the right subclass (e.g. 'typing.Literal[...]' -> '_LiteralGenericAlias').
366+ return origin [args ]
367+
368+ # a non-generic class with type arguments. only reachable for e.g.
369+ # manually-built 'types.GenericAlias' instances and is probably nonsense or at
370+ # least not somthing we can meaningfully represent, so complain about it here,
371+ # rather than silently dropping the arguments.
372+ raise TypeError (f"{ origin .__name__ !r} is not a generic type" )
373+
374+ # a regularly-parametrised generic. Create a new typing._GenericAlias with the
375+ # origin's unbound type params (e.g. for a 'Mapping[str, int]' this is a
376+ # '_GenericAlias(Mapping, (~K, ~V))'), then bind it to the concrete args by
377+ # subscripting with the args *tuple* (i.e. 'alias[(int, str)]', not
378+ # 'alias[int, str]'), so generics with more than one type var work
379+ alias = _GenericAlias (origin , params )
380+ return alias [args ]
0 commit comments