Issue #24018: Add a collections.Generator abstract base class.

This commit is contained in:
Raymond Hettinger 2015-05-09 01:07:23 -04:00
parent dae2ef1cfa
commit bd60e8dece
4 changed files with 145 additions and 2 deletions

View 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

View 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__ = ()

View 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))(),

View File

@ -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).