From 7540a432cedf478d03da1254c470ac552f397916 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Mon, 9 May 2022 22:39:31 -0700 Subject: gh-92261: Disallow iteration of Union (and other special forms) (GH-92262) (GH-92582) (cherry picked from commit 4739997e141c4c84bd2241d4d887c3c658d92700) Co-authored-by: Matthew Rahtz --- Lib/test/test_genericalias.py | 20 ++++++++++++++ Lib/test/test_typing.py | 31 ++++++++++++++++++++++ Lib/typing.py | 25 +++++++++++++---- .../2022-05-03-20-12-18.gh-issue-92261.aigLnb.rst | 1 + 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-05-03-20-12-18.gh-issue-92261.aigLnb.rst diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 5fba74e..6959c2a 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -487,5 +487,25 @@ class BaseTest(unittest.TestCase): del iter_x +class TypeIterationTests(unittest.TestCase): + _UNITERABLE_TYPES = (list, tuple) + + def test_cannot_iterate(self): + for test_type in self._UNITERABLE_TYPES: + with self.subTest(type=test_type): + expected_error_regex = "object is not iterable" + with self.assertRaisesRegex(TypeError, expected_error_regex): + iter(test_type) + with self.assertRaisesRegex(TypeError, expected_error_regex): + list(test_type) + with self.assertRaisesRegex(TypeError, expected_error_regex): + for _ in test_type: + pass + + def test_is_not_instance_of_iterable(self): + for type_to_test in self._UNITERABLE_TYPES: + self.assertNotIsInstance(type_to_test, Iterable) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8399465..6e1e8d6 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -7348,6 +7348,37 @@ class AllTests(BaseTestCase): self.assertSetEqual(computed_all, actual_all) +class TypeIterationTests(BaseTestCase): + _UNITERABLE_TYPES = ( + Any, + Union, + Union[str, int], + Union[str, T], + List, + Tuple, + Callable, + Callable[..., T], + Callable[[T], str], + Annotated, + Annotated[T, ''], + ) + + def test_cannot_iterate(self): + expected_error_regex = "object is not iterable" + for test_type in self._UNITERABLE_TYPES: + with self.subTest(type=test_type): + with self.assertRaisesRegex(TypeError, expected_error_regex): + iter(test_type) + with self.assertRaisesRegex(TypeError, expected_error_regex): + list(test_type) + with self.assertRaisesRegex(TypeError, expected_error_regex): + for _ in test_type: + pass + + def test_is_not_instance_of_iterable(self): + for type_to_test in self._UNITERABLE_TYPES: + self.assertNotIsInstance(type_to_test, collections.abc.Iterable) + if __name__ == '__main__': main() diff --git a/Lib/typing.py b/Lib/typing.py index bdc14e3..46ef2d9 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -405,9 +405,24 @@ class _Immutable: return self +class _NotIterable: + """Mixin to prevent iteration, without being compatible with Iterable. + + That is, we could do: + def __iter__(self): raise TypeError() + But this would make users of this mixin duck type-compatible with + collections.abc.Iterable - isinstance(foo, Iterable) would be True. + + Luckily, we can instead prevent iteration by setting __iter__ to None, which + is treated specially. + """ + + __iter__ = None + + # Internal indicator of special typing constructs. # See __doc__ instance attribute for specific docs. -class _SpecialForm(_Final, _root=True): +class _SpecialForm(_Final, _NotIterable, _root=True): __slots__ = ('_name', '__doc__', '_getitem') def __init__(self, getitem): @@ -1498,7 +1513,7 @@ class _GenericAlias(_BaseGenericAlias, _root=True): # 1 for List and 2 for Dict. It may be -1 if variable number of # parameters are accepted (needs custom __getitem__). -class _SpecialGenericAlias(_BaseGenericAlias, _root=True): +class _SpecialGenericAlias(_NotIterable, _BaseGenericAlias, _root=True): def __init__(self, origin, nparams, *, inst=True, name=None): if name is None: name = origin.__name__ @@ -1541,7 +1556,7 @@ class _SpecialGenericAlias(_BaseGenericAlias, _root=True): def __ror__(self, left): return Union[left, self] -class _CallableGenericAlias(_GenericAlias, _root=True): +class _CallableGenericAlias(_NotIterable, _GenericAlias, _root=True): def __repr__(self): assert self._name == 'Callable' args = self.__args__ @@ -1606,7 +1621,7 @@ class _TupleType(_SpecialGenericAlias, _root=True): return self.copy_with(params) -class _UnionGenericAlias(_GenericAlias, _root=True): +class _UnionGenericAlias(_NotIterable, _GenericAlias, _root=True): def copy_with(self, params): return Union[params] @@ -2046,7 +2061,7 @@ class Protocol(Generic, metaclass=_ProtocolMeta): cls.__init__ = _no_init_or_replace_init -class _AnnotatedAlias(_GenericAlias, _root=True): +class _AnnotatedAlias(_NotIterable, _GenericAlias, _root=True): """Runtime representation of an annotated type. At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't' diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-05-03-20-12-18.gh-issue-92261.aigLnb.rst b/Misc/NEWS.d/next/Core and Builtins/2022-05-03-20-12-18.gh-issue-92261.aigLnb.rst new file mode 100644 index 0000000..df0228e --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-05-03-20-12-18.gh-issue-92261.aigLnb.rst @@ -0,0 +1 @@ +Fix hang when trying to iterate over a ``typing.Union``. -- cgit v0.12