]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-137226: Fix behavior of ForwardRef.evaluate with type_params (#137227)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Wed, 13 Aug 2025 13:47:47 +0000 (06:47 -0700)
committerGitHub <noreply@github.com>
Wed, 13 Aug 2025 13:47:47 +0000 (06:47 -0700)
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.

Lib/annotationlib.py
Lib/test/test_annotationlib.py
Lib/typing.py
Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst [new file with mode: 0644]

index c83a1573ccd3d1ee70069860a365d45c75ed4805..bee019cd51591ec3b8c5057847f23514758fa15b 100644 (file)
@@ -158,21 +158,13 @@ class ForwardRef:
             # 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__}
 
index ae0e73f08c5bd0909f8f998134a2690ece486330..88e0d611647f2844828338e33efb2498d7fc4d11 100644 (file)
@@ -1365,6 +1365,11 @@ class TestAnnotationsToString(unittest.TestCase):
 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 TestForwardRefClass(unittest.TestCase):
             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(
index 036636f7e0e6a8401dba107a8ab11c5fd92fd55c..8c1d265019bb94c7b45ab0099617ce2b75813e09 100644 (file)
@@ -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 (file)
index 0000000..522943c
--- /dev/null
@@ -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.