diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ba0800fae90e301..d8a909d3502f9c7 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2364,6 +2364,38 @@ def foo(a: Union['T']): self.assertEqual(get_type_hints(foo, globals(), locals()), {'a': Union[T]}) + def test_union_forward_recursion(self): + ValueList = List['Value'] + Value = Union[str, ValueList] + + class C: + foo: List[Value] + class D: + foo: Union[Value, ValueList] + class E: + foo: Union[List[Value], ValueList] + class F: + foo: Union[Value, List[Value], ValueList] + + self.assertEqual(get_type_hints(C, globals(), locals()), get_type_hints(C, globals(), locals())) + self.assertEqual(get_type_hints(C, globals(), locals()), + {'foo': List[Union[str, List[Union[str, List['Value']]]]]}) + self.assertEqual(get_type_hints(D, globals(), locals()), + {'foo': Union[str, List[Union[str, List['Value']]]]}) + self.assertEqual(get_type_hints(E, globals(), locals()), + {'foo': Union[ + List[Union[str, List[Union[str, List['Value']]]]], + List[Union[str, List['Value']]] + ] + }) + self.assertEqual(get_type_hints(F, globals(), locals()), + {'foo': Union[ + str, + List[Union[str, List['Value']]], + List[Union[str, List[Union[str, List['Value']]]]] + ] + }) + def test_tuple_forward(self): def foo(a: Tuple['T']): diff --git a/Lib/typing.py b/Lib/typing.py index 5f1a0ad3d637430..f24bae7fdf00ca8 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -491,7 +491,7 @@ class ForwardRef(_Final, _root=True): __slots__ = ('__forward_arg__', '__forward_code__', '__forward_evaluated__', '__forward_value__', - '__forward_is_argument__') + '__forward_is_argument__', '__filter_recursion_value__') def __init__(self, arg, is_argument=True): if not isinstance(arg, str): @@ -505,6 +505,7 @@ def __init__(self, arg, is_argument=True): self.__forward_evaluated__ = False self.__forward_value__ = None self.__forward_is_argument__ = is_argument + self.__filter_recursion_value__ = None def _evaluate(self, globalns, localns): if not self.__forward_evaluated__ or localns is not globalns: @@ -521,6 +522,16 @@ def _evaluate(self, globalns, localns): self.__forward_evaluated__ = True return self.__forward_value__ + def _check_recursion(self, value, forward_args): + for val in value.__args__: + if isinstance(val, _GenericAlias): + self._check_recursion(val, forward_args) + elif self is val: + forward_args.append(...) + else: + forward_args.append(val) + return tuple(forward_args) + def __eq__(self, other): if not isinstance(other, ForwardRef): return NotImplemented @@ -528,7 +539,11 @@ def __eq__(self, other): self.__forward_value__ == other.__forward_value__) def __hash__(self): - return hash((self.__forward_arg__, self.__forward_value__)) + forward_value = self.__forward_value__ + if self.__forward_evaluated__ and not self.__filter_recursion_value__ \ + and isinstance(forward_value, _GenericAlias): + self.__filter_recursion_value__ = forward_value.copy_with(self._check_recursion(forward_value, [])) + return hash((self.__forward_arg__, self.__filter_recursion_value__ or forward_value)) def __repr__(self): return f'ForwardRef({self.__forward_arg__!r})' diff --git a/Misc/NEWS.d/next/Library/2019-08-28-19-00-50.bpo-37806.HdrlIB.rst b/Misc/NEWS.d/next/Library/2019-08-28-19-00-50.bpo-37806.HdrlIB.rst new file mode 100644 index 000000000000000..50853efd7ace405 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-08-28-19-00-50.bpo-37806.HdrlIB.rst @@ -0,0 +1 @@ +Fix infinite recursion from :func:`typing.get_type_hints`. Patch by hongweipeng.