Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Doc/library/contextlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,35 @@ Functions and classes provided:
Use of :class:`ContextDecorator`.


.. decorator:: asynccontextmanager

Similar to :func:`~contextlib.contextmanager`, but creates an
:ref:`asynchronous context manager <async-context-managers>`.

This function is a :term:`decorator` that can be used to define a factory
function for :keyword:`async with` statement asynchronous context managers,
without needing to create a class or separate :meth:`__aenter__` and
:meth:`__aexit__` methods.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add that the decorator expects to be applied to asynchronous generator functions.


A simple example::

from contextlib import asynccontextmanager

@asynccontextmanager
async def get_connection():
conn = await acquire_db_connection()
try:
yield
finally:
await release_db_connection(conn)

async def get_all_users():
async with get_connection() as conn:
return conn.query('SELECT ...')

.. versionadded:: 3.7


.. function:: closing(thing)

Return a context manager that closes *thing* upon completion of the block. This
Expand Down
2 changes: 2 additions & 0 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2566,6 +2566,8 @@ An example of an asynchronous iterable object::
result in a :exc:`RuntimeError`.


.. _async-context-managers:

Asynchronous Context Managers
-----------------------------

Expand Down
99 changes: 93 additions & 6 deletions Lib/contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from collections import deque
from functools import wraps

__all__ = ["contextmanager", "closing", "AbstractContextManager",
"ContextDecorator", "ExitStack", "redirect_stdout",
"redirect_stderr", "suppress"]
__all__ = ["asynccontextmanager", "contextmanager", "closing",
"AbstractContextManager", "ContextDecorator", "ExitStack",
"redirect_stdout", "redirect_stderr", "suppress"]


class AbstractContextManager(abc.ABC):
Expand Down Expand Up @@ -54,8 +54,8 @@ def inner(*args, **kwds):
return inner


class _GeneratorContextManager(ContextDecorator, AbstractContextManager):
"""Helper for @contextmanager decorator."""
class _GeneratorContextManagerBase:
"""Shared functionality for @contextmanager and @asynccontextmanager."""

def __init__(self, func, args, kwds):
self.gen = func(*args, **kwds)
Expand All @@ -71,6 +71,12 @@ def __init__(self, func, args, kwds):
# for the class instead.
# See http://bugs.python.org/issue19404 for more details.


class _GeneratorContextManager(_GeneratorContextManagerBase,
AbstractContextManager,
ContextDecorator):
"""Helper for @contextmanager decorator."""

def _recreate_cm(self):
# _GCM instances are one-shot context managers, so the
# CM must be recreated each time a decorated function is
Expand Down Expand Up @@ -122,10 +128,59 @@ def __exit__(self, type, value, traceback):
# fixes the impedance mismatch between the throw() protocol
# and the __exit__() protocol.
#
# This cannot use 'except BaseException as exc' (as in the
# async implementation) to maintain compatibility with
# Python 2, where string exceptions are not caught by
# 'except BaseException'.
Copy link
Contributor

@ncoghlan ncoghlan Mar 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem in 2.x is old style classes rather than strings:

>>> raise "Exception"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: exceptions must be old-style classes or derived from BaseException, not str
>>> class LegacyException:
...     pass
... 
>>> raise LegacyException
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.LegacyException: <__main__.LegacyException instance at 0x7f1208568c68>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, I guess string exceptions are already gone by 2.7.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They were completely gone even in 2.6 :)

if sys.exc_info()[1] is not value:
raise


class _AsyncGeneratorContextManager(_GeneratorContextManagerBase):
"""Helper for @asynccontextmanager."""

async def __aenter__(self):
try:
return await self.gen.__anext__()
except StopAsyncIteration:
raise RuntimeError("generator didn't yield") from None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diff coverage shows a missing test case for this line.


async def __aexit__(self, typ, value, traceback):
if typ is None:
try:
await self.gen.__anext__()
except StopAsyncIteration:
return
else:
raise RuntimeError("generator didn't stop")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test case here as well.

else:
if value is None:
value = typ()
# See _GeneratorContextManager.__exit__ for comments on subtleties
# in this implementation
try:
await self.gen.athrow(typ, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopAsyncIteration as exc:
return exc is not value
except RuntimeError as exc:
if exc is value:
return False
# Avoid suppressing if a StopIteration exception
# was passed to throw() and later wrapped into a RuntimeError
# (see PEP 479 for sync generators; async generators also
# have this behavior). But do this only if the exception wrapped
# by the RuntimeError is actully Stop(Async)Iteration (see
# issue29692).
if isinstance(value, (StopIteration, StopAsyncIteration)):
if exc.__cause__ is value:
return False
raise
except BaseException as exc:
if exc is not value:
raise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hitting this line means adding a test case that replaces the thrown in exception with an entirely unrelated one that is neither StopAsyncIteration nor RuntimeError



def contextmanager(func):
"""@contextmanager decorator.

Expand All @@ -152,14 +207,46 @@ def some_generator(<arguments>):
<body>
finally:
<cleanup>

"""
@wraps(func)
def helper(*args, **kwds):
return _GeneratorContextManager(func, args, kwds)
return helper


def asynccontextmanager(func):
"""@asynccontextmanager decorator.

Typical usage:

@asynccontextmanager
async def some_async_generator(<arguments>):
<setup>
try:
yield <value>
finally:
<cleanup>

This makes this:

async with some_async_generator(<arguments>) as <variable>:
<body>

equivalent to this:

<setup>
try:
<variable> = <value>
<body>
finally:
<cleanup>
"""
@wraps(func)
def helper(*args, **kwds):
return _AsyncGeneratorContextManager(func, args, kwds)
return helper


class closing(AbstractContextManager):
"""Context to automatically close something at the end of a block.

Expand Down
Loading