Skip to content

Commit 2d12bdf

Browse files
committed
gh-137226: Fix behavior of ForwardRef.evaluate with type_params
The previous behavior was copied from earlier typing code. It works around the way typing.get_type_hints passes its namespaces, but I don't think the behavior is logical or correct.
1 parent 5236b02 commit 2d12bdf

File tree

4 files changed

+47
-14
lines changed

4 files changed

+47
-14
lines changed

Lib/annotationlib.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,21 +158,13 @@ def evaluate(
158158
# as a way of emulating annotation scopes when calling `eval()`
159159
type_params = getattr(owner, "__type_params__", None)
160160

161-
# type parameters require some special handling,
162-
# as they exist in their own scope
163-
# but `eval()` does not have a dedicated parameter for that scope.
164-
# For classes, names in type parameter scopes should override
165-
# names in the global scope (which here are called `localns`!),
166-
# but should in turn be overridden by names in the class scope
167-
# (which here are called `globalns`!)
161+
# Type parameters exist in their own scope, which is logically
162+
# between the locals and the globals. We simulate this by adding
163+
# them to the globals.
168164
if type_params is not None:
169165
globals = dict(globals)
170-
locals = dict(locals)
171166
for param in type_params:
172-
param_name = param.__name__
173-
if not self.__forward_is_class__ or param_name not in globals:
174-
globals[param_name] = param
175-
locals.pop(param_name, None)
167+
globals[param.__name__] = param
176168
if self.__extra_names__:
177169
locals = {**locals, **self.__extra_names__}
178170

Lib/test/test_annotationlib.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,11 @@ def test_annotations_to_string(self):
13651365
class A:
13661366
pass
13671367

1368+
TypeParamsAlias1 = int
1369+
1370+
class TypeParamsSample[TypeParamsAlias1, TypeParamsAlias2]:
1371+
TypeParamsAlias2 = str
1372+
13681373

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

1605+
def test_evaluate_with_type_params_and_scope_conflict(self):
1606+
for is_class in (False, True):
1607+
with self.subTest(is_class=is_class):
1608+
fwdref1 = ForwardRef("TypeParamsAlias1", owner=TypeParamsSample, is_class=is_class)
1609+
fwdref2 = ForwardRef("TypeParamsAlias2", owner=TypeParamsSample, is_class=is_class)
1610+
1611+
self.assertIs(
1612+
fwdref1.evaluate(),
1613+
TypeParamsSample.__type_params__[0],
1614+
)
1615+
self.assertIs(
1616+
fwdref2.evaluate(),
1617+
TypeParamsSample.TypeParamsAlias2,
1618+
)
1619+
16001620
def test_fwdref_with_module(self):
16011621
self.assertIs(ForwardRef("Format", module="annotationlib").evaluate(), Format)
16021622
self.assertIs(

Lib/typing.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2342,10 +2342,13 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
23422342
# *base_globals* first rather than *base_locals*.
23432343
# This only affects ForwardRefs.
23442344
base_globals, base_locals = base_locals, base_globals
2345+
type_params = base.__type_params__
2346+
base_globals, base_locals = _add_type_params_to_scope(
2347+
type_params, base_globals, base_locals, True)
23452348
for name, value in ann.items():
23462349
if isinstance(value, str):
23472350
value = _make_forward_ref(value, is_argument=False, is_class=True)
2348-
value = _eval_type(value, base_globals, base_locals, base.__type_params__,
2351+
value = _eval_type(value, base_globals, base_locals, (),
23492352
format=format, owner=obj)
23502353
if value is None:
23512354
value = type(None)
@@ -2381,6 +2384,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
23812384
elif localns is None:
23822385
localns = globalns
23832386
type_params = getattr(obj, "__type_params__", ())
2387+
globalns, localns = _add_type_params_to_scope(type_params, globalns, localns, False)
23842388
for name, value in hints.items():
23852389
if isinstance(value, str):
23862390
# class-level forward refs were handled above, this must be either
@@ -2390,13 +2394,27 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
23902394
is_argument=not isinstance(obj, types.ModuleType),
23912395
is_class=False,
23922396
)
2393-
value = _eval_type(value, globalns, localns, type_params, format=format, owner=obj)
2397+
value = _eval_type(value, globalns, localns, (), format=format, owner=obj)
23942398
if value is None:
23952399
value = type(None)
23962400
hints[name] = value
23972401
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
23982402

23992403

2404+
# Add type parameters to the globals and locals scope. This is needed for
2405+
# compatibility.
2406+
def _add_type_params_to_scope(type_params, globalns, localns, is_class):
2407+
if not type_params:
2408+
return globalns, localns
2409+
globalns = dict(globalns)
2410+
localns = dict(localns)
2411+
for param in type_params:
2412+
if not is_class or param.__name__ not in globalns:
2413+
globalns[param.__name__] = param
2414+
localns.pop(param.__name__, None)
2415+
return globalns, localns
2416+
2417+
24002418
def _strip_annotations(t):
24012419
"""Strip the annotations from a given type."""
24022420
if isinstance(t, _AnnotatedAlias):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix behavior of :meth:`annotationlib.ForwardRef.evaluate` when the
2+
*type_params* parameter is passed and the name of a type param is also
3+
present in an enclosing scope.

0 commit comments

Comments
 (0)