https://github.com/python/cpython/commit/089a324a4258fd5783b7e65acc29231d1042646e
commit: 089a324a4258fd5783b7e65acc29231d1042646e
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-08-13T06:47:47-07:00
summary:
gh-137226: Fix behavior of ForwardRef.evaluate with type_params (#137227)
The previous behavior was copied from earlier typing code. It works around the
way
typing.get_type_hints passes its namespaces, but I don't think the behavior is
logical
or correct.
files:
A Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py
M Lib/typing.py
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index c83a1573ccd3d1..bee019cd51591e 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -158,21 +158,13 @@ def evaluate(
# as a way of emulating annotation scopes when calling `eval()`
type_params = getattr(owner, "__type_params__", None)
- # type parameters require some special handling,
- # as they exist in their own scope
- # but `eval()` does not have a dedicated parameter for that scope.
- # For classes, names in type parameter scopes should override
- # names in the global scope (which here are called `localns`!),
- # but should in turn be overridden by names in the class scope
- # (which here are called `globalns`!)
+ # Type parameters exist in their own scope, which is logically
+ # between the locals and the globals. We simulate this by adding
+ # them to the globals.
if type_params is not None:
globals = dict(globals)
- locals = dict(locals)
for param in type_params:
- param_name = param.__name__
- if not self.__forward_is_class__ or param_name not in globals:
- globals[param_name] = param
- locals.pop(param_name, None)
+ globals[param.__name__] = param
if self.__extra_names__:
locals = {**locals, **self.__extra_names__}
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index ae0e73f08c5bd0..88e0d611647f28 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -1365,6 +1365,11 @@ def test_annotations_to_string(self):
class A:
pass
+TypeParamsAlias1 = int
+
+class TypeParamsSample[TypeParamsAlias1, TypeParamsAlias2]:
+ TypeParamsAlias2 = str
+
class TestForwardRefClass(unittest.TestCase):
def test_forwardref_instance_type_error(self):
@@ -1597,6 +1602,21 @@ class Gen[T]:
ForwardRef("alias").evaluate(owner=Gen, locals={"alias": str}), str
)
+ def test_evaluate_with_type_params_and_scope_conflict(self):
+ for is_class in (False, True):
+ with self.subTest(is_class=is_class):
+ fwdref1 = ForwardRef("TypeParamsAlias1",
owner=TypeParamsSample, is_class=is_class)
+ fwdref2 = ForwardRef("TypeParamsAlias2",
owner=TypeParamsSample, is_class=is_class)
+
+ self.assertIs(
+ fwdref1.evaluate(),
+ TypeParamsSample.__type_params__[0],
+ )
+ self.assertIs(
+ fwdref2.evaluate(),
+ TypeParamsSample.TypeParamsAlias2,
+ )
+
def test_fwdref_with_module(self):
self.assertIs(ForwardRef("Format", module="annotationlib").evaluate(),
Format)
self.assertIs(
diff --git a/Lib/typing.py b/Lib/typing.py
index 036636f7e0e6a8..8c1d265019bb94 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -2366,10 +2366,13 @@ def get_type_hints(obj, globalns=None, localns=None,
include_extras=False,
# *base_globals* first rather than *base_locals*.
# This only affects ForwardRefs.
base_globals, base_locals = base_locals, base_globals
+ type_params = base.__type_params__
+ base_globals, base_locals = _add_type_params_to_scope(
+ type_params, base_globals, base_locals, True)
for name, value in ann.items():
if isinstance(value, str):
value = _make_forward_ref(value, is_argument=False,
is_class=True)
- value = _eval_type(value, base_globals, base_locals,
base.__type_params__,
+ value = _eval_type(value, base_globals, base_locals, (),
format=format, owner=obj)
if value is None:
value = type(None)
@@ -2405,6 +2408,7 @@ def get_type_hints(obj, globalns=None, localns=None,
include_extras=False,
elif localns is None:
localns = globalns
type_params = getattr(obj, "__type_params__", ())
+ globalns, localns = _add_type_params_to_scope(type_params, globalns,
localns, False)
for name, value in hints.items():
if isinstance(value, str):
# class-level forward refs were handled above, this must be either
@@ -2414,13 +2418,27 @@ def get_type_hints(obj, globalns=None, localns=None,
include_extras=False,
is_argument=not isinstance(obj, types.ModuleType),
is_class=False,
)
- value = _eval_type(value, globalns, localns, type_params,
format=format, owner=obj)
+ value = _eval_type(value, globalns, localns, (), format=format,
owner=obj)
if value is None:
value = type(None)
hints[name] = value
return hints if include_extras else {k: _strip_annotations(t) for k, t in
hints.items()}
+# Add type parameters to the globals and locals scope. This is needed for
+# compatibility.
+def _add_type_params_to_scope(type_params, globalns, localns, is_class):
+ if not type_params:
+ return globalns, localns
+ globalns = dict(globalns)
+ localns = dict(localns)
+ for param in type_params:
+ if not is_class or param.__name__ not in globalns:
+ globalns[param.__name__] = param
+ localns.pop(param.__name__, None)
+ return globalns, localns
+
+
def _strip_annotations(t):
"""Strip the annotations from a given type."""
if isinstance(t, _AnnotatedAlias):
diff --git
a/Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst
b/Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst
new file mode 100644
index 00000000000000..522943cdd376dc
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst
@@ -0,0 +1,3 @@
+Fix behavior of :meth:`annotationlib.ForwardRef.evaluate` when the
+*type_params* parameter is passed and the name of a type param is also
+present in an enclosing scope.
_______________________________________________
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]