From 83ba140b8f907e75bf1ce36e92d19b5043a7d63f Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Tue, 3 May 2022 21:01:24 +0100 Subject: [PATCH 1/9] Disallow iteration of Union --- Lib/test/test_typing.py | 9 +++++++++ Lib/typing.py | 3 +++ 2 files changed, 12 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 55e18c08537df7..af5267745d7fa9 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1482,6 +1482,15 @@ def test_cannot_instantiate(self): with self.assertRaises(TypeError): type(u)() + def test_cannot_iterate(self): + with self.assertRaises(TypeError): + iter(Union) + with self.assertRaises(TypeError): + list(Union) + with self.assertRaises(TypeError): + for _ in Union: + pass + def test_union_generalization(self): self.assertFalse(Union[str, typing.Iterable[int]] == str) self.assertFalse(Union[str, typing.Iterable[int]] == typing.Iterable[int]) diff --git a/Lib/typing.py b/Lib/typing.py index bdc14e39033dcf..cd713629e46ac7 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -433,6 +433,9 @@ def __reduce__(self): def __call__(self, *args, **kwds): raise TypeError(f"Cannot instantiate {self!r}") + def __iter__(self): + raise TypeError(f"{self} does not support iteration") + def __or__(self, other): return Union[self, other] From 9cb69ca50833b91c9ddad3fd691d1d8ca99b4b7e Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 20:12:19 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2022-05-03-20-12-18.gh-issue-92261.aigLnb.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-05-03-20-12-18.gh-issue-92261.aigLnb.rst 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 00000000000000..df0228e273d8e3 --- /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``. From 42b2a56674aaec4f2f72d5d7028a9e961112d345 Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sat, 7 May 2022 12:53:03 +0100 Subject: [PATCH 3/9] Add test for isinstance(Iterable) --- Lib/test/test_typing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index af5267745d7fa9..a8dee650b8fe4f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1491,6 +1491,9 @@ def test_cannot_iterate(self): for _ in Union: pass + def test_is_not_instance_of_iterable(self): + self.assertNotIsInstance(Union, collections.abc.Iterable) + def test_union_generalization(self): self.assertFalse(Union[str, typing.Iterable[int]] == str) self.assertFalse(Union[str, typing.Iterable[int]] == typing.Iterable[int]) From 6d6d4ff8b1106d1949df4813a17e483518c9ba6c Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sat, 7 May 2022 12:53:21 +0100 Subject: [PATCH 4/9] Switch to __iter__ = None --- Lib/typing.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index cd713629e46ac7..b8676f2e8fa2be 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -433,9 +433,6 @@ def __reduce__(self): def __call__(self, *args, **kwds): raise TypeError(f"Cannot instantiate {self!r}") - def __iter__(self): - raise TypeError(f"{self} does not support iteration") - def __or__(self, other): return Union[self, other] @@ -452,6 +449,10 @@ def __subclasscheck__(self, cls): def __getitem__(self, parameters): return self._getitem(self, parameters) + # Prevent iteration without making ourselves duck type-compatible + # with Iterable. + __iter__ = None + class _LiteralSpecialForm(_SpecialForm, _root=True): def __getitem__(self, parameters): From 207a0f62a9604099e0c2f7a4297307aabf5563d2 Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sat, 7 May 2022 12:53:32 +0100 Subject: [PATCH 5/9] Test the specific error message --- Lib/test/test_typing.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a8dee650b8fe4f..26dac0c7b50171 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1483,11 +1483,12 @@ def test_cannot_instantiate(self): type(u)() def test_cannot_iterate(self): - with self.assertRaises(TypeError): + expected_message = "^'_SpecialForm' object is not iterable$" + with self.assertRaisesRegex(TypeError, expected_message): iter(Union) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, expected_message): list(Union) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, expected_message): for _ in Union: pass From 70d5c097faf10a74d77f3a8fc893ec26bbec37c1 Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sat, 7 May 2022 13:42:17 +0100 Subject: [PATCH 6/9] Add tests for and fix iteration of Any, List, Callable, Tuple, and Annotated --- Lib/test/test_typing.py | 44 +++++++++++++++++++++++++++++------------ Lib/typing.py | 29 ++++++++++++++++++--------- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 26dac0c7b50171..c13fab476f6b3f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1482,19 +1482,6 @@ def test_cannot_instantiate(self): with self.assertRaises(TypeError): type(u)() - def test_cannot_iterate(self): - expected_message = "^'_SpecialForm' object is not iterable$" - with self.assertRaisesRegex(TypeError, expected_message): - iter(Union) - with self.assertRaisesRegex(TypeError, expected_message): - list(Union) - with self.assertRaisesRegex(TypeError, expected_message): - for _ in Union: - pass - - def test_is_not_instance_of_iterable(self): - self.assertNotIsInstance(Union, collections.abc.Iterable) - def test_union_generalization(self): self.assertFalse(Union[str, typing.Iterable[int]] == str) self.assertFalse(Union[str, typing.Iterable[int]] == typing.Iterable[int]) @@ -7347,6 +7334,37 @@ def test_all_exported_names(self): self.assertSetEqual(computed_all, actual_all) +class TypeIterationTests(BaseTestCase): + _EXPECTED_ERROR_BY_TYPE = { + Any: "'_AnyMeta' object is not iterable", + Union: "'_SpecialForm' object is not iterable", + Union[str, int]: "'_UnionGenericAlias' object is not iterable", + Union[str, T]: "'_UnionGenericAlias' object is not iterable", + List: "'_SpecialGenericAlias' object is not iterable", + Tuple: "'_TupleType' object is not iterable", + Callable: "'_CallableType' object is not iterable", + Callable[..., T]: "'_CallableGenericAlias' object is not iterable", + Callable[[T], str]: "'_CallableGenericAlias' object is not iterable", + Annotated: "'type' object is not iterable", + Annotated[T, '']: "'_AnnotatedAlias' object is not iterable", + } + + def test_cannot_iterate(self): + for test_type, expected_error in self._EXPECTED_ERROR_BY_TYPE.items(): + with self.subTest(type=test_type): + expected_error_regex = '^{}$'.format(expected_error) + 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._EXPECTED_ERROR_BY_TYPE.keys(): + self.assertNotIsInstance(type_to_test, collections.abc.Iterable) + if __name__ == '__main__': main() diff --git a/Lib/typing.py b/Lib/typing.py index b8676f2e8fa2be..46ef2d9cf60056 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -405,9 +405,24 @@ def __deepcopy__(self, memo): 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): @@ -449,10 +464,6 @@ def __subclasscheck__(self, cls): def __getitem__(self, parameters): return self._getitem(self, parameters) - # Prevent iteration without making ourselves duck type-compatible - # with Iterable. - __iter__ = None - class _LiteralSpecialForm(_SpecialForm, _root=True): def __getitem__(self, parameters): @@ -1502,7 +1513,7 @@ def __iter__(self): # 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__ @@ -1545,7 +1556,7 @@ def __or__(self, right): 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__ @@ -1610,7 +1621,7 @@ def __getitem__(self, params): return self.copy_with(params) -class _UnionGenericAlias(_GenericAlias, _root=True): +class _UnionGenericAlias(_NotIterable, _GenericAlias, _root=True): def copy_with(self, params): return Union[params] @@ -2050,7 +2061,7 @@ def _proto_hook(other): 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' From a9eaa182ff31555a339e337d3452aa792f235619 Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sat, 7 May 2022 14:18:28 +0100 Subject: [PATCH 7/9] Make the error message that we test for less specific --- Lib/test/test_typing.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 52bed8ba6f2a66..6e1e8d6e1b4730 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -7349,24 +7349,24 @@ def test_all_exported_names(self): class TypeIterationTests(BaseTestCase): - _EXPECTED_ERROR_BY_TYPE = { - Any: "'_AnyMeta' object is not iterable", - Union: "'_SpecialForm' object is not iterable", - Union[str, int]: "'_UnionGenericAlias' object is not iterable", - Union[str, T]: "'_UnionGenericAlias' object is not iterable", - List: "'_SpecialGenericAlias' object is not iterable", - Tuple: "'_TupleType' object is not iterable", - Callable: "'_CallableType' object is not iterable", - Callable[..., T]: "'_CallableGenericAlias' object is not iterable", - Callable[[T], str]: "'_CallableGenericAlias' object is not iterable", - Annotated: "'type' object is not iterable", - Annotated[T, '']: "'_AnnotatedAlias' object is not iterable", - } + _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): - for test_type, expected_error in self._EXPECTED_ERROR_BY_TYPE.items(): + expected_error_regex = "object is not iterable" + for test_type in self._UNITERABLE_TYPES: with self.subTest(type=test_type): - expected_error_regex = '^{}$'.format(expected_error) with self.assertRaisesRegex(TypeError, expected_error_regex): iter(test_type) with self.assertRaisesRegex(TypeError, expected_error_regex): @@ -7376,7 +7376,7 @@ def test_cannot_iterate(self): pass def test_is_not_instance_of_iterable(self): - for type_to_test in self._EXPECTED_ERROR_BY_TYPE.keys(): + for type_to_test in self._UNITERABLE_TYPES: self.assertNotIsInstance(type_to_test, collections.abc.Iterable) From b82c9e0e9dbd3f6a372bdac31dddaf73b9014f01 Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sat, 7 May 2022 14:20:41 +0100 Subject: [PATCH 8/9] Add tests for list(list) and list(tuple) --- Lib/test/test_genericalias.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 5fba74ec864f1b..617bdace75b7a7 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -1,5 +1,6 @@ """Tests for C-implemented GenericAlias.""" +import collections import unittest import pickle import copy @@ -487,5 +488,25 @@ def test_del_iter(self): 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, collections.abc.Iterable) + + if __name__ == "__main__": unittest.main() From 64f22fb9576ecc20ee7316807aa8f3f0a6326756 Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sat, 7 May 2022 17:58:20 +0100 Subject: [PATCH 9/9] Use existing import of Iterable --- Lib/test/test_genericalias.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 617bdace75b7a7..6959c2ae3c80e3 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -1,6 +1,5 @@ """Tests for C-implemented GenericAlias.""" -import collections import unittest import pickle import copy @@ -505,7 +504,7 @@ def test_cannot_iterate(self): def test_is_not_instance_of_iterable(self): for type_to_test in self._UNITERABLE_TYPES: - self.assertNotIsInstance(type_to_test, collections.abc.Iterable) + self.assertNotIsInstance(type_to_test, Iterable) if __name__ == "__main__":