bpo-41341: Recursive evaluation of ForwardRef in get_type_hints (#21553)

The issue raised by recursive evaluation is infinite recursion with
recursive types. In that case, only the first recursive ForwardRef is
evaluated.
This commit is contained in:
wyfo 2020-07-22 21:47:28 +02:00 committed by GitHub
parent bf2f76ec09
commit 653f420b53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 21 additions and 6 deletions

View File

@ -2456,6 +2456,12 @@ class ForwardRefTests(BaseTestCase):
self.assertEqual(get_type_hints(foo, globals(), locals()), self.assertEqual(get_type_hints(foo, globals(), locals()),
{'a': tuple[T]}) {'a': tuple[T]})
def test_double_forward(self):
def foo(a: 'List[\'int\']'):
pass
self.assertEqual(get_type_hints(foo, globals(), locals()),
{'a': List[int]})
def test_forward_recursion_actually(self): def test_forward_recursion_actually(self):
def namespace1(): def namespace1():
a = typing.ForwardRef('A') a = typing.ForwardRef('A')

View File

@ -244,14 +244,16 @@ def _tp_cache(func):
return inner return inner
def _eval_type(t, globalns, localns): def _eval_type(t, globalns, localns, recursive_guard=frozenset()):
"""Evaluate all forward reverences in the given type t. """Evaluate all forward reverences in the given type t.
For use of globalns and localns see the docstring for get_type_hints(). For use of globalns and localns see the docstring for get_type_hints().
recursive_guard is used to prevent prevent infinite recursion
with recursive ForwardRef.
""" """
if isinstance(t, ForwardRef): if isinstance(t, ForwardRef):
return t._evaluate(globalns, localns) return t._evaluate(globalns, localns, recursive_guard)
if isinstance(t, (_GenericAlias, GenericAlias)): if isinstance(t, (_GenericAlias, GenericAlias)):
ev_args = tuple(_eval_type(a, globalns, localns) for a in t.__args__) ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__)
if ev_args == t.__args__: if ev_args == t.__args__:
return t return t
if isinstance(t, GenericAlias): if isinstance(t, GenericAlias):
@ -477,7 +479,9 @@ class ForwardRef(_Final, _root=True):
self.__forward_value__ = None self.__forward_value__ = None
self.__forward_is_argument__ = is_argument self.__forward_is_argument__ = is_argument
def _evaluate(self, globalns, localns): def _evaluate(self, globalns, localns, recursive_guard):
if self.__forward_arg__ in recursive_guard:
return self
if not self.__forward_evaluated__ or localns is not globalns: if not self.__forward_evaluated__ or localns is not globalns:
if globalns is None and localns is None: if globalns is None and localns is None:
globalns = localns = {} globalns = localns = {}
@ -485,10 +489,14 @@ class ForwardRef(_Final, _root=True):
globalns = localns globalns = localns
elif localns is None: elif localns is None:
localns = globalns localns = globalns
self.__forward_value__ = _type_check( type_ =_type_check(
eval(self.__forward_code__, globalns, localns), eval(self.__forward_code__, globalns, localns),
"Forward references must evaluate to types.", "Forward references must evaluate to types.",
is_argument=self.__forward_is_argument__) is_argument=self.__forward_is_argument__,
)
self.__forward_value__ = _eval_type(
type_, globalns, localns, recursive_guard | {self.__forward_arg__}
)
self.__forward_evaluated__ = True self.__forward_evaluated__ = True
return self.__forward_value__ return self.__forward_value__

View File

@ -0,0 +1 @@
Recursive evaluation of `typing.ForwardRef` in `get_type_hints`.