Skip to content

Commit fe2fc30

Browse files
committed
gh-138949: Fix non-generic children of generic TypedDicts with future annotations
1 parent 633b6be commit fe2fc30

4 files changed

Lines changed: 86 additions & 4 deletions

File tree

Lib/test/test_typing.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5364,6 +5364,43 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
53645364
set(results.generic_func.__type_params__)
53655365
)
53665366

5367+
def test_pep695_generic_class_with_future_typed_dicts(self):
5368+
# gh-138949
5369+
td1_hints = get_type_hints(ann_module695.TD1)
5370+
self.assertEqual(td1_hints, {'a': ann_module695.TD1.__type_params__[0]})
5371+
5372+
td2_hints = get_type_hints(ann_module695.TD2) # used to fail with `NameError`
5373+
self.assertEqual(
5374+
td2_hints,
5375+
{'a': ann_module695.TD1.__type_params__[0], 'b': int},
5376+
)
5377+
5378+
td3_hints = get_type_hints(ann_module695.TD3)
5379+
self.assertEqual(
5380+
td3_hints,
5381+
{
5382+
'a': ann_module695.TD1.__type_params__[0],
5383+
'b': int,
5384+
'c': ann_module695.TD3.__type_params__[0],
5385+
},
5386+
)
5387+
5388+
td4_hints = get_type_hints(ann_module695.TD4)
5389+
self.assertEqual(
5390+
td4_hints,
5391+
{
5392+
# Type param `TD4.T` must have a higher precedence over `TD1.T`:
5393+
'a': ann_module695.TD4.__type_params__[0],
5394+
'b': int,
5395+
'c': ann_module695.TD3.__type_params__[0],
5396+
'd': ann_module695.TD4.__type_params__[0],
5397+
'e': ann_module695.TD4.__type_params__[1],
5398+
},
5399+
)
5400+
5401+
with self.assertRaisesRegex(NameError, "name 'T' is not defined"):
5402+
get_type_hints(ann_module695.TD1_2)
5403+
53675404
def test_extended_generic_rules_subclassing(self):
53685405
class T1(Tuple[T, KT]): ...
53695406
class T2(Tuple[T, ...]): ...

Lib/test/typinganndata/ann_module695.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
2-
from typing import Callable
2+
from typing import Callable, TypedDict
33

44

55
class A[T, *Ts, **P]:
@@ -70,3 +70,22 @@ def generic_function[Eggs, **Spam](x: Eggs, y: Spam): pass
7070
generic_func=generic_function,
7171
hints_for_generic_func=get_type_hints(generic_function)
7272
)
73+
74+
# gh-138949
75+
class TD1[T](TypedDict):
76+
a: T
77+
78+
class TD2(TD1):
79+
b: int
80+
81+
class TD3[CT](TD2):
82+
c: CT
83+
84+
class TD4[T, E](TD3):
85+
d: T
86+
e: E
87+
88+
class TD1_2(TD1):
89+
# This must raise a `NameError`, because `T` is only defined for a parent
90+
# keys scope.
91+
b: T

Lib/typing.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -449,9 +449,33 @@ def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset()
449449
# prefer_fwd_module flag), so that the default behavior remains more straightforward.
450450
if prefer_fwd_module and t.__forward_module__ is not None:
451451
globalns = None
452-
# If there are type params on the owner, we need to add them back, because
453-
# annotationlib won't.
454-
if owner_type_params := getattr(owner, "__type_params__", None):
452+
# # If there are type params on the owner, we need to add them back, because
453+
# # annotationlib won't.
454+
owner_type_params = getattr(owner, "__type_params__", ())
455+
# TypedDict classes copy the parent type annotations, but do not
456+
# copy parent type params / mro. So, we need to collect them manually here.
457+
if is_typeddict(owner):
458+
owner_type_params = list(owner_type_params)
459+
mro_stack = list(owner.__orig_bases__)
460+
seen = {tp.__name__ for tp in owner_type_params}
461+
while mro_stack:
462+
typ = mro_stack.pop(0)
463+
if is_typeddict(typ):
464+
mro_stack.extend(typ.__orig_bases__)
465+
if t not in typ.__annotations__.values():
466+
# We only copy __type_params__ for types that own
467+
# this annotation. So, it won't be possible to use
468+
# undeclared type parameters from parent types in children.
469+
continue
470+
471+
base_type_params = getattr(typ, "__type_params__", ())
472+
for btp in base_type_params:
473+
if btp.__name__ in seen:
474+
continue
475+
owner_type_params.append(btp)
476+
seen.add(btp.__name__)
477+
owner_type_params = tuple(owner_type_params)
478+
if owner_type_params:
455479
globalns = getattr(
456480
sys.modules.get(t.__forward_module__, None), "__dict__", None
457481
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :exc:`NameError` in :func:`typing.get_type_hints` on non-generic
2+
children of generic typed dicts with future annotations flag enabled.

0 commit comments

Comments
 (0)