Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-92261: Disallow iteration of Union (and other _SpecialForms) #92262

Merged
merged 10 commits into from
May 8, 2022
31 changes: 31 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7334,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()
25 changes: 20 additions & 5 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -1498,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__
Expand Down Expand Up @@ -1541,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__
Expand Down Expand Up @@ -1606,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]

Expand Down Expand Up @@ -2046,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'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix hang when trying to iterate over a ``typing.Union``.