Skip to content

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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 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
115 changes: 99 additions & 16 deletions 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,112 @@ 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)
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)

# Now should pick up the globals of this module
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 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)
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