[Python-checkins] cpython: Issue #25609: Introduce contextlib.AbstractContextManager and

brett.cannon python-checkins at python.org
Fri Apr 8 15:16:27 EDT 2016


https://hg.python.org/cpython/rev/841a263c0c56
changeset:   100872:841a263c0c56
user:        Brett Cannon <brett at python.org>
date:        Fri Apr 08 12:15:27 2016 -0700
summary:
  Issue #25609: Introduce contextlib.AbstractContextManager and
typing.ContextManager.

files:
  Doc/library/contextlib.rst  |  16 ++++++++-
  Doc/library/typing.rst      |  12 +++++-
  Doc/whatsnew/3.6.rst        |  26 +++++++++++++-
  Lib/contextlib.py           |  42 ++++++++++++++++++------
  Lib/test/test_contextlib.py |  33 +++++++++++++++++++
  Lib/test/test_typing.py     |  18 ++++++++++
  Lib/typing.py               |   7 ++++
  Misc/NEWS                   |   3 +
  8 files changed, 138 insertions(+), 19 deletions(-)


diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst
--- a/Doc/library/contextlib.rst
+++ b/Doc/library/contextlib.rst
@@ -18,6 +18,18 @@
 
 Functions and classes provided:
 
+.. class:: AbstractContextManager
+
+   An abstract base class for classes that implement
+   :meth:`object.__enter__` and :meth:`object.__exit__`. A default
+   implementation for :meth:`object.__enter__` is provided which returns
+   ``self`` while :meth:`object.__exit__` is an abstract method which by default
+   returns ``None``. See also the definition of :ref:`typecontextmanager`.
+
+   .. versionadded:: 3.6
+
+
+
 .. decorator:: contextmanager
 
    This function is a :term:`decorator` that can be used to define a factory
@@ -447,9 +459,9 @@
 acquisition and release functions, along with an optional validation function,
 and maps them to the context management protocol::
 
-   from contextlib import contextmanager, ExitStack
+   from contextlib import contextmanager, AbstractContextManager, ExitStack
 
-   class ResourceManager:
+   class ResourceManager(AbstractContextManager):
 
        def __init__(self, acquire_resource, release_resource, check_resource_ok=None):
            self.acquire_resource = acquire_resource
diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -345,15 +345,15 @@
 
 .. class:: Iterable(Generic[T_co])
 
-    A generic version of the :class:`collections.abc.Iterable`.
+    A generic version of :class:`collections.abc.Iterable`.
 
 .. class:: Iterator(Iterable[T_co])
 
-    A generic version of the :class:`collections.abc.Iterator`.
+    A generic version of :class:`collections.abc.Iterator`.
 
 .. class:: Reversible(Iterable[T_co])
 
-    A generic version of the :class:`collections.abc.Reversible`.
+    A generic version of :class:`collections.abc.Reversible`.
 
 .. class:: SupportsInt
 
@@ -448,6 +448,12 @@
 
    A generic version of :class:`collections.abc.ValuesView`.
 
+.. class:: ContextManager(Generic[T_co])
+
+   A generic version of :class:`contextlib.AbstractContextManager`.
+
+   .. versionadded:: 3.6
+
 .. class:: Dict(dict, MutableMapping[KT, VT])
 
    A generic version of :class:`dict`.
diff --git a/Doc/whatsnew/3.6.rst b/Doc/whatsnew/3.6.rst
--- a/Doc/whatsnew/3.6.rst
+++ b/Doc/whatsnew/3.6.rst
@@ -190,6 +190,18 @@
 Improved Modules
 ================
 
+contextlib
+----------
+
+The :class:`contextlib.AbstractContextManager` class has been added to
+provide an abstract base class for context managers. It provides a
+sensible default implementation for `__enter__()` which returns
+`self` and leaves `__exit__()` an abstract method. A matching
+class has been added to the :mod:`typing` module as
+:class:`typing.ContextManager`.
+(Contributed by Brett Cannon in :issue:`25609`.)
+
+
 datetime
 --------
 
@@ -246,6 +258,14 @@
 Stéphane Wirtel in :issue:`25485`).
 
 
+typing
+------
+
+The :class:`typing.ContextManager` class has been added for
+representing :class:`contextlib.AbstractContextManager`.
+(Contributed by Brett Cannon in :issue:`25609`.)
+
+
 unittest.mock
 -------------
 
@@ -372,9 +392,9 @@
 Deprecated Python modules, functions and methods
 ------------------------------------------------
 
-* :meth:`importlib.machinery.SourceFileLoader` and
-  :meth:`importlib.machinery.SourcelessFileLoader` are now deprecated. They
-  were the only remaining implementations of
+* :meth:`importlib.machinery.SourceFileLoader.load_module` and
+  :meth:`importlib.machinery.SourcelessFileLoader.load_module` are now
+  deprecated. They were the only remaining implementations of
   :meth:`importlib.abc.Loader.load_module` in :mod:`importlib` that had not
   been deprecated in previous versions of Python in favour of
   :meth:`importlib.abc.Loader.exec_module`.
diff --git a/Lib/contextlib.py b/Lib/contextlib.py
--- a/Lib/contextlib.py
+++ b/Lib/contextlib.py
@@ -1,11 +1,34 @@
 """Utilities for with-statement contexts.  See PEP 343."""
-
+import abc
 import sys
 from collections import deque
 from functools import wraps
 
-__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
-           "redirect_stdout", "redirect_stderr", "suppress"]
+__all__ = ["contextmanager", "closing", "AbstractContextManager",
+           "ContextDecorator", "ExitStack", "redirect_stdout",
+           "redirect_stderr", "suppress"]
+
+
+class AbstractContextManager(abc.ABC):
+
+    """An abstract base class for context managers."""
+
+    def __enter__(self):
+        """Return `self` upon entering the runtime context."""
+        return self
+
+    @abc.abstractmethod
+    def __exit__(self, exc_type, exc_value, traceback):
+        """Raise any exception triggered within the runtime context."""
+        return None
+
+    @classmethod
+    def __subclasshook__(cls, C):
+        if cls is AbstractContextManager:
+            if (any("__enter__" in B.__dict__ for B in C.__mro__) and
+                any("__exit__" in B.__dict__ for B in C.__mro__)):
+                    return True
+        return NotImplemented
 
 
 class ContextDecorator(object):
@@ -31,7 +54,7 @@
         return inner
 
 
-class _GeneratorContextManager(ContextDecorator):
+class _GeneratorContextManager(ContextDecorator, AbstractContextManager):
     """Helper for @contextmanager decorator."""
 
     def __init__(self, func, args, kwds):
@@ -134,7 +157,7 @@
     return helper
 
 
-class closing(object):
+class closing(AbstractContextManager):
     """Context to automatically close something at the end of a block.
 
     Code like this:
@@ -159,7 +182,7 @@
         self.thing.close()
 
 
-class _RedirectStream:
+class _RedirectStream(AbstractContextManager):
 
     _stream = None
 
@@ -199,7 +222,7 @@
     _stream = "stderr"
 
 
-class suppress:
+class suppress(AbstractContextManager):
     """Context manager to suppress specified exceptions
 
     After the exception is suppressed, execution proceeds with the next
@@ -230,7 +253,7 @@
 
 
 # Inspired by discussions on http://bugs.python.org/issue13585
-class ExitStack(object):
+class ExitStack(AbstractContextManager):
     """Context manager for dynamic management of a stack of exit callbacks
 
     For example:
@@ -309,9 +332,6 @@
         """Immediately unwind the context stack"""
         self.__exit__(None, None, None)
 
-    def __enter__(self):
-        return self
-
     def __exit__(self, *exc_details):
         received_exc = exc_details[0] is not None
 
diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py
--- a/Lib/test/test_contextlib.py
+++ b/Lib/test/test_contextlib.py
@@ -12,6 +12,39 @@
     threading = None
 
 
+class TestAbstractContextManager(unittest.TestCase):
+
+    def test_enter(self):
+        class DefaultEnter(AbstractContextManager):
+            def __exit__(self, *args):
+                super().__exit__(*args)
+
+        manager = DefaultEnter()
+        self.assertIs(manager.__enter__(), manager)
+
+    def test_exit_is_abstract(self):
+        class MissingExit(AbstractContextManager):
+            pass
+
+        with self.assertRaises(TypeError):
+            MissingExit()
+
+    def test_structural_subclassing(self):
+        class ManagerFromScratch:
+            def __enter__(self):
+                return self
+            def __exit__(self, exc_type, exc_value, traceback):
+                return None
+
+        self.assertTrue(issubclass(ManagerFromScratch, AbstractContextManager))
+
+        class DefaultEnter(AbstractContextManager):
+            def __exit__(self, *args):
+                super().__exit__(*args)
+
+        self.assertTrue(issubclass(DefaultEnter, AbstractContextManager))
+
+
 class ContextManagerTestCase(unittest.TestCase):
 
     def test_contextmanager_plain(self):
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -1,3 +1,4 @@
+import contextlib
 import pickle
 import re
 import sys
@@ -1309,6 +1310,21 @@
         assert len(MMB[KT, VT]()) == 0
 
 
+class OtherABCTests(TestCase):
+
+    @skipUnless(hasattr(typing, 'ContextManager'),
+                'requires typing.ContextManager')
+    def test_contextmanager(self):
+        @contextlib.contextmanager
+        def manager():
+            yield 42
+
+        cm = manager()
+        assert isinstance(cm, typing.ContextManager)
+        assert isinstance(cm, typing.ContextManager[int])
+        assert not isinstance(42, typing.ContextManager)
+
+
 class NamedTupleTests(TestCase):
 
     def test_basics(self):
@@ -1447,6 +1463,8 @@
         assert 'ValuesView' in a
         assert 'cast' in a
         assert 'overload' in a
+        if hasattr(contextlib, 'AbstractContextManager'):
+            assert 'ContextManager' in a
         # Check that io and re are not exported.
         assert 'io' not in a
         assert 're' not in a
diff --git a/Lib/typing.py b/Lib/typing.py
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -1,6 +1,7 @@
 import abc
 from abc import abstractmethod, abstractproperty
 import collections
+import contextlib
 import functools
 import re as stdlib_re  # Avoid confusion with the re we export.
 import sys
@@ -1530,6 +1531,12 @@
     pass
 
 
+if hasattr(contextlib, 'AbstractContextManager'):
+    class ContextManager(Generic[T_co], extra=contextlib.AbstractContextManager):
+        __slots__ = ()
+    __all__.append('ContextManager')
+
+
 class Dict(dict, MutableMapping[KT, VT]):
 
     def __new__(cls, *args, **kwds):
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -237,6 +237,9 @@
 Library
 -------
 
+- Issue #25609: Introduce contextlib.AbstractContextManager and
+  typing.ContextManager.
+
 - Issue #26709: Fixed Y2038 problem in loading binary PLists.
 
 - Issue #23735: Handle terminal resizing with Readline 6.3+ by installing our

-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list