Skip to content

gh-125618: Make FORWARDREF format succeed more often #132818

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 11 commits into from
May 4, 2025
15 changes: 11 additions & 4 deletions Doc/library/annotationlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Classes

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

.. attribute:: STRING
Expand Down Expand Up @@ -172,14 +172,21 @@ Classes
:class:`~ForwardRef`. The string may not be exactly equivalent
to the original source.

.. method:: evaluate(*, owner=None, globals=None, locals=None, type_params=None)
.. method:: evaluate(*, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE)

Evaluate the forward reference, returning its value.

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

The *owner* parameter provides the preferred mechanism for passing scope
information to this method. The owner of a :class:`~ForwardRef` is the
Expand Down
182 changes: 131 additions & 51 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,28 @@ def __init__(
def __init_subclass__(cls, /, *args, **kwds):
raise TypeError("Cannot subclass ForwardRef")

def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
def evaluate(
self,
*,
globals=None,
locals=None,
type_params=None,
owner=None,
format=Format.VALUE,
):
"""Evaluate the forward reference and return the value.

If the forward reference cannot be evaluated, raise an exception.
"""
match format:
case Format.STRING:
return self.__forward_arg__
case Format.VALUE:
is_forwardref_format = False
case Format.FORWARDREF:
is_forwardref_format = True
case _:
raise NotImplementedError(format)
if self.__cell__ is not None:
try:
return self.__cell__.cell_contents
Expand Down Expand Up @@ -159,17 +176,36 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
arg = self.__forward_arg__
if arg.isidentifier() and not keyword.iskeyword(arg):
if arg in locals:
value = locals[arg]
return locals[arg]
elif arg in globals:
value = globals[arg]
return globals[arg]
elif hasattr(builtins, arg):
return getattr(builtins, arg)
elif is_forwardref_format:
return self
else:
raise NameError(arg)
else:
code = self.__forward_code__
value = eval(code, globals=globals, locals=locals)
return value
try:
return eval(code, globals=globals, locals=locals)
except Exception:
if not is_forwardref_format:
raise
new_locals = _StringifierDict(
{**builtins.__dict__, **locals},
globals=globals,
owner=owner,
is_class=self.__forward_is_class__,
format=format,
)
try:
result = eval(code, globals=globals, locals=new_locals)
except Exception:
return self
else:
new_locals.transmogrify()
return result

def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard):
import typing
Expand Down Expand Up @@ -546,6 +582,14 @@ def __missing__(self, key):
self.stringifiers.append(fwdref)
return fwdref

def transmogrify(self):
for obj in self.stringifiers:
obj.__class__ = ForwardRef
obj.__stringifier_dict__ = None # not needed for ForwardRef
if isinstance(obj.__ast_node__, str):
obj.__arg__ = obj.__ast_node__
obj.__ast_node__ = None

def create_unique_name(self):
name = f"__annotationlib_name_{self.next_id}__"
self.next_id += 1
Expand Down Expand Up @@ -595,19 +639,10 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
# convert each of those into a string to get an approximation of the
# original source.
globals = _StringifierDict({}, format=format)
if annotate.__closure__:
freevars = annotate.__code__.co_freevars
new_closure = []
for i, cell in enumerate(annotate.__closure__):
if i < len(freevars):
name = freevars[i]
else:
name = "__cell__"
fwdref = _Stringifier(name, stringifier_dict=globals)
new_closure.append(types.CellType(fwdref))
closure = tuple(new_closure)
else:
closure = None
is_class = isinstance(owner, type)
closure = _build_closure(
annotate, owner, is_class, globals, allow_evaluation=False
)
func = types.FunctionType(
annotate.__code__,
globals,
Expand Down Expand Up @@ -649,32 +684,36 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
is_class=is_class,
format=format,
)
if annotate.__closure__:
freevars = annotate.__code__.co_freevars
new_closure = []
for i, cell in enumerate(annotate.__closure__):
try:
cell.cell_contents
except ValueError:
if i < len(freevars):
name = freevars[i]
else:
name = "__cell__"
fwdref = _Stringifier(
name,
cell=cell,
owner=owner,
globals=annotate.__globals__,
is_class=is_class,
stringifier_dict=globals,
)
globals.stringifiers.append(fwdref)
new_closure.append(types.CellType(fwdref))
else:
new_closure.append(cell)
closure = tuple(new_closure)
closure = _build_closure(
annotate, owner, is_class, globals, allow_evaluation=True
)
func = types.FunctionType(
annotate.__code__,
globals,
closure=closure,
argdefs=annotate.__defaults__,
kwdefaults=annotate.__kwdefaults__,
)
try:
result = func(Format.VALUE_WITH_FAKE_GLOBALS)
except Exception:
pass
else:
closure = None
globals.transmogrify()
return result

# Try again, but do not provide any globals. This allows us to return
# a value in certain cases where an exception gets raised during evaluation.
globals = _StringifierDict(
{},
globals=annotate.__globals__,
owner=owner,
is_class=is_class,
format=format,
)
closure = _build_closure(
annotate, owner, is_class, globals, allow_evaluation=False
)
func = types.FunctionType(
annotate.__code__,
globals,
Expand All @@ -683,13 +722,21 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
kwdefaults=annotate.__kwdefaults__,
)
result = func(Format.VALUE_WITH_FAKE_GLOBALS)
for obj in globals.stringifiers:
obj.__class__ = ForwardRef
obj.__stringifier_dict__ = None # not needed for ForwardRef
if isinstance(obj.__ast_node__, str):
obj.__arg__ = obj.__ast_node__
obj.__ast_node__ = None
return result
globals.transmogrify()
if _is_evaluate:
if isinstance(result, ForwardRef):
return result.evaluate(format=Format.FORWARDREF)
else:
return result
else:
return {
key: (
val.evaluate(format=Format.FORWARDREF)
if isinstance(val, ForwardRef)
else val
)
for key, val in result.items()
}
elif format == Format.VALUE:
# Should be impossible because __annotate__ functions must not raise
# NotImplementedError for this format.
Expand All @@ -698,6 +745,39 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
raise ValueError(f"Invalid format: {format!r}")


def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluation):
if not annotate.__closure__:
return None
freevars = annotate.__code__.co_freevars
new_closure = []
for i, cell in enumerate(annotate.__closure__):
if i < len(freevars):
name = freevars[i]
else:
name = "__cell__"
new_cell = None
if allow_evaluation:
try:
cell.cell_contents
except ValueError:
pass
else:
new_cell = cell
if new_cell is None:
fwdref = _Stringifier(
name,
cell=cell,
owner=owner,
globals=annotate.__globals__,
is_class=is_class,
stringifier_dict=stringifier_dict,
)
stringifier_dict.stringifiers.append(fwdref)
new_cell = types.CellType(fwdref)
new_closure.append(new_cell)
return tuple(new_closure)


def _stringify_single(anno):
if anno is ...:
return "..."
Expand Down Expand Up @@ -809,7 +889,7 @@ def get_annotations(
# But if we didn't get it, we use __annotations__ instead.
ann = _get_dunder_annotations(obj)
if ann is not None:
return annotations_to_string(ann)
return annotations_to_string(ann)
case Format.VALUE_WITH_FAKE_GLOBALS:
raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only")
case _:
Expand Down
Loading
Loading