Skip to content
37 changes: 23 additions & 14 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,33 +150,42 @@ def evaluate(
if globals is None:
globals = {}

if type_params is None and owner is not None:
type_params = getattr(owner, "__type_params__", None)

if locals is None:
locals = {}
if isinstance(owner, type):
locals.update(vars(owner))
elif (
type_params is not None
or isinstance(self.__cell__, dict)
or self.__extra_names__
):
# Create a new locals dict if necessary,
# to avoid mutating the argument.
locals = dict(locals)

if type_params is None and owner is not None:
# "Inject" type parameters into the local namespace
# (unless they are shadowed by assignments *in* the local namespace),
# as a way of emulating annotation scopes when calling `eval()`
type_params = getattr(owner, "__type_params__", None)

# Type parameters exist in their own scope, which is logically
# between the locals and the globals. We simulate this by adding
# them to the globals. Similar reasoning applies to nonlocals stored in cells.
if type_params is not None or isinstance(self.__cell__, dict):
globals = dict(globals)
# "Inject" type parameters into the local namespace
# (unless they are shadowed by assignments *in* the local namespace),
# as a way of emulating annotation scopes when calling `eval()`
if type_params is not None:
for param in type_params:
globals[param.__name__] = param
if param.__name__ not in locals:
locals[param.__name__] = param

# Similar logic can be used for nonlocals, which should not
# override locals.
if isinstance(self.__cell__, dict):
for cell_name, cell_value in self.__cell__.items():
try:
globals[cell_name] = cell_value.cell_contents
if cell_name not in locals:
locals[cell_name] = cell_value.cell_contents
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you narrow the try-except so it only covers this line? (The part that can raise ValueError is the .cell_contents attribute read.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully replacing with setdefault is fine - if the object doesn't have a 'setdefault' attribute, it will raise an AttributeError not ValueError.

except ValueError:
pass

if self.__extra_names__:
locals = {**locals, **self.__extra_names__}
locals.update(self.__extra_names__)

arg = self.__forward_arg__
if arg.isidentifier() and not keyword.iskeyword(arg):
Expand Down
18 changes: 18 additions & 0 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1926,6 +1926,24 @@ def test_fwdref_invalid_syntax(self):
with self.assertRaises(SyntaxError):
fr.evaluate()

def test_re_evaluate_generics(self):
global global_alias

class C:
x: global_alias[int]

# Evaluate the ForwardRef once
evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(
format=Format.FORWARDREF
)

# Now define the global and ensure that the ForwardRef evaluates
global_alias = list
self.assertEqual(evaluated.evaluate(), list[int])

# If we run this test again, ensure the type is still undefined
del global_alias


class TestAnnotationLib(unittest.TestCase):
def test__all__(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :meth:`annotationlib.ForwardRef.evaluate` returning
:class:`~annotationlib.ForwardRef` objects which don't update with new
globals.
Loading