mirror of https://github.com/python/cpython
gh-114053: Fix another edge case involving `get_type_hints`, PEP 695 and PEP 563 (#120272)
This commit is contained in:
parent
8f5a01707f
commit
2d3187bf20
|
@ -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, ...]): ...
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -1060,15 +1060,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__,
|
||||
|
|
|
@ -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…
Reference in New Issue