https://github.com/python/cpython/commit/5b56daa9d728fa38a1fb6d8a823d795081f067d8
commit: 5b56daa9d728fa38a1fb6d8a823d795081f067d8
branch: main
author: Victorien <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-07-05T06:55:39-07:00
summary:
gh-130870: Preserve `GenericAlias` subclasses in `typing.get_type_hints()`
(#131583)
files:
A Misc/NEWS.d/next/Library/2025-06-10-10-22-18.gh-issue-130870.JipqbO.rst
M Lib/test/test_typing.py
M Lib/typing.py
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index ef02e8202fc829..bef6773ad6cb2f 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -1605,7 +1605,10 @@ def func1(*args: *Ts): pass
self.assertEqual(gth(func1), {'args': Unpack[Ts]})
def func2(*args: *tuple[int, str]): pass
- self.assertEqual(gth(func2), {'args': Unpack[tuple[int, str]]})
+ hint = gth(func2)['args']
+ self.assertIsInstance(hint, types.GenericAlias)
+ self.assertEqual(hint.__args__[0], int)
+ self.assertIs(hint.__unpacked__, True)
class CustomVariadic(Generic[*Ts]): pass
@@ -1620,7 +1623,10 @@ def func1(*args: '*Ts'): pass
{'args': Unpack[Ts]})
def func2(*args: '*tuple[int, str]'): pass
- self.assertEqual(gth(func2), {'args': Unpack[tuple[int, str]]})
+ hint = gth(func2)['args']
+ self.assertIsInstance(hint, types.GenericAlias)
+ self.assertEqual(hint.__args__[0], int)
+ self.assertIs(hint.__unpacked__, True)
class CustomVariadic(Generic[*Ts]): pass
@@ -7114,6 +7120,24 @@ def add_right(self, node: 'Node[T]' = None):
right_hints = get_type_hints(t.add_right, globals(), locals())
self.assertEqual(right_hints['node'], Node[T])
+ def test_get_type_hints_preserve_generic_alias_subclasses(self):
+ # https://github.com/python/cpython/issues/130870
+ # A real world example of this is `collections.abc.Callable`. When
parameterized,
+ # the result is a subclass of `types.GenericAlias`.
+ class MyAlias(types.GenericAlias):
+ pass
+
+ class MyClass:
+ def __class_getitem__(cls, args):
+ return MyAlias(cls, args)
+
+ # Using a forward reference is important, otherwise it works as
expected.
+ # `y` tests that the `GenericAlias` subclass is preserved when
stripping `Annotated`.
+ def func(x: MyClass['int'], y: MyClass[Annotated[int, ...]]): ...
+
+ assert isinstance(get_type_hints(func)['x'], MyAlias)
+ assert isinstance(get_type_hints(func)['y'], MyAlias)
+
class GetUtilitiesTestCase(TestCase):
def test_get_origin(self):
diff --git a/Lib/typing.py b/Lib/typing.py
index ed1dd4fc6413a5..4ebf0eb92f589f 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -407,6 +407,17 @@ def inner(*args, **kwds):
return decorator
+def _rebuild_generic_alias(alias: GenericAlias, args: tuple[object, ...]) ->
GenericAlias:
+ is_unpacked = alias.__unpacked__
+ if _should_unflatten_callable_args(alias, args):
+ t = alias.__origin__[(args[:-1], args[-1])]
+ else:
+ t = alias.__origin__[args]
+ if is_unpacked:
+ t = Unpack[t]
+ return t
+
+
def _deprecation_warning_for_no_type_params_passed(funcname: str) -> None:
import warnings
@@ -454,25 +465,20 @@ def _eval_type(t, globalns, localns,
type_params=_sentinel, *, recursive_guard=f
_make_forward_ref(arg) if isinstance(arg, str) else arg
for arg in t.__args__
)
- is_unpacked = t.__unpacked__
- if _should_unflatten_callable_args(t, args):
- t = t.__origin__[(args[:-1], args[-1])]
- else:
- t = t.__origin__[args]
- if is_unpacked:
- t = Unpack[t]
+ else:
+ args = t.__args__
ev_args = tuple(
_eval_type(
a, globalns, localns, type_params,
recursive_guard=recursive_guard,
format=format, owner=owner,
)
- for a in t.__args__
+ for a in args
)
if ev_args == t.__args__:
return t
if isinstance(t, GenericAlias):
- return GenericAlias(t.__origin__, ev_args)
+ return _rebuild_generic_alias(t, ev_args)
if isinstance(t, Union):
return functools.reduce(operator.or_, ev_args)
else:
@@ -2404,7 +2410,7 @@ def _strip_annotations(t):
stripped_args = tuple(_strip_annotations(a) for a in t.__args__)
if stripped_args == t.__args__:
return t
- return GenericAlias(t.__origin__, stripped_args)
+ return _rebuild_generic_alias(t, stripped_args)
if isinstance(t, Union):
stripped_args = tuple(_strip_annotations(a) for a in t.__args__)
if stripped_args == t.__args__:
diff --git
a/Misc/NEWS.d/next/Library/2025-06-10-10-22-18.gh-issue-130870.JipqbO.rst
b/Misc/NEWS.d/next/Library/2025-06-10-10-22-18.gh-issue-130870.JipqbO.rst
new file mode 100644
index 00000000000000..64173285e08417
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-06-10-10-22-18.gh-issue-130870.JipqbO.rst
@@ -0,0 +1,2 @@
+Preserve :class:`types.GenericAlias` subclasses in
+:func:`typing.get_type_hints`
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]