-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
bpo-29679: Implement @contextlib.asynccontextmanager #360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
904e8a8
b3d59f1
a1d5b3f
c5b8b43
ca77cd2
689f4a5
299d968
e974d48
5808a4c
9caa243
64e6908
178433b
6d0dddb
ad65b4d
737fd0f
3fc20a7
06697a8
bb8de0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -97,19 +97,21 @@ New Modules | |
Improved Modules | ||
================ | ||
|
||
|
||
contextlib | ||
---------- | ||
|
||
:func:`contextlib.asynccontextmanager` has been added. (Contributed by | ||
Jelle Zijlstra in :issue:`29679`.) | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd leave reordering out of this PR. It should be done in a separate commit. |
||
unittest.mock | ||
------------- | ||
|
||
The :const:`~unittest.mock.sentinel` attributes now preserve their identity | ||
when they are :mod:`copied <copy>` or :mod:`pickled <pickle>`. | ||
(Contributed by Serhiy Storchaka in :issue:`20804`.) | ||
|
||
xmlrpc.server | ||
------------- | ||
|
||
:meth:`register_function` of :class:`xmlrpc.server.SimpleXMLRPCDispatcher` and | ||
its subclasses can be used as a decorator. | ||
(Contributed by Xiang Zhang in :issue:`7769`.) | ||
|
||
urllib.parse | ||
------------ | ||
|
@@ -119,6 +121,14 @@ adding `~` to the set of characters that is never quoted by default. | |
(Contributed by Christian Theune and Ratnadeep Debnath in :issue:`16285`.) | ||
|
||
|
||
xmlrpc.server | ||
------------- | ||
|
||
:meth:`register_function` of :class:`xmlrpc.server.SimpleXMLRPCDispatcher` and | ||
its subclasses can be used as a decorator. | ||
(Contributed by Xiang Zhang in :issue:`7769`.) | ||
|
||
|
||
Optimizations | ||
============= | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
|
@@ -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) | ||
|
@@ -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, | ||
6D40 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 | ||
|
@@ -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 old-style class exceptions are not caught | ||
# by 'except BaseException'. | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
|
||
def contextmanager(func): | ||
"""@contextmanager decorator. | ||
|
||
|
@@ -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. | ||
|
||
|
There was a problem hiding this comment.
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.