Skip to content

gh-137226: Fix behavior of ForwardRef.evaluate with type_params #137227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2025
Merged
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
22 changes: 20 additions & 2 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2342,10 +2342,13 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
# *base_globals* first rather than *base_locals*.
# This only affects ForwardRefs.
base_globals, base_locals = base_locals, base_globals
type_params = base.__type_params__
base_globals, base_locals = _add_type_params_to_scope(
type_params, base_globals, base_locals, True)
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__,
value = _eval_type(value, base_globals, base_locals, (),
format=format, owner=obj)
if value is None:
value = type(None)
Expand Down Expand Up @@ -2381,6 +2384,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
elif localns is None:
localns = globalns
type_params = getattr(obj, "__type_params__", ())
globalns, localns = _add_type_params_to_scope(type_params, globalns, localns, False)
for name, value in hints.items():
if isinstance(value, str):
# class-level forward refs were handled above, this must be either
Expand All @@ -2390,13 +2394,27 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
is_argument=not isinstance(obj, types.ModuleType),
is_class=False,
)
value = _eval_type(value, globalns, localns, type_params, format=format, owner=obj)
value = _eval_type(value, globalns, localns, (), format=format, owner=obj)
if value is None:
value = type(None)
hints[name] = value
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}


# Add type parameters to the globals and locals scope. This is needed for
# compatibility.
def _add_type_params_to_scope(type_params, globalns, localns, is_class):
if not type_params:
return globalns, localns
globalns = dict(globalns)
localns = dict(localns)
for param in type_params:
if not is_class or param.__name__ not in globalns:
globalns[param.__name__] = param
localns.pop(param.__name__, None)
return globalns, localns


def _strip_annotations(t):
"""Strip the annotations from a given type."""
if isinstance(t, _AnnotatedAlias):
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