Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5364,6 +5364,43 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
set(results.generic_func.__type_params__)
)

def test_pep695_generic_class_with_future_typed_dicts(self):
# gh-138949
td1_hints = get_type_hints(ann_module695.TD1)
self.assertEqual(td1_hints, {'a': ann_module695.TD1.__type_params__[0]})

td2_hints = get_type_hints(ann_module695.TD2) # used to fail with `NameError`
self.assertEqual(
td2_hints,
{'a': ann_module695.TD1.__type_params__[0], 'b': int},
)

td3_hints = get_type_hints(ann_module695.TD3)
self.assertEqual(
td3_hints,
{
'a': ann_module695.TD1.__type_params__[0],
'b': int,
'c': ann_module695.TD3.__type_params__[0],
},
)

td4_hints = get_type_hints(ann_module695.TD4)
self.assertEqual(
td4_hints,
{
# Type param `TD4.T` must have a higher precedence over `TD1.T`:
'a': ann_module695.TD4.__type_params__[0],
'b': int,
'c': ann_module695.TD3.__type_params__[0],
'd': ann_module695.TD4.__type_params__[0],
'e': ann_module695.TD4.__type_params__[1],
},
)

with self.assertRaisesRegex(NameError, "name 'T' is not defined"):
get_type_hints(ann_module695.TD1_2)

def test_extended_generic_rules_subclassing(self):
class T1(Tuple[T, KT]): ...
class T2(Tuple[T, ...]): ...
Expand Down
21 changes: 20 additions & 1 deletion Lib/test/typinganndata/ann_module695.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations
from typing import Callable
from typing import Callable, TypedDict


class A[T, *Ts, **P]:
Expand Down Expand Up @@ -70,3 +70,22 @@ def generic_function[Eggs, **Spam](x: Eggs, y: Spam): pass
generic_func=generic_function,
hints_for_generic_func=get_type_hints(generic_function)
)

# gh-138949
class TD1[T](TypedDict):
a: T

class TD2(TD1):
b: int

class TD3[CT](TD2):
c: CT

class TD4[T, E](TD3):
d: T
e: E

class TD1_2(TD1):
# This must raise a `NameError`, because `T` is only defined for a parent
# keys scope.
b: T
30 changes: 27 additions & 3 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,9 +449,33 @@ def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset()
# prefer_fwd_module flag), so that the default behavior remains more straightforward.
if prefer_fwd_module and t.__forward_module__ is not None:
globalns = None
# If there are type params on the owner, we need to add them back, because
# annotationlib won't.
if owner_type_params := getattr(owner, "__type_params__", None):
# # If there are type params on the owner, we need to add them back, because
# # annotationlib won't.
owner_type_params = getattr(owner, "__type_params__", ())
# TypedDict classes copy the parent type annotations, but do not
# copy parent type params / mro. So, we need to collect them manually here.
if is_typeddict(owner):
owner_type_params = list(owner_type_params)
mro_stack = list(owner.__orig_bases__)
seen = {tp.__name__ for tp in owner_type_params}
while mro_stack:
typ = mro_stack.pop(0)
if is_typeddict(typ):
mro_stack.extend(typ.__orig_bases__)
if t not in typ.__annotations__.values():
# We only copy __type_params__ for types that own
# this annotation. So, it won't be possible to use
# undeclared type parameters from parent types in children.
continue

base_type_params = getattr(typ, "__type_params__", ())
for btp in base_type_params:
if btp.__name__ in seen:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems wrong, base classes can have another type param with the same name

continue
owner_type_params.append(btp)
seen.add(btp.__name__)
owner_type_params = tuple(owner_type_params)
if owner_type_params:
globalns = getattr(
sys.modules.get(t.__forward_module__, None), "__dict__", None
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :exc:`NameError` in :func:`typing.get_type_hints` on non-generic
children of generic typed dicts with future annotations flag enabled.
Loading