From 22214ab0af2cbea1611b2193354f248ca6b03e87 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 16 Nov 2016 18:25:04 -0500 Subject: [PATCH] Issue #28720: Add collections.abc.AsyncGenerator. --- Doc/library/collections.abc.rst | 8 ++++ Doc/whatsnew/3.6.rst | 4 ++ Lib/_collections_abc.py | 59 ++++++++++++++++++++++- Lib/test/test_collections.py | 84 ++++++++++++++++++++++++++++++++- Misc/NEWS | 2 + 5 files changed, 155 insertions(+), 2 deletions(-) diff --git a/Doc/library/collections.abc.rst b/Doc/library/collections.abc.rst index e4b75a0f0a0..3ac49db78d7 100644 --- a/Doc/library/collections.abc.rst +++ b/Doc/library/collections.abc.rst @@ -92,6 +92,7 @@ ABC Inherits from Abstract Methods Mixin :class:`Coroutine` :class:`Awaitable` ``send``, ``throw`` ``close`` :class:`AsyncIterable` ``__aiter__`` :class:`AsyncIterator` :class:`AsyncIterable` ``__anext__`` ``__aiter__`` +:class:`AsyncGenerator` :class:`AsyncIterator` ``asend``, ``athrow`` ``aclose``, ``__aiter__``, ``__anext__`` ========================== ====================== ======================= ==================================================== @@ -222,6 +223,13 @@ ABC Inherits from Abstract Methods Mixin .. versionadded:: 3.5 +.. class:: Generator + + ABC for asynchronous generator classes that implement the protocol + defined in :pep:`525` and :pep:`492`. + + .. versionadded:: 3.6 + These ABCs allow us to ask classes or instances if they provide particular functionality, for example:: diff --git a/Doc/whatsnew/3.6.rst b/Doc/whatsnew/3.6.rst index 801191c9b41..ce8ab98abd1 100644 --- a/Doc/whatsnew/3.6.rst +++ b/Doc/whatsnew/3.6.rst @@ -912,6 +912,10 @@ The new :class:`~collections.abc.Reversible` abstract base class represents iterable classes that also provide the :meth:`__reversed__`. (Contributed by Ivan Levkivskyi in :issue:`25987`.) +The new :class:`~collections.abc.AsyncGenerator` abstract base class represents +asynchronous generators. +(Contributed by Yury Selivanov in :issue:`28720`.) + The :func:`~collections.namedtuple` function now accepts an optional keyword argument *module*, which, when specified, is used for the ``__module__`` attribute of the returned named tuple class. diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index ee7d0f18f81..b172f3f360e 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -9,7 +9,8 @@ Unit tests are in test_collections. from abc import ABCMeta, abstractmethod import sys -__all__ = ["Awaitable", "Coroutine", "AsyncIterable", "AsyncIterator", +__all__ = ["Awaitable", "Coroutine", + "AsyncIterable", "AsyncIterator", "AsyncGenerator", "Hashable", "Iterable", "Iterator", "Generator", "Reversible", "Sized", "Container", "Callable", "Collection", "Set", "MutableSet", @@ -59,6 +60,11 @@ _coro = _coro() coroutine = type(_coro) _coro.close() # Prevent ResourceWarning del _coro +## asynchronous generator ## +async def _ag(): yield +_ag = _ag() +async_generator = type(_ag) +del _ag ### ONE-TRICK PONIES ### @@ -183,6 +189,57 @@ class AsyncIterator(AsyncIterable): return NotImplemented +class AsyncGenerator(AsyncIterator): + + __slots__ = () + + async def __anext__(self): + """Return the next item from the asynchronous generator. + When exhausted, raise StopAsyncIteration. + """ + return await self.asend(None) + + @abstractmethod + async def asend(self, value): + """Send a value into the asynchronous generator. + Return next yielded value or raise StopAsyncIteration. + """ + raise StopAsyncIteration + + @abstractmethod + async def athrow(self, typ, val=None, tb=None): + """Raise an exception in the asynchronous generator. + Return next yielded value or raise StopAsyncIteration. + """ + if val is None: + if tb is None: + raise typ + val = typ() + if tb is not None: + val = val.with_traceback(tb) + raise val + + async def aclose(self): + """Raise GeneratorExit inside coroutine. + """ + try: + await self.athrow(GeneratorExit) + except (GeneratorExit, StopAsyncIteration): + pass + else: + raise RuntimeError("asynchronous generator ignored GeneratorExit") + + @classmethod + def __subclasshook__(cls, C): + if cls is AsyncGenerator: + return _check_methods(C, '__aiter__', '__anext__', + 'asend', 'athrow', 'aclose') + return NotImplemented + + +AsyncGenerator.register(async_generator) + + class Iterable(metaclass=ABCMeta): __slots__ = () diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 52ff256eb94..87454cc6704 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -19,7 +19,8 @@ from collections import namedtuple, Counter, OrderedDict, _count_elements from collections import UserDict, UserString, UserList from collections import ChainMap from collections import deque -from collections.abc import Awaitable, Coroutine, AsyncIterator, AsyncIterable +from collections.abc import Awaitable, Coroutine +from collections.abc import AsyncIterator, AsyncIterable, AsyncGenerator from collections.abc import Hashable, Iterable, Iterator, Generator, Reversible from collections.abc import Sized, Container, Callable, Collection from collections.abc import Set, MutableSet @@ -959,6 +960,87 @@ class TestOneTrickPonyABCs(ABCTestCase): self.assertRaises(RuntimeError, IgnoreGeneratorExit().close) + def test_AsyncGenerator(self): + class NonAGen1: + def __aiter__(self): return self + def __anext__(self): return None + def aclose(self): pass + def athrow(self, typ, val=None, tb=None): pass + + class NonAGen2: + def __aiter__(self): return self + def __anext__(self): return None + def aclose(self): pass + def asend(self, value): return value + + class NonAGen3: + def aclose(self): pass + def asend(self, value): return value + def athrow(self, typ, val=None, tb=None): pass + + non_samples = [ + None, 42, 3.14, 1j, b"", "", (), [], {}, set(), + iter(()), iter([]), NonAGen1(), NonAGen2(), NonAGen3()] + for x in non_samples: + self.assertNotIsInstance(x, AsyncGenerator) + self.assertFalse(issubclass(type(x), AsyncGenerator), repr(type(x))) + + class Gen: + def __aiter__(self): return self + async def __anext__(self): return None + async def aclose(self): pass + async def asend(self, value): return value + async def athrow(self, typ, val=None, tb=None): pass + + class MinimalAGen(AsyncGenerator): + async def asend(self, value): + return value + async def athrow(self, typ, val=None, tb=None): + await super().athrow(typ, val, tb) + + async def gen(): + yield 1 + + samples = [gen(), Gen(), MinimalAGen()] + for x in samples: + self.assertIsInstance(x, AsyncIterator) + self.assertIsInstance(x, AsyncGenerator) + self.assertTrue(issubclass(type(x), AsyncGenerator), repr(type(x))) + self.validate_abstract_methods(AsyncGenerator, 'asend', 'athrow') + + def run_async(coro): + result = None + while True: + try: + coro.send(None) + except StopIteration as ex: + result = ex.args[0] if ex.args else None + break + return result + + # mixin tests + mgen = MinimalAGen() + self.assertIs(mgen, mgen.__aiter__()) + self.assertIs(run_async(mgen.asend(None)), run_async(mgen.__anext__())) + self.assertEqual(2, run_async(mgen.asend(2))) + self.assertIsNone(run_async(mgen.aclose())) + with self.assertRaises(ValueError): + run_async(mgen.athrow(ValueError)) + + class FailOnClose(AsyncGenerator): + async def asend(self, value): return value + async def athrow(self, *args): raise ValueError + + with self.assertRaises(ValueError): + run_async(FailOnClose().aclose()) + + class IgnoreGeneratorExit(AsyncGenerator): + async def asend(self, value): return value + async def athrow(self, *args): pass + + with self.assertRaises(RuntimeError): + run_async(IgnoreGeneratorExit().aclose()) + def test_Sized(self): non_samples = [None, 42, 3.14, 1j, _test_gen(), diff --git a/Misc/NEWS b/Misc/NEWS index 68b99074eee..558366f4a61 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -69,6 +69,8 @@ Library - Issue #28704: Fix create_unix_server to support Path-like objects (PEP 519). +- Issue #28720: Add collections.abc.AsyncGenerator. + Documentation -------------