Skip to content

Commit 9e080e0

Browse files
committed
Issue #25609: Introduce contextlib.AbstractContextManager and
typing.ContextManager.
1 parent c5b5ba9 commit 9e080e0

8 files changed

Lines changed: 138 additions & 19 deletions

File tree

Doc/library/contextlib.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ Utilities
1818

1919
Functions and classes provided:
2020

21+
.. class:: AbstractContextManager
22+
23+
An abstract base class for classes that implement
24+
:meth:`object.__enter__` and :meth:`object.__exit__`. A default
25+
implementation for :meth:`object.__enter__` is provided which returns
26+
``self`` while :meth:`object.__exit__` is an abstract method which by default
27+
returns ``None``. See also the definition of :ref:`typecontextmanager`.
28+
29+
.. versionadded:: 3.6
30+
31+
32+
2133
.. decorator:: contextmanager
2234

2335
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
447459
acquisition and release functions, along with an optional validation function,
448460
and maps them to the context management protocol::
449461

450-
from contextlib import contextmanager, ExitStack
462+
from contextlib import contextmanager, AbstractContextManager, ExitStack
451463

452-
class ResourceManager:
464+
class ResourceManager(AbstractContextManager):
453465

454466
def __init__(self, acquire_resource, release_resource, check_resource_ok=None):
455467
self.acquire_resource = acquire_resource

Doc/library/typing.rst

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,15 +345,15 @@ The module defines the following classes, functions and decorators:
345345

346346
.. class:: Iterable(Generic[T_co])
347347

348-
A generic version of the :class:`collections.abc.Iterable`.
348+
A generic version of :class:`collections.abc.Iterable`.
349349

350350
.. class:: Iterator(Iterable[T_co])
351351

352-
A generic version of the :class:`collections.abc.Iterator`.
352+
A generic version of :class:`collections.abc.Iterator`.
353353

354354
.. class:: Reversible(Iterable[T_co])
355355

356-
A generic version of the :class:`collections.abc.Reversible`.
356+
A generic version of :class:`collections.abc.Reversible`.
357357

358358
.. class:: SupportsInt
359359

@@ -448,6 +448,12 @@ The module defines the following classes, functions and decorators:
448448

449449
A generic version of :class:`collections.abc.ValuesView`.
450450

451+
.. class:: ContextManager(Generic[T_co])
452+
453+
A generic version of :class:`contextlib.AbstractContextManager`.
454+
455+
.. versionadded:: 3.6
456+
451457
.. class:: Dict(dict, MutableMapping[KT, VT])
452458

453459
A generic version of :class:`dict`.

Doc/whatsnew/3.6.rst

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,18 @@ New Modules
190190
Improved Modules
191191
================
192192

193+
contextlib
194+
----------
195+
196+
The :class:`contextlib.AbstractContextManager` class has been added to
197+
provide an abstract base class for context managers. It provides a
198+
sensible default implementation for `__enter__()` which returns
199+
`self` and leaves `__exit__()` an abstract method. A matching
200+
class has been added to the :mod:`typing` module as
201+
:class:`typing.ContextManager`.
202+
(Contributed by Brett Cannon in :issue:`25609`.)
203+
204+
193205
datetime
194206
--------
195207

@@ -246,6 +258,14 @@ telnetlib
246258
Stéphane Wirtel in :issue:`25485`).
247259

248260

261+
typing
262+
------
263+
264+
The :class:`typing.ContextManager` class has been added for
265+
representing :class:`contextlib.AbstractContextManager`.
266+
(Contributed by Brett Cannon in :issue:`25609`.)
267+
268+
249269
unittest.mock
250270
-------------
251271

@@ -372,9 +392,9 @@ become proper keywords in Python 3.7.
372392
Deprecated Python modules, functions and methods
373393
------------------------------------------------
374394

375-
* :meth:`importlib.machinery.SourceFileLoader` and
376-
:meth:`importlib.machinery.SourcelessFileLoader` are now deprecated. They
377-
were the only remaining implementations of
395+
* :meth:`importlib.machinery.SourceFileLoader.load_module` and
396+
:meth:`importlib.machinery.SourcelessFileLoader.load_module` are now
397+
deprecated. They were the only remaining implementations of
378398
:meth:`importlib.abc.Loader.load_module` in :mod:`importlib` that had not
379399
been deprecated in previous versions of Python in favour of
380400
:meth:`importlib.abc.Loader.exec_module`.

Lib/contextlib.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
11
"""Utilities for with-statement contexts. See PEP 343."""
2-
2+
import abc
33
import sys
44
from collections import deque
55
from functools import wraps
66

7-
__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
8-
"redirect_stdout", "redirect_stderr", "suppress"]
7+
__all__ = ["contextmanager", "closing", "AbstractContextManager",
8+
"ContextDecorator", "ExitStack", "redirect_stdout",
9+
"redirect_stderr", "suppress"]
10+
11+
12+
class AbstractContextManager(abc.ABC):
13+
14+
"""An abstract base class for context managers."""
15+
16+
def __enter__(self):
17+
"""Return `self` upon entering the runtime context."""
18+
return self
19+
20+
@abc.abstractmethod
21+
def __exit__(self, exc_type, exc_value, traceback):
22+
"""Raise any exception triggered within the runtime context."""
23+
return None
24+
25+
@classmethod
26+
def __subclasshook__(cls, C):
27+
if cls is AbstractContextManager:
28+
if (any("__enter__" in B.__dict__ for B in C.__mro__) and
29+
any("__exit__" in B.__dict__ for B in C.__mro__)):
30+
return True
31+
return NotImplemented
932

1033

1134
class ContextDecorator(object):
@@ -31,7 +54,7 @@ def inner(*args, **kwds):
3154
return inner
3255

3356

34-
class _GeneratorContextManager(ContextDecorator):
57+
class _GeneratorContextManager(ContextDecorator, AbstractContextManager):
3558
"""Helper for @contextmanager decorator."""
3659

3760
def __init__(self, func, args, kwds):
@@ -134,7 +157,7 @@ def helper(*args, **kwds):
134157
return helper
135158

136159

137-
class closing(object):
160+
class closing(AbstractContextManager):
138161
"""Context to automatically close something at the end of a block.
139162
140163
Code like this:
@@ -159,7 +182,7 @@ def __exit__(self, *exc_info):
159182
self.thing.close()
160183

161184

162-
class _RedirectStream:
185+
class _RedirectStream(AbstractContextManager):
163186

164187
_stream = None
165188

@@ -199,7 +222,7 @@ class redirect_stderr(_RedirectStream):
199222
_stream = "stderr"
200223

201224

202-
class suppress:
225+
class suppress(AbstractContextManager):
203226
"""Context manager to suppress specified exceptions
204227
205228
After the exception is suppressed, execution proceeds with the next
@@ -230,7 +253,7 @@ def __exit__(self, exctype, excinst, exctb):
230253

231254

232255
# Inspired by discussions on http://bugs.python.org/issue13585
233-
class ExitStack(object):
256+
class ExitStack(AbstractContextManager):
234257
"""Context manager for dynamic management of a stack of exit callbacks
235258
236259
For example:
@@ -309,9 +332,6 @@ def close(self):
309332
"""Immediately unwind the context stack"""
310333
self.__exit__(None, None, None)
311334

312-
def __enter__(self):
313-
return self
314-
315335
def __exit__(self, *exc_details):
316336
received_exc = exc_details[0] is not None
317337

Lib/test/test_contextlib.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,39 @@
1212
threading = None
1313

1414

15+
class TestAbstractContextManager(unittest.TestCase):
16+
17+
def test_enter(self):
18+
class DefaultEnter(AbstractContextManager):
19+
def __exit__(self, *args):
20+
super().__exit__(*args)
21+
22+
manager = DefaultEnter()
23+
self.assertIs(manager.__enter__(), manager)
24+
25+
def test_exit_is_abstract(self):
26+
class MissingExit(AbstractContextManager):
27+
pass
28+
29+
with self.assertRaises(TypeError):
30+
MissingExit()
31+
32+
def test_structural_subclassing(self):
33+
class ManagerFromScratch:
34+
def __enter__(self):
35+
return self
36+
def __exit__(self, exc_type, exc_value, traceback):
37+
return None
38+
39+
self.assertTrue(issubclass(ManagerFromScratch, AbstractContextManager))
40+
41+
class DefaultEnter(AbstractContextManager):
42+
def __exit__(self, *args):
43+
super().__exit__(*args)
44+
45+
self.assertTrue(issubclass(DefaultEnter, AbstractContextManager))
46+
47+
1548
class ContextManagerTestCase(unittest.TestCase):
1649

1750
def test_contextmanager_plain(self):

Lib/test/test_typing.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import pickle
23
import re
34
import sys
@@ -1309,6 +1310,21 @@ def __len__(self):
13091310
assert len(MMB[KT, VT]()) == 0
13101311

13111312

1313+
class OtherABCTests(TestCase):
1314+
1315+
@skipUnless(hasattr(typing, 'ContextManager'),
1316+
'requires typing.ContextManager')
1317+
def test_contextmanager(self):
1318+
@contextlib.contextmanager
1319+
def manager():
1320+
yield 42
1321+
1322+
cm = manager()
1323+
assert isinstance(cm, typing.ContextManager)
1324+
assert isinstance(cm, typing.ContextManager[int])
1325+
assert not isinstance(42, typing.ContextManager)
1326+
1327+
13121328
class NamedTupleTests(TestCase):
13131329

13141330
def test_basics(self):
@@ -1447,6 +1463,8 @@ def test_all(self):
14471463
assert 'ValuesView' in a
14481464
assert 'cast' in a
14491465
assert 'overload' in a
1466+
if hasattr(contextlib, 'AbstractContextManager'):
1467+
assert 'ContextManager' in a
14501468
# Check that io and re are not exported.
14511469
assert 'io' not in a
14521470
assert 're' not in a

Lib/typing.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import abc
22
from abc import abstractmethod, abstractproperty
33
import collections
4+
import contextlib
45
import functools
56
import re as stdlib_re # Avoid confusion with the re we export.
67
import sys
@@ -1530,6 +1531,12 @@ class ValuesView(MappingView[VT_co], extra=collections_abc.ValuesView):
15301531
pass
15311532

15321533

1534+
if hasattr(contextlib, 'AbstractContextManager'):
1535+
class ContextManager(Generic[T_co], extra=contextlib.AbstractContextManager):
1536+
__slots__ = ()
1537+
__all__.append('ContextManager')
1538+
1539+
15331540
class Dict(dict, MutableMapping[KT, VT]):
15341541

15351542
def __new__(cls, *args, **kwds):

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ Core and Builtins
237237
Library
238238
-------
239239

240+
- Issue #25609: Introduce contextlib.AbstractContextManager and
241+
typing.ContextManager.
242+
240243
- Issue #26709: Fixed Y2038 problem in loading binary PLists.
241244

242245
- Issue #23735: Handle terminal resizing with Readline 6.3+ by installing our

0 commit comments

Comments
 (0)