From 904e8a8bb699b6eff1f086a2e60f36303d614968 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 18 Jan 2017 22:36:31 -0800 Subject: [PATCH 01/16] implement @asynccontextmanager Needs docs and tests --- Lib/contextlib.py | 81 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 8421968525947e..da99ebad39aa23 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -54,8 +54,9 @@ def inner(*args, **kwds): return inner -class _GeneratorContextManager(ContextDecorator, AbstractContextManager): - """Helper for @contextmanager decorator.""" +class _GeneratorContextManagerBase(ContextDecorator): + """Shared functionality for the @contextmanager and @asynccontextmanager + implementations.""" def __init__(self, func, args, kwds): self.gen = func(*args, **kwds) @@ -77,6 +78,10 @@ def _recreate_cm(self): # called return self.__class__(self.func, self.args, self.kwds) + +class _GeneratorContextManager(_GeneratorContextManagerBase, AbstractContextManager): + """Helper for @contextmanager decorator.""" + def __enter__(self): try: return next(self.gen) @@ -126,6 +131,44 @@ def __exit__(self, type, value, traceback): 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 + + async def __aexit__(self, type, value, traceback): + if type is None: + try: + await self.gen.__anext__() + except StopAsyncIteration: + return + else: + raise RuntimeError("generator didn't stop") + else: + if value is None: + value = type() + # See _GeneratorContextManager.__exit__ for comments on subtleties + # in this implementation + try: + await self.gen.athrow(type, 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 + if exc.__cause__ is value: + return False + raise + except: + if sys.exc_info()[1] is not value: + raise + + def contextmanager(func): """@contextmanager decorator. @@ -160,6 +203,40 @@ def helper(*args, **kwds): return helper +def asynccontextmanager(func): + """@contextmanager decorator. + + Typical usage: + + @asynccontextmanager + async def some_async_generator(): + + try: + yield + finally: + + + This makes this: + + async with some_async_generator() as : + + + equivalent to this: + + + try: + = + + finally: + + + """ + @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. From b3d59f16062c7c93de00f4408b98d83529ec5ae3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Feb 2017 15:57:05 -0800 Subject: [PATCH 02/16] add it to __all__ --- Lib/contextlib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index da99ebad39aa23..5a02152c4abc01 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -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): From a1d5b3f4e11f4842beaa1cd24084bf3e7e2df688 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Feb 2017 15:57:14 -0800 Subject: [PATCH 03/16] add tests (duplicating the @contextmanager ones) --- Lib/test/test_contextlib.py | 155 ++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index c04c804af57047..7c6e99a2f9d96f 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -1,5 +1,6 @@ """Unit tests for contextlib.py, and other context managers.""" +import asyncio import io import sys import tempfile @@ -189,6 +190,160 @@ def woohoo(self, func, args, kwds): self.assertEqual(target, (11, 22, 33, 44)) +def _async_test(func): + """Decorator to turn an async function into a test case.""" + def wrapper(*args, **kwargs): + loop = asyncio.get_event_loop() + coro = func(*args, **kwargs) + return loop.run_until_complete(coro) + return wrapper + + +class AsyncContextManagerTestCase(unittest.TestCase): + + @_async_test + async def test_contextmanager_plain(self): + state = [] + @asynccontextmanager + async def woohoo(): + state.append(1) + yield 42 + state.append(999) + async with woohoo() as x: + self.assertEqual(state, [1]) + self.assertEqual(x, 42) + state.append(x) + self.assertEqual(state, [1, 42, 999]) + + @_async_test + async def test_contextmanager_finally(self): + state = [] + @asynccontextmanager + async def woohoo(): + state.append(1) + try: + yield 42 + finally: + state.append(999) + with self.assertRaises(ZeroDivisionError): + async with woohoo() as x: + self.assertEqual(state, [1]) + self.assertEqual(x, 42) + state.append(x) + raise ZeroDivisionError() + self.assertEqual(state, [1, 42, 999]) + + @_async_test + async def test_contextmanager_no_reraise(self): + @asynccontextmanager + async def whee(): + yield + ctx = whee() + await ctx.__aenter__() + # Calling __exit__ should not result in an exception + self.assertFalse(await ctx.__aexit__(TypeError, TypeError("foo"), None)) + + @_async_test + async def test_contextmanager_trap_yield_after_throw(self): + @asynccontextmanager + async def whoo(): + try: + yield + except: + yield + ctx = whoo() + await ctx.__aenter__() + with self.assertRaises(RuntimeError): + await ctx.__aexit__(TypeError, TypeError('foo'), None) + + @_async_test + async def test_contextmanager_except(self): + state = [] + @asynccontextmanager + async def woohoo(): + state.append(1) + try: + yield 42 + except ZeroDivisionError as e: + state.append(e.args[0]) + self.assertEqual(state, [1, 42, 999]) + async with woohoo() as x: + self.assertEqual(state, [1]) + self.assertEqual(x, 42) + state.append(x) + raise ZeroDivisionError(999) + self.assertEqual(state, [1, 42, 999]) + + @_async_test + async def test_contextmanager_except_stopiter(self): + stop_exc = StopIteration('spam') + @asynccontextmanager + async def woohoo(): + yield + try: + async with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail('StopIteration was suppressed') + + @_async_test + async def test_contextmanager_except_stopasynciter(self): + stop_exc = StopAsyncIteration('spam') + @asynccontextmanager + async def woohoo(): + yield + try: + async with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail('StopAsyncIteration was suppressed') + + def _create_contextmanager_attribs(self): + def attribs(**kw): + def decorate(func): + for k,v in kw.items(): + setattr(func,k,v) + return func + return decorate + @asynccontextmanager + @attribs(foo='bar') + async def baz(spam): + """Whee!""" + yield + return baz + + def test_contextmanager_attribs(self): + baz = self._create_contextmanager_attribs() + self.assertEqual(baz.__name__,'baz') + self.assertEqual(baz.foo, 'bar') + + @support.requires_docstrings + def test_contextmanager_doc_attrib(self): + baz = self._create_contextmanager_attribs() + self.assertEqual(baz.__doc__, "Whee!") + + @support.requires_docstrings + @_async_test + async def test_instance_docstring_given_cm_docstring(self): + baz = self._create_contextmanager_attribs()(None) + self.assertEqual(baz.__doc__, "Whee!") + async with baz: + pass # suppress warning + + @_async_test + async def test_keywords(self): + # Ensure no keyword arguments are inhibited + @asynccontextmanager + async def woohoo(self, func, args, kwds): + yield (self, func, args, kwds) + async with woohoo(self=11, func=22, args=33, kwds=44) as target: + self.assertEqual(target, (11, 22, 33, 44)) + + class ClosingTestCase(unittest.TestCase): @support.requires_docstrings From c5b8b436c0bfae160478870d7838e5c416cfb1e1 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Feb 2017 16:16:57 -0800 Subject: [PATCH 04/16] add docs --- Doc/library/contextlib.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index dd34c96c8f8d60..c65e6c8c0cead4 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -80,6 +80,35 @@ Functions and classes provided: Use of :class:`ContextDecorator`. +.. decorator:: asynccontextmanager + + Similar to :func:`~contextlib.contextmanager`, but works with + :term:`coroutines `. + + 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. + + 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 From ca77cd2b7c43e9b071aa001ddaaa28966a49df62 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Feb 2017 20:42:49 -0800 Subject: [PATCH 05/16] back out ContextDecorator for asynccontextmanager (it doesn't work); fix docstring --- Lib/contextlib.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 5a02152c4abc01..40f2519ad1f510 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -54,7 +54,7 @@ def inner(*args, **kwds): return inner -class _GeneratorContextManagerBase(ContextDecorator): +class _GeneratorContextManagerBase: """Shared functionality for the @contextmanager and @asynccontextmanager implementations.""" @@ -72,16 +72,18 @@ 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 # called return self.__class__(self.func, self.args, self.kwds) - -class _GeneratorContextManager(_GeneratorContextManagerBase, AbstractContextManager): - """Helper for @contextmanager decorator.""" - def __enter__(self): try: return next(self.gen) @@ -204,7 +206,7 @@ def helper(*args, **kwds): def asynccontextmanager(func): - """@contextmanager decorator. + """@asynccontextmanager decorator. Typical usage: From 689f4a54c3e8dcbecdc8148d0fbc6f0f90c77fc7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Feb 2017 20:43:11 -0800 Subject: [PATCH 06/16] move asynccontextmanager tests into their own file --- Lib/test/test_contextlib.py | 155 ---------------------------- Lib/test/test_contextlib_async.py | 162 ++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 155 deletions(-) create mode 100644 Lib/test/test_contextlib_async.py diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 7c6e99a2f9d96f..c04c804af57047 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -1,6 +1,5 @@ """Unit tests for contextlib.py, and other context managers.""" -import asyncio import io import sys import tempfile @@ -190,160 +189,6 @@ def woohoo(self, func, args, kwds): self.assertEqual(target, (11, 22, 33, 44)) -def _async_test(func): - """Decorator to turn an async function into a test case.""" - def wrapper(*args, **kwargs): - loop = asyncio.get_event_loop() - coro = func(*args, **kwargs) - return loop.run_until_complete(coro) - return wrapper - - -class AsyncContextManagerTestCase(unittest.TestCase): - - @_async_test - async def test_contextmanager_plain(self): - state = [] - @asynccontextmanager - async def woohoo(): - state.append(1) - yield 42 - state.append(999) - async with woohoo() as x: - self.assertEqual(state, [1]) - self.assertEqual(x, 42) - state.append(x) - self.assertEqual(state, [1, 42, 999]) - - @_async_test - async def test_contextmanager_finally(self): - state = [] - @asynccontextmanager - async def woohoo(): - state.append(1) - try: - yield 42 - finally: - state.append(999) - with self.assertRaises(ZeroDivisionError): - async with woohoo() as x: - self.assertEqual(state, [1]) - self.assertEqual(x, 42) - state.append(x) - raise ZeroDivisionError() - self.assertEqual(state, [1, 42, 999]) - - @_async_test - async def test_contextmanager_no_reraise(self): - @asynccontextmanager - async def whee(): - yield - ctx = whee() - await ctx.__aenter__() - # Calling __exit__ should not result in an exception - self.assertFalse(await ctx.__aexit__(TypeError, TypeError("foo"), None)) - - @_async_test - async def test_contextmanager_trap_yield_after_throw(self): - @asynccontextmanager - async def whoo(): - try: - yield - except: - yield - ctx = whoo() - await ctx.__aenter__() - with self.assertRaises(RuntimeError): - await ctx.__aexit__(TypeError, TypeError('foo'), None) - - @_async_test - async def test_contextmanager_except(self): - state = [] - @asynccontextmanager - async def woohoo(): - state.append(1) - try: - yield 42 - except ZeroDivisionError as e: - state.append(e.args[0]) - self.assertEqual(state, [1, 42, 999]) - async with woohoo() as x: - self.assertEqual(state, [1]) - self.assertEqual(x, 42) - state.append(x) - raise ZeroDivisionError(999) - self.assertEqual(state, [1, 42, 999]) - - @_async_test - async def test_contextmanager_except_stopiter(self): - stop_exc = StopIteration('spam') - @asynccontextmanager - async def woohoo(): - yield - try: - async with woohoo(): - raise stop_exc - except Exception as ex: - self.assertIs(ex, stop_exc) - else: - self.fail('StopIteration was suppressed') - - @_async_test - async def test_contextmanager_except_stopasynciter(self): - stop_exc = StopAsyncIteration('spam') - @asynccontextmanager - async def woohoo(): - yield - try: - async with woohoo(): - raise stop_exc - except Exception as ex: - self.assertIs(ex, stop_exc) - else: - self.fail('StopAsyncIteration was suppressed') - - def _create_contextmanager_attribs(self): - def attribs(**kw): - def decorate(func): - for k,v in kw.items(): - setattr(func,k,v) - return func - return decorate - @asynccontextmanager - @attribs(foo='bar') - async def baz(spam): - """Whee!""" - yield - return baz - - def test_contextmanager_attribs(self): - baz = self._create_contextmanager_attribs() - self.assertEqual(baz.__name__,'baz') - self.assertEqual(baz.foo, 'bar') - - @support.requires_docstrings - def test_contextmanager_doc_attrib(self): - baz = self._create_contextmanager_attribs() - self.assertEqual(baz.__doc__, "Whee!") - - @support.requires_docstrings - @_async_test - async def test_instance_docstring_given_cm_docstring(self): - baz = self._create_contextmanager_attribs()(None) - self.assertEqual(baz.__doc__, "Whee!") - async with baz: - pass # suppress warning - - @_async_test - async def test_keywords(self): - # Ensure no keyword arguments are inhibited - @asynccontextmanager - async def woohoo(self, func, args, kwds): - yield (self, func, args, kwds) - async with woohoo(self=11, func=22, args=33, kwds=44) as target: - self.assertEqual(target, (11, 22, 33, 44)) - - class ClosingTestCase(unittest.TestCase): @support.requires_docstrings diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py new file mode 100644 index 00000000000000..3c501b84848674 --- /dev/null +++ b/Lib/test/test_contextlib_async.py @@ -0,0 +1,162 @@ +import asyncio +from contextlib import asynccontextmanager +from test import support +import unittest + + +def _async_test(func): + """Decorator to turn an async function into a test case.""" + def wrapper(*args, **kwargs): + loop = asyncio.get_event_loop() + coro = func(*args, **kwargs) + return loop.run_until_complete(coro) + return wrapper + + +class AsyncContextManagerTestCase(unittest.TestCase): + + @_async_test + async def test_contextmanager_plain(self): + state = [] + @asynccontextmanager + async def woohoo(): + state.append(1) + yield 42 + state.append(999) + async with woohoo() as x: + self.assertEqual(state, [1]) + self.assertEqual(x, 42) + state.append(x) + self.assertEqual(state, [1, 42, 999]) + + @_async_test + async def test_contextmanager_finally(self): + state = [] + @asynccontextmanager + async def woohoo(): + state.append(1) + try: + yield 42 + finally: + state.append(999) + with self.assertRaises(ZeroDivisionError): + async with woohoo() as x: + self.assertEqual(state, [1]) + self.assertEqual(x, 42) + state.append(x) + raise ZeroDivisionError() + self.assertEqual(state, [1, 42, 999]) + + @_async_test + async def test_contextmanager_no_reraise(self): + @asynccontextmanager + async def whee(): + yield + ctx = whee() + await ctx.__aenter__() + # Calling __exit__ should not result in an exception + self.assertFalse(await ctx.__aexit__(TypeError, TypeError("foo"), None)) + + @_async_test + async def test_contextmanager_trap_yield_after_throw(self): + @asynccontextmanager + async def whoo(): + try: + yield + except: + yield + ctx = whoo() + await ctx.__aenter__() + with self.assertRaises(RuntimeError): + await ctx.__aexit__(TypeError, TypeError('foo'), None) + + @_async_test + async def test_contextmanager_except(self): + state = [] + @asynccontextmanager + async def woohoo(): + state.append(1) + try: + yield 42 + except ZeroDivisionError as e: + state.append(e.args[0]) + self.assertEqual(state, [1, 42, 999]) + async with woohoo() as x: + self.assertEqual(state, [1]) + self.assertEqual(x, 42) + state.append(x) + raise ZeroDivisionError(999) + self.assertEqual(state, [1, 42, 999]) + + @_async_test + async def test_contextmanager_except_stopiter(self): + stop_exc = StopIteration('spam') + @asynccontextmanager + async def woohoo(): + yield + try: + async with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail('StopIteration was suppressed') + + @_async_test + async def test_contextmanager_except_stopasynciter(self): + stop_exc = StopAsyncIteration('spam') + @asynccontextmanager + async def woohoo(): + yield + try: + async with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail('StopAsyncIteration was suppressed') + + def _create_contextmanager_attribs(self): + def attribs(**kw): + def decorate(func): + for k,v in kw.items(): + setattr(func,k,v) + return func + return decorate + @asynccontextmanager + @attribs(foo='bar') + async def baz(spam): + """Whee!""" + yield + return baz + + def test_contextmanager_attribs(self): + baz = self._create_contextmanager_attribs() + self.assertEqual(baz.__name__,'baz') + self.assertEqual(baz.foo, 'bar') + + @support.requires_docstrings + def test_contextmanager_doc_attrib(self): + baz = self._create_contextmanager_attribs() + self.assertEqual(baz.__doc__, "Whee!") + + @support.requires_docstrings + @_async_test + async def test_instance_docstring_given_cm_docstring(self): + baz = self._create_contextmanager_attribs()(None) + self.assertEqual(baz.__doc__, "Whee!") + async with baz: + pass # suppress warning + + @_async_test + async def test_keywords(self): + # Ensure no keyword arguments are inhibited + @asynccontextmanager + async def woohoo(self, func, args, kwds): + yield (self, func, args, kwds) + async with woohoo(self=11, func=22, args=33, kwds=44) as target: + self.assertEqual(target, (11, 22, 33, 44)) + + +if __name__ == '__main__': + unittest.main() From 299d968dcd469c95bfbb55eb7c9252a3f3feb631 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Mar 2017 00:19:13 -0800 Subject: [PATCH 07/16] fix when tests are run after test_asyncio --- Lib/test/test_contextlib_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 3c501b84848674..0303623c55f78d 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -7,7 +7,7 @@ def _async_test(func): """Decorator to turn an async function into a test case.""" def wrapper(*args, **kwargs): - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() coro = func(*args, **kwargs) return loop.run_until_complete(coro) return wrapper From e974d480aafb3bfe770911cd89dcbcfe6d8857b0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Mar 2017 08:01:56 -0800 Subject: [PATCH 08/16] add a few more tests --- Lib/test/test_contextlib_async.py | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 0303623c55f78d..3df887062c66fa 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -54,7 +54,7 @@ async def whee(): yield ctx = whee() await ctx.__aenter__() - # Calling __exit__ should not result in an exception + # Calling __aexit__ should not result in an exception self.assertFalse(await ctx.__aexit__(TypeError, TypeError("foo"), None)) @_async_test @@ -70,6 +70,41 @@ async def whoo(): with self.assertRaises(RuntimeError): await ctx.__aexit__(TypeError, TypeError('foo'), None) + @_async_test + async def test_contextmanager_trap_no_yield(self): + @asynccontextmanager + async def whoo(): + if False: + yield + ctx = whoo() + with self.assertRaises(RuntimeError): + await ctx.__aenter__() + + @_async_test + async def test_contextmanager_trap_second_yield(self): + @asynccontextmanager + async def whoo(): + yield + yield + ctx = whoo() + await ctx.__aenter__() + with self.assertRaises(RuntimeError): + await ctx.__aexit__(None, None, None) + + @_async_test + async def test_contextmanager_non_normalised(self): + @asynccontextmanager + async def whoo(): + try: + yield + except RuntimeError: + raise SyntaxError + + ctx = whoo() + await ctx.__aenter__() + with self.assertRaises(SyntaxError): + await ctx.__aexit__(RuntimeError, None, None) + @_async_test async def test_contextmanager_except(self): state = [] From 5808a4c987cab85103de034f230346130b1f6b74 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Mar 2017 09:05:19 -0800 Subject: [PATCH 09/16] combine duplicate tests --- Lib/test/test_contextlib_async.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 3df887062c66fa..ee0d2a857dd18c 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -125,31 +125,19 @@ async def woohoo(): @_async_test async def test_contextmanager_except_stopiter(self): - stop_exc = StopIteration('spam') @asynccontextmanager async def woohoo(): yield - try: - async with woohoo(): - raise stop_exc - except Exception as ex: - self.assertIs(ex, stop_exc) - else: - self.fail('StopIteration was suppressed') - @_async_test - async def test_contextmanager_except_stopasynciter(self): - stop_exc = StopAsyncIteration('spam') - @asynccontextmanager - async def woohoo(): - yield - try: - async with woohoo(): - raise stop_exc - except Exception as ex: - self.assertIs(ex, stop_exc) - else: - self.fail('StopAsyncIteration was suppressed') + for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')): + with self.subTest(type=type(stop_exc)): + try: + async with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail(f'{stop_exc} was suppressed') def _create_contextmanager_attribs(self): def attribs(**kw): From 9caa2435a76836a9c7cf2b35f123e72e6186e297 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Mar 2017 09:11:21 -0800 Subject: [PATCH 10/16] additional test for RuntimeError wrapping --- Lib/contextlib.py | 11 +++++++++-- Lib/test/test_contextlib_async.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 40f2519ad1f510..a212ca4890cc0e 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -163,8 +163,15 @@ async def __aexit__(self, type, value, traceback): except RuntimeError as exc: if exc is value: return False - if exc.__cause__ 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: if sys.exc_info()[1] is not value: diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index ee0d2a857dd18c..af193525b08dea 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -139,6 +139,26 @@ async def woohoo(): else: self.fail(f'{stop_exc} was suppressed') + @_async_test + async def test_contextmanager_wrap_runtimeerror(self): + @asynccontextmanager + async def woohoo(): + try: + yield + except Exception as exc: + raise RuntimeError(f'caught {exc}') from exc + + with self.assertRaises(RuntimeError): + async with woohoo(): + 1 / 0 + + # If the context manager wrapped StopAsyncIteration in a RuntimeError, + # we also unwrap it, because we can't tell whether the wrapping was + # done by the generator machinery or by the generator itself. + with self.assertRaises(StopAsyncIteration): + async with woohoo(): + raise StopAsyncIteration + def _create_contextmanager_attribs(self): def attribs(**kw): def decorate(func): From 64e69089ab09d206ed7466f4d72b131a94f69ffe Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Mar 2017 09:51:06 -0800 Subject: [PATCH 11/16] address 1st1's comments --- Doc/library/contextlib.rst | 4 ++-- Doc/reference/datamodel.rst | 2 ++ Lib/contextlib.py | 11 +++++------ Lib/test/test_contextlib_async.py | 10 ++++++++-- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index c65e6c8c0cead4..52ec96efa65f9e 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -82,8 +82,8 @@ Functions and classes provided: .. decorator:: asynccontextmanager - Similar to :func:`~contextlib.contextmanager`, but works with - :term:`coroutines `. + Similar to :func:`~contextlib.contextmanager`, but creates an + :ref:`asynchronous context manager `. This function is a :term:`decorator` that can be used to define a factory function for :keyword:`async with` statement asynchronous context managers, diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 095a2380b379bc..f09aabe4d15360 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -2566,6 +2566,8 @@ An example of an asynchronous iterable object:: result in a :exc:`RuntimeError`. +.. _async-context-managers: + Asynchronous Context Managers ----------------------------- diff --git a/Lib/contextlib.py b/Lib/contextlib.py index a212ca4890cc0e..dea483cbd79072 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -55,8 +55,7 @@ def inner(*args, **kwds): class _GeneratorContextManagerBase: - """Shared functionality for the @contextmanager and @asynccontextmanager - implementations.""" + """Shared functionality for @contextmanager and @asynccontextmanager.""" def __init__(self, func, args, kwds): self.gen = func(*args, **kwds) @@ -142,8 +141,8 @@ async def __aenter__(self): except StopAsyncIteration: raise RuntimeError("generator didn't yield") from None - async def __aexit__(self, type, value, traceback): - if type is None: + async def __aexit__(self, typ, value, traceback): + if typ is None: try: await self.gen.__anext__() except StopAsyncIteration: @@ -152,11 +151,11 @@ async def __aexit__(self, type, value, traceback): raise RuntimeError("generator didn't stop") else: if value is None: - value = type() + value = typ() # See _GeneratorContextManager.__exit__ for comments on subtleties # in this implementation try: - await self.gen.athrow(type, value, traceback) + await self.gen.athrow(typ, value, traceback) raise RuntimeError("generator didn't stop after throw()") except StopAsyncIteration as exc: return exc is not value diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index af193525b08dea..5884565cc8c1b5 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -1,5 +1,6 @@ import asyncio from contextlib import asynccontextmanager +import functools from test import support import unittest @@ -7,9 +8,14 @@ def _async_test(func): """Decorator to turn an async function into a test case.""" def wrapper(*args, **kwargs): - loop = asyncio.new_event_loop() coro = func(*args, **kwargs) - return loop.run_until_complete(coro) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(coro) + finally: + loop.close() + asyncio.set_event_loop(None) return wrapper From 178433b572bd9cf071e728f4d73517fe02dc962a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Mar 2017 20:44:34 -0800 Subject: [PATCH 12/16] clean up "except:" and explain why we can't do that for @contextmanager --- Lib/contextlib.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index dea483cbd79072..93587533ac724c 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -128,6 +128,10 @@ 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'. if sys.exc_info()[1] is not value: raise @@ -172,8 +176,8 @@ async def __aexit__(self, typ, value, traceback): if exc.__cause__ is value: return False raise - except: - if sys.exc_info()[1] is not value: + except BaseException as exc: + if exc is not value: raise @@ -203,7 +207,6 @@ def some_generator(): finally: - """ @wraps(func) def helper(*args, **kwds): @@ -237,7 +240,6 @@ async def some_async_generator(): finally: - """ @wraps(func) def helper(*args, **kwds): From 6d0dddb6762369a96bf6abaac90c8bc3d5be0ee8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Mar 2017 22:46:02 -0800 Subject: [PATCH 13/16] old-style classes, not strings --- Lib/contextlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 93587533ac724c..6f13e6626cd6d9 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -130,8 +130,8 @@ def __exit__(self, type, value, traceback): # # 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'. + # Python 2, where old-style class exceptions are not caught + # by 'except BaseException'. if sys.exc_info()[1] is not value: raise From ad65b4d093a940abfb7b1bb4714137e8182a75d3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 2 Mar 2017 23:21:56 -0800 Subject: [PATCH 14/16] add to whatsnew (and alphabetize modules) --- Doc/whatsnew/3.7.rst | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 1f5aa79c87a1e5..cb067194b1ae86 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -97,6 +97,14 @@ New Modules Improved Modules ================ + +contextlib +---------- + +:func:`contextlib.asynccontextmanager` has been added. (Contributed by +Jelle Zijlstra in :issue:`29679`.) + + unittest.mock ------------- @@ -104,12 +112,6 @@ The :const:`~unittest.mock.sentinel` attributes now preserve their identity when they are :mod:`copied ` or :mod:`pickled `. (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 ============= From 737fd0f3e3d558eb575f0eff755078a5eaaef60a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 3 Mar 2017 00:10:35 -0800 Subject: [PATCH 15/16] add wraps decorator, add to docstring --- Doc/library/contextlib.rst | 3 ++- Lib/test/test_contextlib_async.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index 52ec96efa65f9e..19793693b7ba68 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -88,7 +88,8 @@ Functions and classes provided: 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. + :meth:`__aexit__` methods. It must be applied to an :term:`asynchronous + generator` function. A simple example:: diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 5884565cc8c1b5..42cc331c0afdb9 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -7,6 +7,7 @@ def _async_test(func): """Decorator to turn an async function into a test case.""" + @functools.wraps(func) def wrapper(*args, **kwargs): coro = func(*args, **kwargs) loop = asyncio.new_event_loop() From 3fc20a7cee48b5006f63905d8820ef2b2aa4f236 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 3 Mar 2017 08:00:58 -0800 Subject: [PATCH 16/16] unalphabetize whatsnew --- Doc/whatsnew/3.7.rst | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index cb067194b1ae86..e49b8b7e767e81 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -112,6 +112,12 @@ The :const:`~unittest.mock.sentinel` attributes now preserve their identity when they are :mod:`copied ` or :mod:`pickled `. (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 ------------ @@ -121,14 +127,6 @@ 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 =============