Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

gh-133960: Improve typing.evaluate_forward_ref #133961

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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 3 additions & 12 deletions 15 Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3500,20 +3500,11 @@ Introspection helpers
Evaluate an :class:`annotationlib.ForwardRef` as a :term:`type hint`.

This is similar to calling :meth:`annotationlib.ForwardRef.evaluate`,
but unlike that method, :func:`!evaluate_forward_ref` also:

* Recursively evaluates forward references nested within the type hint.
* Raises :exc:`TypeError` when it encounters certain objects that are
not valid type hints.
* Replaces type hints that evaluate to :const:`!None` with
:class:`types.NoneType`.
* Supports the :attr:`~annotationlib.Format.FORWARDREF` and
:attr:`~annotationlib.Format.STRING` formats.
but unlike that method, :func:`!evaluate_forward_ref` also
recursively evaluates forward references nested within the type hint.

See the documentation for :meth:`annotationlib.ForwardRef.evaluate` for
the meaning of the *owner*, *globals*, *locals*, and *type_params* parameters.
*format* specifies the format of the annotation and is a member of
the :class:`annotationlib.Format` enum.
the meaning of the *owner*, *globals*, *locals*, *type_params*, and *format* parameters.

.. versionadded:: 3.14

Expand Down
116 changes: 100 additions & 16 deletions 116 Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6859,12 +6859,10 @@ def test_forward_ref_and_final(self):
self.assertEqual(hints, {'value': Final})

def test_top_level_class_var(self):
# https://bugs.python.org/issue45166
with self.assertRaisesRegex(
TypeError,
r'typing.ClassVar\[int\] is not valid as type argument',
):
get_type_hints(ann_module6)
# This is not meaningful but we don't raise for it.
# https://github.com/python/cpython/issues/133959
hints = get_type_hints(ann_module6)
self.assertEqual(hints, {'wrong': ClassVar[int]})

def test_get_type_hints_typeddict(self):
self.assertEqual(get_type_hints(TotalMovie), {'title': str, 'year': int})
Expand Down Expand Up @@ -6967,6 +6965,11 @@ def foo(a: 'Callable[..., T]'):
self.assertEqual(get_type_hints(foo, globals(), locals()),
{'a': Callable[..., T]})

def test_special_forms_no_forward(self):
def f(x: ClassVar[int]):
pass
self.assertEqual(get_type_hints(f), {'x': ClassVar[int]})

def test_special_forms_forward(self):

class C:
Expand All @@ -6982,8 +6985,9 @@ class CF:
self.assertEqual(get_type_hints(C, globals())['b'], Final[int])
self.assertEqual(get_type_hints(C, globals())['x'], ClassVar)
self.assertEqual(get_type_hints(C, globals())['y'], Final)
with self.assertRaises(TypeError):
get_type_hints(CF, globals()),
lfi = get_type_hints(CF, globals())['b']
self.assertIs(get_origin(lfi), list)
self.assertEqual(get_args(lfi), (Final[int],))

def test_union_forward_recursion(self):
ValueList = List['Value']
Expand Down Expand Up @@ -7216,33 +7220,113 @@ class C(Generic[T]): pass
class EvaluateForwardRefTests(BaseTestCase):
def test_evaluate_forward_ref(self):
int_ref = ForwardRef('int')
missing = ForwardRef('missing')
self.assertIs(typing.evaluate_forward_ref(int_ref), int)
self.assertIs(
typing.evaluate_forward_ref(int_ref, type_params=()),
int,
)
self.assertIs(
typing.evaluate_forward_ref(int_ref, format=annotationlib.Format.VALUE),
int,
)
self.assertIs(
typing.evaluate_forward_ref(
int_ref, type_params=(), format=annotationlib.Format.FORWARDREF,
int_ref, format=annotationlib.Format.FORWARDREF,
),
int,
)
self.assertEqual(
typing.evaluate_forward_ref(
int_ref, format=annotationlib.Format.STRING,
),
'int',
)

def test_evaluate_forward_ref_undefined(self):
missing = ForwardRef('missing')
with self.assertRaises(NameError):
typing.evaluate_forward_ref(missing)
self.assertIs(
typing.evaluate_forward_ref(
missing, type_params=(), format=annotationlib.Format.FORWARDREF,
missing, format=annotationlib.Format.FORWARDREF,
),
missing,
)
self.assertEqual(
typing.evaluate_forward_ref(
int_ref, type_params=(), format=annotationlib.Format.STRING,
missing, format=annotationlib.Format.STRING,
),
'int',
"missing",
)

def test_evaluate_forward_ref_no_type_params(self):
ref = ForwardRef('int')
self.assertIs(typing.evaluate_forward_ref(ref), int)
def test_evaluate_forward_ref_nested(self):
ref = ForwardRef("int | list['str']")
self.assertEqual(
typing.evaluate_forward_ref(ref),
int | list[str],
)
self.assertEqual(
typing.evaluate_forward_ref(ref, format=annotationlib.Format.FORWARDREF),
int | list[str],
)
self.assertEqual(
typing.evaluate_forward_ref(ref, format=annotationlib.Format.STRING),
"int | list['str']",
)

why = ForwardRef('"\'str\'"')
self.assertIs(typing.evaluate_forward_ref(why), str)

def test_evaluate_forward_ref_none(self):
none_ref = ForwardRef('None')
self.assertIs(typing.evaluate_forward_ref(none_ref), None)

def test_globals(self):
A = "str"
ref = ForwardRef('list[A]')
with self.assertRaises(NameError):
typing.evaluate_forward_ref(ref)
Comment on lines +7285 to +7288
Copy link
Member

Choose a reason for hiding this comment

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

remind me why this needs to raise NameError? Does the globals argument not default to the globals of the module calling evaluate_forward_ref? I think that's what I'd naively expect.

The docs for typing.evaluate_forward_ref point me towards https://docs.python.org/3.14/library/annotationlib.html#annotationlib.ForwardRef.evaluate for information on these parameters, but the docs there are silent on what happens if globals is None and owner is None

Copy link
Member Author

Choose a reason for hiding this comment

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

It eventually defaults to empty globals. I think that's right for most use cases; usually you'll be evaluating a ForwardRef in a library that came from some user code, and the globals of the module where you're doing the evaluating aren't a particularly good place to look for data.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll make a separate PR adding this to the annotationlib docs

self.assertEqual(
typing.evaluate_forward_ref(ref, globals={'A': A}),
list[str],
)

def test_owner(self):
ref = ForwardRef("A")

with self.assertRaises(NameError):
typing.evaluate_forward_ref(ref)

# We default to the globals of `owner`,
# so it no longer raises `NameError`
self.assertIs(
typing.evaluate_forward_ref(ref, owner=Loop), A
)

def test_inherited_owner(self):
# owner passed to evaluate_forward_ref
ref = ForwardRef("list['A']")
self.assertEqual(
typing.evaluate_forward_ref(ref, owner=Loop),
list[A],
)

# owner set on the ForwardRef
ref = ForwardRef("list['A']", owner=Loop)
self.assertEqual(
typing.evaluate_forward_ref(ref),
list[A],
)

def test_partial_evaluation(self):
ref = ForwardRef("list[A]")
with self.assertRaises(NameError):
typing.evaluate_forward_ref(ref)

self.assertEqual(
typing.evaluate_forward_ref(ref, format=annotationlib.Format.FORWARDREF),
list[EqualToForwardRef('A')],
)


class CollectionsAbcTests(BaseTestCase):
Expand Down
52 changes: 25 additions & 27 deletions 52 Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -956,12 +956,8 @@ def evaluate_forward_ref(
"""Evaluate a forward reference as a type hint.

This is similar to calling the ForwardRef.evaluate() method,
but unlike that method, evaluate_forward_ref() also:

* Recursively evaluates forward references nested within the type hint.
* Rejects certain objects that are not valid type hints.
* Replaces type hints that evaluate to None with types.NoneType.
* Supports the *FORWARDREF* and *STRING* formats.
but unlike that method, evaluate_forward_ref() also
recursively evaluates forward references nested within the type hint.

*forward_ref* must be an instance of ForwardRef. *owner*, if given,
should be the object that holds the annotations that the forward reference
Expand All @@ -981,23 +977,24 @@ def evaluate_forward_ref(
if forward_ref.__forward_arg__ in _recursive_guard:
return forward_ref

try:
value = forward_ref.evaluate(globals=globals, locals=locals,
type_params=type_params, owner=owner)
except NameError:
if format == _lazy_annotationlib.Format.FORWARDREF:
return forward_ref
else:
raise

type_ = _type_check(
value,
"Forward references must evaluate to types.",
is_argument=forward_ref.__forward_is_argument__,
allow_special_forms=forward_ref.__forward_is_class__,
)
if format is None:
format = _lazy_annotationlib.Format.VALUE
value = forward_ref.evaluate(globals=globals, locals=locals,
type_params=type_params, owner=owner, format=format)

if (isinstance(value, _lazy_annotationlib.ForwardRef)
and format == _lazy_annotationlib.Format.FORWARDREF):
return value

if isinstance(value, str):
value = _make_forward_ref(value, module=forward_ref.__forward_module__,
owner=owner or forward_ref.__owner__,
is_argument=forward_ref.__forward_is_argument__,
is_class=forward_ref.__forward_is_class__)
if owner is None:
owner = forward_ref.__owner__
return _eval_type(
type_,
value,
globals,
locals,
type_params,
Expand Down Expand Up @@ -2338,12 +2335,12 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
# This only affects ForwardRefs.
base_globals, base_locals = base_locals, base_globals
for name, value in ann.items():
if value is None:
value = type(None)
if isinstance(value, str):
value = _make_forward_ref(value, is_argument=False, is_class=True)
value = _eval_type(value, base_globals, base_locals, base.__type_params__,
format=format, owner=obj)
if value is None:
value = type(None)
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
hints[name] = value
if include_extras or format == Format.STRING:
return hints
Expand Down Expand Up @@ -2377,8 +2374,6 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
localns = globalns
type_params = getattr(obj, "__type_params__", ())
for name, value in hints.items():
if value is None:
value = type(None)
if isinstance(value, str):
# class-level forward refs were handled above, this must be either
# a module-level annotation or a function argument annotation
Expand All @@ -2387,7 +2382,10 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
is_argument=not isinstance(obj, types.ModuleType),
is_class=False,
)
hints[name] = _eval_type(value, globalns, localns, type_params, format=format, owner=obj)
value = _eval_type(value, globalns, localns, type_params, format=format, owner=obj)
if value is None:
value = type(None)
hints[name] = value
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Simplify and improve :func:`typing.evaluate_forward_ref`. It now no longer
raises errors on certain invalid types. In several situations, it is now
able to evaluate forward references that were previously unsupported.
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.