From 9e080e0e741dd70cf86500f848eee19cf8b29efa Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 8 Apr 2016 12:15:27 -0700 Subject: Issue #25609: Introduce contextlib.AbstractContextManager and typing.ContextManager. --- 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 index c112241..7876e7a 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -18,6 +18,18 @@ Utilities 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 @@ Here's an example of doing this for a context manager that accepts resource 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 index 1bd4b09..f609a51 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -345,15 +345,15 @@ The module defines the following classes, functions and decorators: .. 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 @@ The module defines the following classes, functions and decorators: 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 index 6799f69..555814e 100644 --- a/Doc/whatsnew/3.6.rst +++ b/Doc/whatsnew/3.6.rst @@ -190,6 +190,18 @@ New Modules 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 @@ telnetlib 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 @@ become proper keywords in Python 3.7. 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 index 5377987..98903bc 100644 --- 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 @@ class ContextDecorator(object): return inner -class _GeneratorContextManager(ContextDecorator): +class _GeneratorContextManager(ContextDecorator, AbstractContextManager): """Helper for @contextmanager decorator.""" def __init__(self, func, args, kwds): @@ -134,7 +157,7 @@ def contextmanager(func): 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 @@ class closing(object): self.thing.close() -class _RedirectStream: +class _RedirectStream(AbstractContextManager): _stream = None @@ -199,7 +222,7 @@ class redirect_stderr(_RedirectStream): _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 @@ class suppress: # 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 @@ class ExitStack(object): """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 index 04fc875..5c8bc98 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -12,6 +12,39 @@ except ImportError: 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 index f1c6e12..a360824 100644 --- 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 @@ class CollectionsAbcTests(TestCase): 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 @@ class AllTests(TestCase): 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 index 6ead3c4..42a9ea3 100644 --- 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 @@ class ValuesView(MappingView[VT_co], extra=collections_abc.ValuesView): 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 index 9970636..56b1501 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -237,6 +237,9 @@ Core and Builtins 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 -- cgit v0.12