diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index c83a1573ccd3d1..bee019cd51591e 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -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__} diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index ae0e73f08c5bd0..88e0d611647f28 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -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): @@ -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( diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index b1615bbff383c2..234d33c5fc9678 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -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: @@ -8762,7 +8762,7 @@ 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]): @@ -8770,7 +8770,7 @@ class FooBarGeneric(BarGeneric[int]): 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): diff --git a/Lib/typing.py b/Lib/typing.py index f1455c273d31ca..2b0bbc58566df2 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -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 @@ -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 @@ -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 diff --git a/Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst b/Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst new file mode 100644 index 00000000000000..522943cdd376dc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst @@ -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.