Skip to content

Commit 9c6d2db

Browse files
[3.14] gh-133960: Improve typing.evaluate_forward_ref (GH-133961) (#134663)
gh-133960: Improve typing.evaluate_forward_ref (GH-133961) As explained in GH-133960, this removes most of the behavior differences with ForwardRef.evaluate. The remaining difference is about recursive evaluation of forwardrefs; this is practically useful in cases where an annotation refers to a type alias that itself is string-valued. This also improves several edge cases that were previously not handled optimally. For example, the function now takes advantage of the partial evaluation behavior of ForwardRef.evaluate() to evaluate more ForwardRefs in the FORWARDREF format. This also fixes GH-133959 as a side effect, because the buggy behavior in GH-133959 derives from evaluate_forward_ref(). (cherry picked from commit 57fef27) Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent 8c66534 commit 9c6d2db

File tree

4 files changed

+131
-55
lines changed

4 files changed

+131
-55
lines changed

Doc/library/typing.rst

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3500,20 +3500,11 @@ Introspection helpers
35003500
Evaluate an :class:`annotationlib.ForwardRef` as a :term:`type hint`.
35013501

35023502
This is similar to calling :meth:`annotationlib.ForwardRef.evaluate`,
3503-
but unlike that method, :func:`!evaluate_forward_ref` also:
3504-
3505-
* Recursively evaluates forward references nested within the type hint.
3506-
* Raises :exc:`TypeError` when it encounters certain objects that are
3507-
not valid type hints.
3508-
* Replaces type hints that evaluate to :const:`!None` with
3509-
:class:`types.NoneType`.
3510-
* Supports the :attr:`~annotationlib.Format.FORWARDREF` and
3511-
:attr:`~annotationlib.Format.STRING` formats.
3503+
but unlike that method, :func:`!evaluate_forward_ref` also
3504+
recursively evaluates forward references nested within the type hint.
35123505

35133506
See the documentation for :meth:`annotationlib.ForwardRef.evaluate` for
3514-
the meaning of the *owner*, *globals*, *locals*, and *type_params* parameters.
3515-
*format* specifies the format of the annotation and is a member of
3516-
the :class:`annotationlib.Format` enum.
3507+
the meaning of the *owner*, *globals*, *locals*, *type_params*, and *format* parameters.
35173508

35183509
.. versionadded:: 3.14
35193510

Lib/test/test_typing.py

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6859,12 +6859,10 @@ def test_forward_ref_and_final(self):
68596859
self.assertEqual(hints, {'value': Final})
68606860

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

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

6968+
def test_special_forms_no_forward(self):
6969+
def f(x: ClassVar[int]):
6970+
pass
6971+
self.assertEqual(get_type_hints(f), {'x': ClassVar[int]})
6972+
69706973
def test_special_forms_forward(self):
69716974

69726975
class C:
@@ -6982,8 +6985,9 @@ class CF:
69826985
self.assertEqual(get_type_hints(C, globals())['b'], Final[int])
69836986
self.assertEqual(get_type_hints(C, globals())['x'], ClassVar)
69846987
self.assertEqual(get_type_hints(C, globals())['y'], Final)
6985-
with self.assertRaises(TypeError):
6986-
get_type_hints(CF, globals()),
6988+
lfi = get_type_hints(CF, globals())['b']
6989+
self.assertIs(get_origin(lfi), list)
6990+
self.assertEqual(get_args(lfi), (Final[int],))
69876991

69886992
def test_union_forward_recursion(self):
69896993
ValueList = List['Value']
@@ -7216,33 +7220,113 @@ class C(Generic[T]): pass
72167220
class EvaluateForwardRefTests(BaseTestCase):
72177221
def test_evaluate_forward_ref(self):
72187222
int_ref = ForwardRef('int')
7219-
missing = ForwardRef('missing')
7223+
self.assertIs(typing.evaluate_forward_ref(int_ref), int)
72207224
self.assertIs(
72217225
typing.evaluate_forward_ref(int_ref, type_params=()),
72227226
int,
72237227
)
7228+
self.assertIs(
7229+
typing.evaluate_forward_ref(int_ref, format=annotationlib.Format.VALUE),
7230+
int,
7231+
)
72247232
self.assertIs(
72257233
typing.evaluate_forward_ref(
7226-
int_ref, type_params=(), format=annotationlib.Format.FORWARDREF,
7234+
int_ref, format=annotationlib.Format.FORWARDREF,
72277235
),
72287236
int,
72297237
)
7238+
self.assertEqual(
7239+
typing.evaluate_forward_ref(
7240+
int_ref, format=annotationlib.Format.STRING,
7241+
),
7242+
'int',
7243+
)
7244+
7245+
def test_evaluate_forward_ref_undefined(self):
7246+
missing = ForwardRef('missing')
7247+
with self.assertRaises(NameError):
7248+
typing.evaluate_forward_ref(missing)
72307249
self.assertIs(
72317250
typing.evaluate_forward_ref(
7232-
missing, type_params=(), format=annotationlib.Format.FORWARDREF,
7251+
missing, format=annotationlib.Format.FORWARDREF,
72337252
),
72347253
missing,
72357254
)
72367255
self.assertEqual(
72377256
typing.evaluate_forward_ref(
7238-
int_ref, type_params=(), format=annotationlib.Format.STRING,
7257+
missing, format=annotationlib.Format.STRING,
72397258
),
7240-
'int',
7259+
"missing",
72417260
)
72427261

7243-
def test_evaluate_forward_ref_no_type_params(self):
7244-
ref = ForwardRef('int')
7245-
self.assertIs(typing.evaluate_forward_ref(ref), int)
7262+
def test_evaluate_forward_ref_nested(self):
7263+
ref = ForwardRef("int | list['str']")
7264+
self.assertEqual(
7265+
typing.evaluate_forward_ref(ref),
7266+
int | list[str],
7267+
)
7268+
self.assertEqual(
7269+
typing.evaluate_forward_ref(ref, format=annotationlib.Format.FORWARDREF),
7270+
int | list[str],
7271+
)
7272+
self.assertEqual(
7273+
typing.evaluate_forward_ref(ref, format=annotationlib.Format.STRING),
7274+
"int | list['str']",
7275+
)
7276+
7277+
why = ForwardRef('"\'str\'"')
7278+
self.assertIs(typing.evaluate_forward_ref(why), str)
7279+
7280+
def test_evaluate_forward_ref_none(self):
7281+
none_ref = ForwardRef('None')
7282+
self.assertIs(typing.evaluate_forward_ref(none_ref), None)
7283+
7284+
def test_globals(self):
7285+
A = "str"
7286+
ref = ForwardRef('list[A]')
7287+
with self.assertRaises(NameError):
7288+
typing.evaluate_forward_ref(ref)
7289+
self.assertEqual(
7290+
typing.evaluate_forward_ref(ref, globals={'A': A}),
7291+
list[str],
7292+
)
7293+
7294+
def test_owner(self):
7295+
ref = ForwardRef("A")
7296+
7297+
with self.assertRaises(NameError):
7298+
typing.evaluate_forward_ref(ref)
7299+
7300+
# We default to the globals of `owner`,
7301+
# so it no longer raises `NameError`
7302+
self.assertIs(
7303+
typing.evaluate_forward_ref(ref, owner=Loop), A
7304+
)
7305+
7306+
def test_inherited_owner(self):
7307+
# owner passed to evaluate_forward_ref
7308+
ref = ForwardRef("list['A']")
7309+
self.assertEqual(
7310+
typing.evaluate_forward_ref(ref, owner=Loop),
7311+
list[A],
7312+
)
7313+
7314+
# owner set on the ForwardRef
7315+
ref = ForwardRef("list['A']", owner=Loop)
7316+
self.assertEqual(
7317+
typing.evaluate_forward_ref(ref),
7318+
list[A],
7319+
)
7320+
7321+
def test_partial_evaluation(self):
7322+
ref = ForwardRef("list[A]")
7323+
with self.assertRaises(NameError):
7324+
typing.evaluate_forward_ref(ref)
7325+
7326+
self.assertEqual(
7327+
typing.evaluate_forward_ref(ref, format=annotationlib.Format.FORWARDREF),
7328+
list[EqualToForwardRef('A')],
7329+
)
72467330

72477331

72487332
class CollectionsAbcTests(BaseTestCase):

Lib/typing.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -956,12 +956,8 @@ def evaluate_forward_ref(
956956
"""Evaluate a forward reference as a type hint.
957957
958958
This is similar to calling the ForwardRef.evaluate() method,
959-
but unlike that method, evaluate_forward_ref() also:
960-
961-
* Recursively evaluates forward references nested within the type hint.
962-
* Rejects certain objects that are not valid type hints.
963-
* Replaces type hints that evaluate to None with types.NoneType.
964-
* Supports the *FORWARDREF* and *STRING* formats.
959+
but unlike that method, evaluate_forward_ref() also
960+
recursively evaluates forward references nested within the type hint.
965961
966962
*forward_ref* must be an instance of ForwardRef. *owner*, if given,
967963
should be the object that holds the annotations that the forward reference
@@ -981,23 +977,24 @@ def evaluate_forward_ref(
981977
if forward_ref.__forward_arg__ in _recursive_guard:
982978
return forward_ref
983979

984-
try:
985-
value = forward_ref.evaluate(globals=globals, locals=locals,
986-
type_params=type_params, owner=owner)
987-
except NameError:
988-
if format == _lazy_annotationlib.Format.FORWARDREF:
989-
return forward_ref
990-
else:
991-
raise
992-
993-
type_ = _type_check(
994-
value,
995-
"Forward references must evaluate to types.",
996-
is_argument=forward_ref.__forward_is_argument__,
997-
allow_special_forms=forward_ref.__forward_is_class__,
998-
)
980+
if format is None:
981+
format = _lazy_annotationlib.Format.VALUE
982+
value = forward_ref.evaluate(globals=globals, locals=locals,
983+
type_params=type_params, owner=owner, format=format)
984+
985+
if (isinstance(value, _lazy_annotationlib.ForwardRef)
986+
and format == _lazy_annotationlib.Format.FORWARDREF):
987+
return value
988+
989+
if isinstance(value, str):
990+
value = _make_forward_ref(value, module=forward_ref.__forward_module__,
991+
owner=owner or forward_ref.__owner__,
992+
is_argument=forward_ref.__forward_is_argument__,
993+
is_class=forward_ref.__forward_is_class__)
994+
if owner is None:
995+
owner = forward_ref.__owner__
999996
return _eval_type(
1000-
type_,
997+
value,
1001998
globals,
1002999
locals,
10031000
type_params,
@@ -2338,12 +2335,12 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
23382335
# This only affects ForwardRefs.
23392336
base_globals, base_locals = base_locals, base_globals
23402337
for name, value in ann.items():
2341-
if value is None:
2342-
value = type(None)
23432338
if isinstance(value, str):
23442339
value = _make_forward_ref(value, is_argument=False, is_class=True)
23452340
value = _eval_type(value, base_globals, base_locals, base.__type_params__,
23462341
format=format, owner=obj)
2342+
if value is None:
2343+
value = type(None)
23472344
hints[name] = value
23482345
if include_extras or format == Format.STRING:
23492346
return hints
@@ -2377,8 +2374,6 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
23772374
localns = globalns
23782375
type_params = getattr(obj, "__type_params__", ())
23792376
for name, value in hints.items():
2380-
if value is None:
2381-
value = type(None)
23822377
if isinstance(value, str):
23832378
# class-level forward refs were handled above, this must be either
23842379
# a module-level annotation or a function argument annotation
@@ -2387,7 +2382,10 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
23872382
is_argument=not isinstance(obj, types.ModuleType),
23882383
is_class=False,
23892384
)
2390-
hints[name] = _eval_type(value, globalns, localns, type_params, format=format, owner=obj)
2385+
value = _eval_type(value, globalns, localns, type_params, format=format, owner=obj)
2386+
if value is None:
2387+
value = type(None)
2388+
hints[name] = value
23912389
return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
23922390

23932391

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Simplify and improve :func:`typing.evaluate_forward_ref`. It now no longer
2+
raises errors on certain invalid types. In several situations, it is now
3+
able to evaluate forward references that were previously unsupported.

0 commit comments

Comments
 (0)