]> granicus.if.org Git - python/commitdiff
Issue #24018: Add a collections.Generator abstract base class.
authorRaymond Hettinger <python@rcn.com>
Sat, 9 May 2015 05:07:23 +0000 (01:07 -0400)
committerRaymond Hettinger <python@rcn.com>
Sat, 9 May 2015 05:07:23 +0000 (01:07 -0400)
Doc/library/collections.abc.rst
Lib/_collections_abc.py
Lib/test/test_collections.py
Misc/NEWS

index 99c43113e3d0138414dab8503c4a71aae40cd3db..8a71259631fd1eb22129c987a882fc584f50d049 100644 (file)
@@ -40,6 +40,7 @@ ABC                        Inherits from          Abstract Methods        Mixin
 :class:`Hashable`                                 ``__hash__``
 :class:`Iterable`                                 ``__iter__``
 :class:`Iterator`          :class:`Iterable`      ``__next__``            ``__iter__``
+:class:`Generator`         :class:`Iterator`      ``send``, ``throw``     ``close``, ``__iter__``, ``__next__``
 :class:`Sized`                                    ``__len__``
 :class:`Callable`                                 ``__call__``
 
@@ -102,6 +103,15 @@ ABC                        Inherits from          Abstract Methods        Mixin
    :meth:`~iterator.__next__` methods.  See also the definition of
    :term:`iterator`.
 
+.. class:: Generator
+
+   ABC for generator classes that implement the protocol defined in
+   :pep:`342` that extends iterators with the :meth:`~generator.send`,
+   :meth:`~generator.throw` and :meth:`~generator.close` methods.
+   See also the definition of :term:`generator`.
+
+   .. versionadded:: 3.5
+
 .. class:: Sequence
            MutableSequence
 
index 3d3f07b92ef32636cb3067d60cad2fa4af249055..cb87e6b730d0a7f7beaa8aedaa379e1538814a1e 100644 (file)
@@ -9,7 +9,7 @@ Unit tests are in test_collections.
 from abc import ABCMeta, abstractmethod
 import sys
 
-__all__ = ["Hashable", "Iterable", "Iterator",
+__all__ = ["Hashable", "Iterable", "Iterator", "Generator",
            "Sized", "Container", "Callable",
            "Set", "MutableSet",
            "Mapping", "MutableMapping",
@@ -50,6 +50,7 @@ dict_values = type({}.values())
 dict_items = type({}.items())
 ## misc ##
 mappingproxy = type(type.__dict__)
+generator = type((lambda: (yield))())
 
 
 ### ONE-TRICK PONIES ###
@@ -124,6 +125,64 @@ Iterator.register(str_iterator)
 Iterator.register(tuple_iterator)
 Iterator.register(zip_iterator)
 
+
+class Generator(Iterator):
+
+    __slots__ = ()
+
+    def __next__(self):
+        """Return the next item from the generator.
+        When exhausted, raise StopIteration.
+        """
+        return self.send(None)
+
+    @abstractmethod
+    def send(self, value):
+        """Send a value into the generator.
+        Return next yielded value or raise StopIteration.
+        """
+        raise StopIteration
+
+    @abstractmethod
+    def throw(self, typ, val=None, tb=None):
+        """Raise an exception in the generator.
+        Return next yielded value or raise StopIteration.
+        """
+        if val is None:
+            if tb is None:
+                raise typ
+            val = typ()
+        if tb is not None:
+            val = val.with_traceback(tb)
+        raise val
+
+    def close(self):
+        """Raise GeneratorExit inside generator.
+        """
+        try:
+            self.throw(GeneratorExit)
+        except (GeneratorExit, StopIteration):
+            pass
+        else:
+            raise RuntimeError("generator ignored GeneratorExit")
+
+    @classmethod
+    def __subclasshook__(cls, C):
+        if cls is Generator:
+            mro = C.__mro__
+            for method in ('__iter__', '__next__', 'send', 'throw', 'close'):
+                for base in mro:
+                    if method in base.__dict__:
+                        break
+                else:
+                    return NotImplemented
+            return True
+        return NotImplemented
+
+
+Generator.register(generator)
+
+
 class Sized(metaclass=ABCMeta):
 
     __slots__ = ()
index 958fb6282453fe1adb8c809a11d6c033ea06c11d..5b2e81fd0c95afb31f78bb3acba206534639c16c 100644 (file)
@@ -14,7 +14,7 @@ import sys
 from collections import UserDict
 from collections import ChainMap
 from collections import deque
-from collections.abc import Hashable, Iterable, Iterator
+from collections.abc import Hashable, Iterable, Iterator, Generator
 from collections.abc import Sized, Container, Callable
 from collections.abc import Set, MutableSet
 from collections.abc import Mapping, MutableMapping, KeysView, ItemsView
@@ -522,6 +522,77 @@ class TestOneTrickPonyABCs(ABCTestCase):
                 return
         self.assertNotIsInstance(NextOnly(), Iterator)
 
+    def test_Generator(self):
+        class NonGen1:
+            def __iter__(self): return self
+            def __next__(self): return None
+            def close(self): pass
+            def throw(self, typ, val=None, tb=None): pass
+
+        class NonGen2:
+            def __iter__(self): return self
+            def __next__(self): return None
+            def close(self): pass
+            def send(self, value): return value
+
+        class NonGen3:
+            def close(self): pass
+            def send(self, value): return value
+            def throw(self, typ, val=None, tb=None): pass
+
+        non_samples = [
+            None, 42, 3.14, 1j, b"", "", (), [], {}, set(),
+            iter(()), iter([]), NonGen1(), NonGen2(), NonGen3()]
+        for x in non_samples:
+            self.assertNotIsInstance(x, Generator)
+            self.assertFalse(issubclass(type(x), Generator), repr(type(x)))
+
+        class Gen:
+            def __iter__(self): return self
+            def __next__(self): return None
+            def close(self): pass
+            def send(self, value): return value
+            def throw(self, typ, val=None, tb=None): pass
+
+        class MinimalGen(Generator):
+            def send(self, value):
+                return value
+            def throw(self, typ, val=None, tb=None):
+                super().throw(typ, val, tb)
+
+        def gen():
+            yield 1
+
+        samples = [gen(), (lambda: (yield))(), Gen(), MinimalGen()]
+        for x in samples:
+            self.assertIsInstance(x, Iterator)
+            self.assertIsInstance(x, Generator)
+            self.assertTrue(issubclass(type(x), Generator), repr(type(x)))
+        self.validate_abstract_methods(Generator, 'send', 'throw')
+
+        # mixin tests
+        mgen = MinimalGen()
+        self.assertIs(mgen, iter(mgen))
+        self.assertIs(mgen.send(None), next(mgen))
+        self.assertEqual(2, mgen.send(2))
+        self.assertIsNone(mgen.close())
+        self.assertRaises(ValueError, mgen.throw, ValueError)
+        self.assertRaisesRegex(ValueError, "^huhu$",
+                               mgen.throw, ValueError, ValueError("huhu"))
+        self.assertRaises(StopIteration, mgen.throw, StopIteration())
+
+        class FailOnClose(Generator):
+            def send(self, value): return value
+            def throw(self, *args): raise ValueError
+
+        self.assertRaises(ValueError, FailOnClose().close)
+
+        class IgnoreGeneratorExit(Generator):
+            def send(self, value): return value
+            def throw(self, *args): pass
+
+        self.assertRaises(RuntimeError, IgnoreGeneratorExit().close)
+
     def test_Sized(self):
         non_samples = [None, 42, 3.14, 1j,
                        (lambda: (yield))(),
index 13dd8c30bcbb7cbaf94b860379131bcc89da9987..ac065f73085d248e9e4bfc1f291c5ce4f2e2ed3f 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -39,6 +39,9 @@ Library
 - Issue #24134: assertRaises(), assertRaisesRegex(), assertWarns() and
   assertWarnsRegex() checks are not longer successful if the callable is None.
 
+- Issue #24018: Add a collections.Generator abstract base class.
+  Contributed by Stefan Behnel.
+
 - Issue #23880: Tkinter's getint() and getdouble() now support Tcl_Obj.
   Tkinter's getdouble() now supports any numbers (in particular int).