]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
Issue #28720: Add collections.abc.AsyncGenerator.
authorYury Selivanov <yury@magic.io>
Wed, 16 Nov 2016 23:25:04 +0000 (18:25 -0500)
committerYury Selivanov <yury@magic.io>
Wed, 16 Nov 2016 23:25:04 +0000 (18:25 -0500)
Doc/library/collections.abc.rst
Doc/whatsnew/3.6.rst
Lib/_collections_abc.py
Lib/test/test_collections.py
Misc/NEWS

index e4b75a0f0a0282a6312eed36ca4a6da8a77a4ba2..3ac49db78d7dc1705129b661c27c620b8884c61b 100644 (file)
@@ -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::
index 801191c9b412d68bba4d13b60652650f18aab334..ce8ab98abd1f8a36a37f7bd3308791ff60f265fa 100644 (file)
@@ -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.
index ee7d0f18f81c8f5ed3b529ff6a17dc7359cfd74f..b172f3f360e6ef5c98b69c187caa33a28b1f56fb 100644 (file)
@@ -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__ = ()
index 52ff256eb9438b526447282bd8b364e314ab39a5..87454cc6704cb0725a8a4ee5d507dc0c2f573ab0 100644 (file)
@@ -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(),
index 68b99074eeef9388ccf49d036cb64949b1d6d5ca..558366f4a611b0fda2da4bc505f3beb067b2e95a 100644 (file)
--- 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
 -------------