Skip to content

Commit 15ea954

Browse files
committed
gh-125618: Make FORWARDREF format succeed more often
Fixes #125618.
1 parent a4ea80d commit 15ea954

File tree

4 files changed

+201
-55
lines changed

4 files changed

+201
-55
lines changed

Doc/library/annotationlib.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Classes
132132

133133
Values are real annotation values (as per :attr:`Format.VALUE` format)
134134
for defined values, and :class:`ForwardRef` proxies for undefined
135-
values. Real objects may contain references to, :class:`ForwardRef`
135+
values. Real objects may contain references to :class:`ForwardRef`
136136
proxy objects.
137137

138138
.. attribute:: STRING
@@ -172,14 +172,22 @@ Classes
172172
:class:`~ForwardRef`. The string may not be exactly equivalent
173173
to the original source.
174174

175-
.. method:: evaluate(*, owner=None, globals=None, locals=None, type_params=None)
175+
.. method:: evaluate(*, owner=None, globals=None, locals=None, type_params=None,
176+
format=Format.VALUE)
176177

177178
Evaluate the forward reference, returning its value.
178179

179-
This may throw an exception, such as :exc:`NameError`, if the forward
180+
If the *format* argument is :attr:`~Format.VALUE` (the default),
181+
this method may throw an exception, such as :exc:`NameError`, if the forward
180182
reference refers to a name that cannot be resolved. The arguments to this
181183
method can be used to provide bindings for names that would otherwise
182-
be undefined.
184+
be undefined. If the *format* argument is :attr:`~Format.FORWARDREF`,
185+
the method will never throw an exception, but may return a :class:`~ForwardRef`
186+
instance. For example, if the forward reference object contains the code
187+
``list[undefined]``, where ``undefined`` is a name that is not defined,
188+
evaluating it with the :attr:`~Format.FORWARDREF` format will return
189+
``list[ForwardRef('undefined')]``. If the *format* argument is
190+
:attr:`~Format.STRING`, the method will return :attr:`~ForwardRef.__forward_arg__`.
183191

184192
The *owner* parameter provides the preferred mechanism for passing scope
185193
information to this method. The owner of a :class:`~ForwardRef` is the

Lib/annotationlib.py

Lines changed: 115 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,21 @@ def __init__(
9090
def __init_subclass__(cls, /, *args, **kwds):
9191
raise TypeError("Cannot subclass ForwardRef")
9292

93-
def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
93+
def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None,
94+
format=Format.VALUE):
9495
"""Evaluate the forward reference and return the value.
9596
9697
If the forward reference cannot be evaluated, raise an exception.
9798
"""
99+
match format:
100+
case Format.STRING:
101+
return self.__forward_arg__
102+
case Format.VALUE:
103+
is_forwardref_format = False
104+
case Format.FORWARDREF:
105+
is_forwardref_format = True
106+
case _:
107+
raise NotImplementedError(format)
98108
if self.__cell__ is not None:
99109
try:
100110
return self.__cell__.cell_contents
@@ -155,17 +165,33 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
155165
arg = self.__forward_arg__
156166
if arg.isidentifier() and not keyword.iskeyword(arg):
157167
if arg in locals:
158-
value = locals[arg]
168+
return locals[arg]
159169
elif arg in globals:
160-
value = globals[arg]
170+
return globals[arg]
161171
elif hasattr(builtins, arg):
162172
return getattr(builtins, arg)
173+
elif is_forwardref_format:
174+
return self
163175
else:
164176
raise NameError(arg)
165177
else:
166178
code = self.__forward_code__
167-
value = eval(code, globals=globals, locals=locals)
168-
return value
179+
try:
180+
return eval(code, globals=globals, locals=locals)
181+
except Exception:
182+
if not is_forwardref_format:
183+
raise
184+
new_locals = _StringifierDict(
185+
{**builtins.__dict__, **locals}, globals=globals, owner=owner,
186+
is_class=self.__forward_is_class__
187+
)
188+
try:
189+
result = eval(code, globals=globals, locals=new_locals)
190+
except Exception:
191+
return self
192+
else:
193+
new_locals.transmogrify()
194+
return result
169195

170196
def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard):
171197
import typing
@@ -478,6 +504,14 @@ def __missing__(self, key):
478504
self.stringifiers.append(fwdref)
479505
return fwdref
480506

507+
def transmogrify(self):
508+
for obj in self.stringifiers:
509+
obj.__class__ = ForwardRef
510+
obj.__stringifier_dict__ = None # not needed for ForwardRef
511+
if isinstance(obj.__ast_node__, str):
512+
obj.__arg__ = obj.__ast_node__
513+
obj.__ast_node__ = None
514+
481515

482516
def call_evaluate_function(evaluate, format, *, owner=None):
483517
"""Call an evaluate function. Evaluate functions are normally generated for
@@ -522,19 +556,10 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
522556
# convert each of those into a string to get an approximation of the
523557
# original source.
524558
globals = _StringifierDict({})
525-
if annotate.__closure__:
526-
freevars = annotate.__code__.co_freevars
527-
new_closure = []
528-
for i, cell in enumerate(annotate.__closure__):
529-
if i < len(freevars):
530-
name = freevars[i]
531-
else:
532-
name = "__cell__"
533-
fwdref = _Stringifier(name, stringifier_dict=globals)
534-
new_closure.append(types.CellType(fwdref))
535-
closure = tuple(new_closure)
536-
else:
537-
closure = None
559+
is_class = isinstance(owner, type)
560+
closure = _build_closure(
561+
annotate, owner, is_class, globals, allow_evaluation=False
562+
)
538563
func = types.FunctionType(
539564
annotate.__code__,
540565
globals,
@@ -570,32 +595,30 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
570595
namespace = {**annotate.__builtins__, **annotate.__globals__}
571596
is_class = isinstance(owner, type)
572597
globals = _StringifierDict(namespace, annotate.__globals__, owner, is_class)
573-
if annotate.__closure__:
574-
freevars = annotate.__code__.co_freevars
575-
new_closure = []
576-
for i, cell in enumerate(annotate.__closure__):
577-
try:
578-
cell.cell_contents
579-
except ValueError:
580-
if i < len(freevars):
581-
name = freevars[i]
582-
else:
583-
name = "__cell__"
584-
fwdref = _Stringifier(
585-
name,
586-
cell=cell,
587-
owner=owner,
588-
globals=annotate.__globals__,
589-
is_class=is_class,
590-
stringifier_dict=globals,
591-
)
592-
globals.stringifiers.append(fwdref)
593-
new_closure.append(types.CellType(fwdref))
594-
else:
595-
new_closure.append(cell)
596-
closure = tuple(new_closure)
598+
closure = _build_closure(
599+
annotate, owner, is_class, globals, allow_evaluation=True
600+
)
601+
func = types.FunctionType(
602+
annotate.__code__,
603+
globals,
604+
closure=closure,
605+
argdefs=annotate.__defaults__,
606+
kwdefaults=annotate.__kwdefaults__,
607+
)
608+
try:
609+
result = func(Format.VALUE_WITH_FAKE_GLOBALS)
610+
except Exception:
611+
pass
597612
else:
598-
closure = None
613+
globals.transmogrify()
614+
return result
615+
616+
# Try again, but do not provide any globals. This allows us to return
617+
# a value in certain cases where an exception gets raised during evaluation.
618+
globals = _StringifierDict({}, annotate.__globals__, owner, is_class)
619+
closure = _build_closure(
620+
annotate, owner, is_class, globals, allow_evaluation=False
621+
)
599622
func = types.FunctionType(
600623
annotate.__code__,
601624
globals,
@@ -604,13 +627,21 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
604627
kwdefaults=annotate.__kwdefaults__,
605628
)
606629
result = func(Format.VALUE_WITH_FAKE_GLOBALS)
607-
for obj in globals.stringifiers:
608-
obj.__class__ = ForwardRef
609-
obj.__stringifier_dict__ = None # not needed for ForwardRef
610-
if isinstance(obj.__ast_node__, str):
611-
obj.__arg__ = obj.__ast_node__
612-
obj.__ast_node__ = None
613-
return result
630+
globals.transmogrify()
631+
if _is_evaluate:
632+
if isinstance(result, ForwardRef):
633+
return result.evaluate(format=Format.FORWARDREF)
634+
else:
635+
return result
636+
else:
637+
return {
638+
key: (
639+
val.evaluate(format=Format.FORWARDREF)
640+
if isinstance(val, ForwardRef)
641+
else val
642+
)
643+
for key, val in result.items()
644+
}
614645
elif format == Format.VALUE:
615646
# Should be impossible because __annotate__ functions must not raise
616647
# NotImplementedError for this format.
@@ -619,6 +650,40 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
619650
raise ValueError(f"Invalid format: {format!r}")
620651

621652

653+
def _build_closure(annotate, owner, is_class, stringifier_dict, *,
654+
allow_evaluation):
655+
if not annotate.__closure__:
656+
return None
657+
freevars = annotate.__code__.co_freevars
658+
new_closure = []
659+
for i, cell in enumerate(annotate.__closure__):
660+
if i < len(freevars):
661+
name = freevars[i]
662+
else:
663+
name = "__cell__"
664+
new_cell = None
665+
if allow_evaluation:
666+
try:
667+
cell.cell_contents
668+
except ValueError:
669+
pass
670+
else:
671+
new_cell = cell
672+
if new_cell is None:
673+
fwdref = _Stringifier(
674+
name,
675+
cell=cell,
676+
owner=owner,
677+
globals=annotate.__globals__,
678+
is_class=is_class,
679+
stringifier_dict=globals,
680+
)
681+
stringifier_dict.stringifiers.append(fwdref)
682+
new_cell = types.CellType(fwdref)
683+
new_closure.append(new_cell)
684+
return tuple(new_closure)
685+
686+
622687
def get_annotate_function(obj):
623688
"""Get the __annotate__ function for an object.
624689

Lib/test/test_annotationlib.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def test_special_attrs(self):
307307
# Forward refs provide a different introspection API. __name__ and
308308
# __qualname__ make little sense for forward refs as they can store
309309
# complex typing expressions.
310-
fr = annotationlib.ForwardRef("set[Any]")
310+
fr = ForwardRef("set[Any]")
311311
self.assertFalse(hasattr(fr, "__name__"))
312312
self.assertFalse(hasattr(fr, "__qualname__"))
313313
self.assertEqual(fr.__module__, "annotationlib")
@@ -317,6 +317,38 @@ def test_special_attrs(self):
317317
with self.assertRaises(TypeError):
318318
pickle.dumps(fr, proto)
319319

320+
def test_evaluate_string_format(self):
321+
fr = ForwardRef("set[Any]")
322+
self.assertEqual(fr.evaluate(format=Format.STRING), "set[Any]")
323+
324+
def test_evaluate_forwardref_format(self):
325+
fr = ForwardRef("undef")
326+
evaluated = fr.evaluate(format=Format.FORWARDREF)
327+
self.assertIs(fr, evaluated)
328+
329+
fr = ForwardRef("set[undefined]")
330+
evaluated = fr.evaluate(format=Format.FORWARDREF)
331+
self.assertEqual(
332+
evaluated,
333+
set[support.EqualToForwardRef("undefined")],
334+
)
335+
336+
fr = ForwardRef("a + b")
337+
self.assertEqual(
338+
fr.evaluate(format=Format.FORWARDREF),
339+
support.EqualToForwardRef("a + b"),
340+
)
341+
self.assertEqual(
342+
fr.evaluate(format=Format.FORWARDREF, locals={"a": 1, "b": 2}),
343+
3,
344+
)
345+
346+
fr = ForwardRef('"a" + 1')
347+
self.assertEqual(
348+
fr.evaluate(format=Format.FORWARDREF),
349+
support.EqualToForwardRef('"a" + 1'),
350+
)
351+
320352
def test_evaluate_with_type_params(self):
321353
class Gen[T]:
322354
alias = int
@@ -1054,6 +1086,44 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
10541086
set(results.generic_func.__type_params__),
10551087
)
10561088

1089+
maxDiff = None
1090+
1091+
def test_partial_evaluation(self):
1092+
def f(
1093+
x: builtins.undef,
1094+
y: list[int],
1095+
z: 1 + int,
1096+
a: builtins.int,
1097+
b: [builtins.undef, builtins.int],
1098+
):
1099+
pass
1100+
1101+
self.assertEqual(
1102+
annotationlib.get_annotations(f, format=Format.FORWARDREF),
1103+
{
1104+
"x": support.EqualToForwardRef("builtins.undef", owner=f),
1105+
"y": list[int],
1106+
"z": support.EqualToForwardRef("1 + int", owner=f),
1107+
"a": int,
1108+
"b": [
1109+
support.EqualToForwardRef("builtins.undef", owner=f),
1110+
# We can't resolve this because we have to evaluate the whole annotation
1111+
support.EqualToForwardRef("builtins.int", owner=f),
1112+
],
1113+
},
1114+
)
1115+
1116+
self.assertEqual(
1117+
annotationlib.get_annotations(f, format=Format.STRING),
1118+
{
1119+
"x": "builtins.undef",
1120+
"y": "list[int]",
1121+
"z": "1 + int",
1122+
"a": "builtins.int",
1123+
"b": "[builtins.undef, builtins.int]",
1124+
},
1125+
)
1126+
10571127

10581128
class TestCallEvaluateFunction(unittest.TestCase):
10591129
def test_evaluation(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add a *format* parameter to :meth:`annotationlib.ForwardRef.evaluate`.
2+
Evaluating annotations in the ``FORWARDREF`` format now succeeds in more
3+
cases that would previously have raised an exception.

0 commit comments

Comments
 (0)