mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 21:51:50 +00:00 
			
		
		
		
	[3.13] gh-114053: Fix another edge case involving get_type_hints, PEP 695 and PEP 563 (GH-120272) (#121003)
				
					
				
			Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
		
							parent
							
								
									899dfbaf0e
								
							
						
					
					
						commit
						f4f8a714b5
					
				
					 4 changed files with 132 additions and 11 deletions
				
			
		|  | @ -4858,20 +4858,30 @@ def f(x: X): ... | ||||||
|             {'x': list[list[ForwardRef('X')]]} |             {'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) |         hints_for_A = get_type_hints(ann_module695.A) | ||||||
|         A_type_params = ann_module695.A.__type_params__ |         A_type_params = ann_module695.A.__type_params__ | ||||||
|         self.assertIs(hints_for_A["x"], A_type_params[0]) |         self.assertIs(hints_for_A["x"], A_type_params[0]) | ||||||
|         self.assertEqual(hints_for_A["y"].__args__[0], Unpack[A_type_params[1]]) |         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]) |         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) |         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( |         self.assertEqual( | ||||||
|             set(hints_for_B.values()) ^ set(ann_module695.B.__type_params__), |             set(hints_for_C.values()), | ||||||
|             set() |             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) |         hints_for_generic_function = get_type_hints(ann_module695.generic_function) | ||||||
|         func_t_params = ann_module695.generic_function.__type_params__ |         func_t_params = ann_module695.generic_function.__type_params__ | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|  | @ -4882,6 +4892,54 @@ def test_pep695_generic_with_future_annotations(self): | ||||||
|         self.assertIs(hints_for_generic_function["z"].__origin__, func_t_params[2]) |         self.assertIs(hints_for_generic_function["z"].__origin__, func_t_params[2]) | ||||||
|         self.assertIs(hints_for_generic_function["zz"].__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): |     def test_extended_generic_rules_subclassing(self): | ||||||
|         class T1(Tuple[T, KT]): ... |         class T1(Tuple[T, KT]): ... | ||||||
|         class T2(Tuple[T, ...]): ... |         class T2(Tuple[T, ...]): ... | ||||||
|  |  | ||||||
|  | @ -17,6 +17,56 @@ class B[T, *Ts, **P]: | ||||||
|     z: P |     z: P | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | Eggs = int | ||||||
|  | Spam = str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class C[Eggs, **Spam]: | ||||||
|  |     x: Eggs | ||||||
|  |     y: Spam | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def generic_function[T, *Ts, **P]( | def generic_function[T, *Ts, **P]( | ||||||
|     x: T, *y: *Ts, z: P.args, zz: P.kwargs |     x: T, *y: *Ts, z: P.args, zz: P.kwargs | ||||||
| ) -> None: ... | ) -> 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) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  | @ -1061,15 +1061,24 @@ def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard | ||||||
|                 globalns = getattr( |                 globalns = getattr( | ||||||
|                     sys.modules.get(self.__forward_module__, None), '__dict__', globalns |                     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: |             if type_params: | ||||||
|                 # "Inject" type parameters into the local namespace |                 globalns, localns = dict(globalns), dict(localns) | ||||||
|                 # (unless they are shadowed by assignments *in* the local namespace), |                 for param in type_params: | ||||||
|                 # as a way of emulating annotation scopes when calling `eval()` |                     param_name = param.__name__ | ||||||
|                 locals_to_pass = {param.__name__: param for param in type_params} | localns |                     if not self.__forward_is_class__ or param_name not in globalns: | ||||||
|             else: |                         globalns[param_name] = param | ||||||
|                 locals_to_pass = localns |                         localns.pop(param_name, None) | ||||||
|  | 
 | ||||||
|             type_ = _type_check( |             type_ = _type_check( | ||||||
|                 eval(self.__forward_code__, globalns, locals_to_pass), |                 eval(self.__forward_code__, globalns, localns), | ||||||
|                 "Forward references must evaluate to types.", |                 "Forward references must evaluate to types.", | ||||||
|                 is_argument=self.__forward_is_argument__, |                 is_argument=self.__forward_is_argument__, | ||||||
|                 allow_special_forms=self.__forward_is_class__, |                 allow_special_forms=self.__forward_is_class__, | ||||||
|  |  | ||||||
|  | @ -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. | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Miss Islington (bot)
						Miss Islington (bot)