]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-114053: Fix another edge case involving `get_type_hints`, PEP 695 and PEP...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Tue, 25 Jun 2024 16:30:08 +0000 (18:30 +0200)
committerGitHub <noreply@github.com>
Tue, 25 Jun 2024 16:30:08 +0000 (16:30 +0000)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Lib/test/test_typing.py
Lib/test/typinganndata/ann_module695.py
Lib/typing.py
Misc/NEWS.d/next/Library/2024-06-08-15-46-35.gh-issue-114053.Ub2XgJ.rst [new file with mode: 0644]

index 1ab7d35e2401c3a44cd921a5f0541ac9aa9a9d67..0b6cae2093d4644324e84dbb50951db3de293269 100644 (file)
@@ -4858,20 +4858,30 @@ class GenericTests(BaseTestCase):
             {'x': list[list[ForwardRef('X')]]}
         )
 
-    def test_pep695_generic_with_future_annotations(self):
+    def test_pep695_generic_class_with_future_annotations(self):
+        original_globals = dict(ann_module695.__dict__)
+
         hints_for_A = get_type_hints(ann_module695.A)
         A_type_params = ann_module695.A.__type_params__
         self.assertIs(hints_for_A["x"], A_type_params[0])
         self.assertEqual(hints_for_A["y"].__args__[0], Unpack[A_type_params[1]])
         self.assertIs(hints_for_A["z"].__args__[0], A_type_params[2])
 
+        # should not have changed as a result of the get_type_hints() calls!
+        self.assertEqual(ann_module695.__dict__, original_globals)
+
+    def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self):
         hints_for_B = get_type_hints(ann_module695.B)
-        self.assertEqual(hints_for_B.keys(), {"x", "y", "z"})
+        self.assertEqual(hints_for_B, {"x": int, "y": str, "z": bytes})
+
+    def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self):
+        hints_for_C = get_type_hints(ann_module695.C)
         self.assertEqual(
-            set(hints_for_B.values()) ^ set(ann_module695.B.__type_params__),
-            set()
+            set(hints_for_C.values()),
+            set(ann_module695.C.__type_params__)
         )
 
+    def test_pep_695_generic_function_with_future_annotations(self):
         hints_for_generic_function = get_type_hints(ann_module695.generic_function)
         func_t_params = ann_module695.generic_function.__type_params__
         self.assertEqual(
@@ -4882,6 +4892,54 @@ class GenericTests(BaseTestCase):
         self.assertIs(hints_for_generic_function["z"].__origin__, func_t_params[2])
         self.assertIs(hints_for_generic_function["zz"].__origin__, func_t_params[2])
 
+    def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self):
+        self.assertEqual(
+            set(get_type_hints(ann_module695.generic_function_2).values()),
+            set(ann_module695.generic_function_2.__type_params__)
+        )
+
+    def test_pep_695_generic_method_with_future_annotations(self):
+        hints_for_generic_method = get_type_hints(ann_module695.D.generic_method)
+        params = {
+            param.__name__: param
+            for param in ann_module695.D.generic_method.__type_params__
+        }
+        self.assertEqual(
+            hints_for_generic_method,
+            {"x": params["Foo"], "y": params["Bar"], "return": types.NoneType}
+        )
+
+    def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self):
+        self.assertEqual(
+            set(get_type_hints(ann_module695.D.generic_method_2).values()),
+            set(ann_module695.D.generic_method_2.__type_params__)
+        )
+
+    def test_pep_695_generics_with_future_annotations_nested_in_function(self):
+        results = ann_module695.nested()
+
+        self.assertEqual(
+            set(results.hints_for_E.values()),
+            set(results.E.__type_params__)
+        )
+        self.assertEqual(
+            set(results.hints_for_E_meth.values()),
+            set(results.E.generic_method.__type_params__)
+        )
+        self.assertNotEqual(
+            set(results.hints_for_E_meth.values()),
+            set(results.E.__type_params__)
+        )
+        self.assertEqual(
+            set(results.hints_for_E_meth.values()).intersection(results.E.__type_params__),
+            set()
+        )
+
+        self.assertEqual(
+            set(results.hints_for_generic_func.values()),
+            set(results.generic_func.__type_params__)
+        )
+
     def test_extended_generic_rules_subclassing(self):
         class T1(Tuple[T, KT]): ...
         class T2(Tuple[T, ...]): ...
index 2ede9fe382564f1a9a3728cad3e9ffb200e13518..b6f3b06bd5065f5ea54b5acafc28d122c64688ea 100644 (file)
@@ -17,6 +17,56 @@ class B[T, *Ts, **P]:
     z: P
 
 
+Eggs = int
+Spam = str
+
+
+class C[Eggs, **Spam]:
+    x: Eggs
+    y: Spam
+
+
 def generic_function[T, *Ts, **P](
     x: T, *y: *Ts, z: P.args, zz: P.kwargs
 ) -> None: ...
+
+
+def generic_function_2[Eggs, **Spam](x: Eggs, y: Spam): pass
+
+
+class D:
+    Foo = int
+    Bar = str
+
+    def generic_method[Foo, **Bar](
+        self, x: Foo, y: Bar
+    ) -> None: ...
+
+    def generic_method_2[Eggs, **Spam](self, x: Eggs, y: Spam): pass
+
+
+def nested():
+    from types import SimpleNamespace
+    from typing import get_type_hints
+
+    Eggs = bytes
+    Spam = memoryview
+
+
+    class E[Eggs, **Spam]:
+        x: Eggs
+        y: Spam
+
+        def generic_method[Eggs, **Spam](self, x: Eggs, y: Spam): pass
+
+
+    def generic_function[Eggs, **Spam](x: Eggs, y: Spam): pass
+
+
+    return SimpleNamespace(
+        E=E,
+        hints_for_E=get_type_hints(E),
+        hints_for_E_meth=get_type_hints(E.generic_method),
+        generic_func=generic_function,
+        hints_for_generic_func=get_type_hints(generic_function)
+    )
index d09b3208f9d691428ab7d67071dcf18824bc0eaa..fda0b2dd7260c179eec178be57ffaee14fa1e455 100644 (file)
@@ -1061,15 +1061,24 @@ class ForwardRef(_Final, _root=True):
                 globalns = getattr(
                     sys.modules.get(self.__forward_module__, None), '__dict__', globalns
                 )
+
+            # 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`!)
             if type_params:
-                # "Inject" type parameters into the local namespace
-                # (unless they are shadowed by assignments *in* the local namespace),
-                # as a way of emulating annotation scopes when calling `eval()`
-                locals_to_pass = {param.__name__: param for param in type_params} | localns
-            else:
-                locals_to_pass = localns
+                globalns, localns = dict(globalns), dict(localns)
+                for param in type_params:
+                    param_name = param.__name__
+                    if not self.__forward_is_class__ or param_name not in globalns:
+                        globalns[param_name] = param
+                        localns.pop(param_name, None)
+
             type_ = _type_check(
-                eval(self.__forward_code__, globalns, locals_to_pass),
+                eval(self.__forward_code__, globalns, localns),
                 "Forward references must evaluate to types.",
                 is_argument=self.__forward_is_argument__,
                 allow_special_forms=self.__forward_is_class__,
diff --git a/Misc/NEWS.d/next/Library/2024-06-08-15-46-35.gh-issue-114053.Ub2XgJ.rst b/Misc/NEWS.d/next/Library/2024-06-08-15-46-35.gh-issue-114053.Ub2XgJ.rst
new file mode 100644 (file)
index 0000000..8aea591
--- /dev/null
@@ -0,0 +1,4 @@
+Fix edge-case bug where :func:`typing.get_type_hints` would produce
+incorrect results if type parameters in a class scope were overridden by
+assignments in a class scope and ``from __future__ import annotations``
+semantics were enabled. Patch by Alex Waygood.