[3.14] gh-137969: Fix evaluation of ref.evaluate(format=Format.FORWARDREF) objects (GH-138075) (#140929)

gh-137969: Fix evaluation of `ref.evaluate(format=Format.FORWARDREF)` objects (GH-138075)
(cherry picked from commit 63e01d6bae)

Co-authored-by: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
Miss Islington (bot) 2025-11-03 02:45:44 +01:00 committed by GitHub
parent 23e3771045
commit cdb6fe89ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 19 additions and 6 deletions

View file

@ -159,12 +159,12 @@ def evaluate(
type_params = getattr(owner, "__type_params__", None) type_params = getattr(owner, "__type_params__", None)
# Type parameters exist in their own scope, which is logically # Type parameters exist in their own scope, which is logically
# between the locals and the globals. We simulate this by adding # between the locals and the globals.
# them to the globals. type_param_scope = {}
if type_params is not None: if type_params is not None:
globals = dict(globals)
for param in type_params: for param in type_params:
globals[param.__name__] = param type_param_scope[param.__name__] = param
if self.__extra_names__: if self.__extra_names__:
locals = {**locals, **self.__extra_names__} locals = {**locals, **self.__extra_names__}
@ -172,6 +172,8 @@ def evaluate(
if arg.isidentifier() and not keyword.iskeyword(arg): if arg.isidentifier() and not keyword.iskeyword(arg):
if arg in locals: if arg in locals:
return locals[arg] return locals[arg]
elif arg in type_param_scope:
return type_param_scope[arg]
elif arg in globals: elif arg in globals:
return globals[arg] return globals[arg]
elif hasattr(builtins, arg): elif hasattr(builtins, arg):
@ -183,7 +185,7 @@ def evaluate(
else: else:
code = self.__forward_code__ code = self.__forward_code__
try: try:
return eval(code, globals=globals, locals=locals) return eval(code, globals=globals, locals={**type_param_scope, **locals})
except Exception: except Exception:
if not is_forwardref_format: if not is_forwardref_format:
raise raise
@ -191,7 +193,7 @@ def evaluate(
# All variables, in scoping order, should be checked before # All variables, in scoping order, should be checked before
# triggering __missing__ to create a _Stringifier. # triggering __missing__ to create a _Stringifier.
new_locals = _StringifierDict( new_locals = _StringifierDict(
{**builtins.__dict__, **globals, **locals}, {**builtins.__dict__, **globals, **type_param_scope, **locals},
globals=globals, globals=globals,
owner=owner, owner=owner,
is_class=self.__forward_is_class__, is_class=self.__forward_is_class__,

View file

@ -1911,6 +1911,15 @@ def test_fwdref_invalid_syntax(self):
with self.assertRaises(SyntaxError): with self.assertRaises(SyntaxError):
fr.evaluate() fr.evaluate()
def test_re_evaluate_generics(self):
global alias
class C:
x: alias[int]
evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(format=Format.FORWARDREF)
alias = list
self.assertEqual(evaluated.evaluate(), list[int])
class TestAnnotationLib(unittest.TestCase): class TestAnnotationLib(unittest.TestCase):
def test__all__(self): def test__all__(self):

View file

@ -0,0 +1,2 @@
Fix :meth:`annotationlib.ForwardRef.evaluate` returning :class:`annotationlib.ForwardRef`
objects which do not update in new contexts.