Skip to content
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