Hi!
We have a Python library that parses function annotations and uses this information to resolve the corresponding function overloads defined in another language (e.g.: C++, CUDA).
This has been working well, and we’re now in the process of adding support for from __future__ import annotations, however we hit a small bump in the road: string representations of type hints don’t evaluate to their corresponding type if they refer to a variable from a parent scope.
Take the following snippet as an example.
from __future__ import annotations
import inspect
class FooData:
value: float
class Foo:
Data = FooData
def create_fn(foo: Foo):
def fn(data: foo.Data):
# foo
pass
return fn
fn = create_fn(Foo())
try:
print(inspect.get_annotations(fn, eval_str=True))
except NameError as e:
print(f"inspect.get_annotations() failed with: {e}")
print(f"'foo' in fn.__code__.co_freevars ? {'foo' in fn.__code__.co_freevars}")
try:
fn_locals = dict(zip(fn.__code__.co_freevars, (x.cell_contents for x in (fn.__closure__ or ()))))
print(eval("foo.Data", globals(), fn_locals))
except NameError as e:
print(f"eval() failed with: {e}")
As it stands, the type hint foo.Data doesn’t get resolved as expected: inspect.get_annotations() fails with eval_str=True, and the variable foo isn’t found in fn.__code__.co_freevars, so we can’t manually evaluate the type by passing the dictionary of closure variables to eval().
However, if we reference foo within the inner function (e.g.: by uncommenting the line from that snippet), then foo seems to get promoted to a closure variable since it now shows up in fn.__code__.co_freevars. Alas, inspect.get_annotations(eval_str=True) still fails because it doesn’t seem to account for such closure variables?
At first glance, this use case seems to be a valid pattern, so so we’re wondering if this is a known issue or if this could be the expected behavior?
Thank you!