Skip to content
Draft
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
16 changes: 4 additions & 12 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,21 +158,13 @@ def evaluate(
# as a way of emulating annotation scopes when calling `eval()`
type_params = getattr(owner, "__type_params__", None)

# 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`!)
# Type parameters exist in their own scope, which is logically
# between the locals and the globals. We simulate this by adding
# them to the globals.
if type_params is not None:
globals = dict(globals)
locals = dict(locals)
for param in type_params:
param_name = param.__name__
if not self.__forward_is_class__ or param_name not in globals:
globals[param_name] = param
locals.pop(param_name, None)
globals[param.__name__] = param
if self.__extra_names__:
locals = {**locals, **self.__extra_names__}

Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,11 @@ def test_annotations_to_string(self):
class A:
pass

TypeParamsAlias1 = int

class TypeParamsSample[TypeParamsAlias1, TypeParamsAlias2]:
TypeParamsAlias2 = str


class TestForwardRefClass(unittest.TestCase):
def test_forwardref_instance_type_error(self):
Expand Down Expand Up @@ -1597,6 +1602,21 @@ class Gen[T]:
ForwardRef("alias").evaluate(owner=Gen, locals={"alias": str}), str
)

def test_evaluate_with_type_params_and_scope_conflict(self):
for is_class in (False, True):
with self.subTest(is_class=is_class):
fwdref1 = ForwardRef("TypeParamsAlias1", owner=TypeParamsSample, is_class=is_class)
fwdref2 = ForwardRef("TypeParamsAlias2", owner=TypeParamsSample, is_class=is_class)

self.assertIs(
fwdref1.evaluate(),
TypeParamsSample.__type_params__[0],
)
self.assertIs(
fwdref2.evaluate(),
TypeParamsSample.TypeParamsAlias2,
)

def test_fwdref_with_module(self):
self.assertIs(ForwardRef("Format", module="annotationlib").evaluate(), Format)
self.assertIs(
Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6633,7 +6633,7 @@ def test_get_type_hints_classes(self):
self.assertEqual(gth(mod_generics_cache.B),
{'my_inner_a1': mod_generics_cache.B.A,
'my_inner_a2': mod_generics_cache.B.A,
'my_outer_a': mod_generics_cache.A})
'my_outer_a': mod_generics_cache.B.A})

def test_get_type_hints_classes_no_implicit_optional(self):
class WithNoneDefault:
Expand Down Expand Up @@ -8762,15 +8762,15 @@ def test_get_type_hints(self):
def test_get_type_hints_generic(self):
self.assertEqual(
get_type_hints(BarGeneric),
{'a': typing.Optional[T], 'b': int}
{'a': typing.Optional[_typed_dict_helper.T], 'b': int}
)

class FooBarGeneric(BarGeneric[int]):
c: str

self.assertEqual(
get_type_hints(FooBarGeneric),
{'a': typing.Optional[T], 'b': int, 'c': str}
{'a': typing.Optional[_typed_dict_helper.T], 'b': int, 'c': str}
)

def test_pep695_generic_typeddict(self):
Expand Down
45 changes: 14 additions & 31 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2329,24 +2329,12 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
if format == Format.STRING:
hints.update(ann)
continue
if globalns is None:
base_globals = getattr(sys.modules.get(base.__module__, None), '__dict__', {})
else:
base_globals = globalns
base_locals = dict(vars(base)) if localns is None else localns
if localns is None and globalns is None:
# This is surprising, but required. Before Python 3.10,
# get_type_hints only evaluated the globalns of
# a class. To maintain backwards compatibility, we reverse
# the globalns and localns order so that eval() looks into
# *base_globals* first rather than *base_locals*.
# This only affects ForwardRefs.
base_globals, base_locals = base_locals, base_globals
for name, value in ann.items():
if isinstance(value, str):
value = _make_forward_ref(value, is_argument=False, is_class=True)
value = _eval_type(value, base_globals, base_locals, base.__type_params__,
format=format, owner=obj)
value = _make_forward_ref(value, is_argument=False, is_class=True,
owner=base)
value = _eval_type(value, globalns, localns, base.__type_params__,
format=format, owner=base)
if value is None:
value = type(None)
hints[name] = value
Expand All @@ -2367,20 +2355,14 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
if format == Format.STRING:
return hints

if globalns is None:
if isinstance(obj, types.ModuleType):
globalns = obj.__dict__
else:
nsobj = obj
# Find globalns for the unwrapped object.
while hasattr(nsobj, '__wrapped__'):
nsobj = nsobj.__wrapped__
globalns = getattr(nsobj, '__globals__', {})
if localns is None:
localns = globalns
elif localns is None:
localns = globalns
type_params = getattr(obj, "__type_params__", ())
if isinstance(obj, types.ModuleType):
nsobj = obj
else:
nsobj = obj
# Find globalns for the unwrapped object.
while hasattr(nsobj, '__wrapped__'):
nsobj = nsobj.__wrapped__
type_params = getattr(nsobj, "__type_params__", ())
for name, value in hints.items():
if isinstance(value, str):
# class-level forward refs were handled above, this must be either
Expand All @@ -2389,8 +2371,9 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
value,
is_argument=not isinstance(obj, types.ModuleType),
is_class=False,
owner=nsobj,
)
value = _eval_type(value, globalns, localns, type_params, format=format, owner=obj)
value = _eval_type(value, globalns, localns, type_params, format=format, owner=nsobj)
if value is None:
value = type(None)
hints[name] = value
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix behavior of :meth:`annotationlib.ForwardRef.evaluate` when the
*type_params* parameter is passed and the name of a type param is also
present in an enclosing scope.
Loading