From db99d4a562bacab013c664e5419eccc5020fe40a Mon Sep 17 00:00:00 2001 From: Heckad Date: Fri, 29 May 2020 17:55:05 +0300 Subject: [PATCH 01/12] Add basic AsyncContextDecorator implementation --- Lib/contextlib.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index ff92d9f913f4c2..c0d6519e580435 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -80,6 +80,22 @@ def inner(*args, **kwds): return inner +class AsyncContextDecorator(object): + "A base class or mixin that enables async context managers to work as decorators." + + def __recreate_cm(self): + """Return a recreated instance of self. + """ + return self + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwds): + async with self._recreate_cm(): + return await func(*args, **kwds) + return inner + + class _GeneratorContextManagerBase: """Shared functionality for @contextmanager and @asynccontextmanager.""" @@ -167,9 +183,16 @@ def __exit__(self, type, value, traceback): class _AsyncGeneratorContextManager(_GeneratorContextManagerBase, - AbstractAsyncContextManager): + AbstractAsyncContextManager, + AsyncContextDecorator): """Helper for @asynccontextmanager.""" + def _recreate_cm(self): + # _AGCM instances are one-shot context managers, so the + # ACM must be recreated each time a decorated function is + # called + return self.__class__(self.func, self.args, self.kwds) + async def __aenter__(self): try: return await self.gen.__anext__() From 7d1243a3ba75104f0f72215003027672199d348b Mon Sep 17 00:00:00 2001 From: Heckad Date: Fri, 29 May 2020 18:19:04 +0300 Subject: [PATCH 02/12] Fix --- Lib/contextlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index c0d6519e580435..f51cdd85df50dd 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -90,7 +90,7 @@ def __recreate_cm(self): def __call__(self, func): @wraps(func) - def inner(*args, **kwds): + async def inner(*args, **kwds): async with self._recreate_cm(): return await func(*args, **kwds) return inner From 20aeef8a09afda0a1d1d25dbd1da5854fec321c4 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 29 May 2020 15:25:42 +0000 Subject: [PATCH 03/12] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2020-05-29-15-25-41.bpo-40816.w61Pob.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2020-05-29-15-25-41.bpo-40816.w61Pob.rst diff --git a/Misc/NEWS.d/next/Library/2020-05-29-15-25-41.bpo-40816.w61Pob.rst b/Misc/NEWS.d/next/Library/2020-05-29-15-25-41.bpo-40816.w61Pob.rst new file mode 100644 index 00000000000000..66b75779784655 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-29-15-25-41.bpo-40816.w61Pob.rst @@ -0,0 +1 @@ +Add AsyncContextDecorator to contextlib to support async context manager as a decorator. \ No newline at end of file From 858a98fbe9446e4ec781b7f3caaf90505ce2ca23 Mon Sep 17 00:00:00 2001 From: Heckad Date: Mon, 20 Jul 2020 12:11:26 +0300 Subject: [PATCH 04/12] Add test --- Lib/test/test_contextlib_async.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 43fb7fced1bfdb..096cbc15bbff22 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -278,6 +278,25 @@ async def woohoo(self, func, args, kwds): async with woohoo(self=11, func=22, args=33, kwds=44) as target: self.assertEqual(target, (11, 22, 33, 44)) + async def test_recursive(self): + depth = 0 + @asynccontextmanager + async def woohoo(): + nonlocal depth + before = depth + depth += 1 + yield + depth -= 1 + self.assertEqual(depth, before) + + @woohoo() + async def recursive(): + if depth < 10: + recursive() + + await recursive() + self.assertEqual(depth, 0) + class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase): class SyncAsyncExitStack(AsyncExitStack): From 1922773f11080ac6fefeea4278e6f43536dad692 Mon Sep 17 00:00:00 2001 From: Heckad Date: Mon, 20 Jul 2020 19:57:39 +0300 Subject: [PATCH 05/12] Fix test --- Lib/test/test_contextlib_async.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 096cbc15bbff22..8bddc6dac71f00 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -278,6 +278,7 @@ async def woohoo(self, func, args, kwds): async with woohoo(self=11, func=22, args=33, kwds=44) as target: self.assertEqual(target, (11, 22, 33, 44)) + @_async_test async def test_recursive(self): depth = 0 @asynccontextmanager From dcaa6207b298adf5e2eb379384e7984bbcfe163a Mon Sep 17 00:00:00 2001 From: Heckad Date: Wed, 22 Jul 2020 11:02:38 +0300 Subject: [PATCH 06/12] Fix function name --- Lib/contextlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index f51cdd85df50dd..6838bea96a96c6 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -83,7 +83,7 @@ def inner(*args, **kwds): class AsyncContextDecorator(object): "A base class or mixin that enables async context managers to work as decorators." - def __recreate_cm(self): + def _recreate_cm(self): """Return a recreated instance of self. """ return self From 5e6cec7394d964ea2cdf897146744c140a6873dd Mon Sep 17 00:00:00 2001 From: Heckad Date: Wed, 22 Jul 2020 11:52:08 +0300 Subject: [PATCH 07/12] Fix test --- 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 8bddc6dac71f00..e9cc2b62e11461 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -293,7 +293,7 @@ async def woohoo(): @woohoo() async def recursive(): if depth < 10: - recursive() + await recursive() await recursive() self.assertEqual(depth, 0) From a5fd9292023fb756d520db58993a648a51ce5cbe Mon Sep 17 00:00:00 2001 From: Heckad Date: Thu, 23 Jul 2020 12:05:36 +0300 Subject: [PATCH 08/12] Improve tests --- Lib/test/test_contextlib_async.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index e9cc2b62e11461..a631218e158389 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -281,8 +281,13 @@ async def woohoo(self, func, args, kwds): @_async_test async def test_recursive(self): depth = 0 + ncols = 0 + @asynccontextmanager async def woohoo(): + nonlocal ncols + ncols += 1 + nonlocal depth before = depth depth += 1 @@ -296,6 +301,8 @@ async def recursive(): await recursive() await recursive() + + self.assertEqual(ncols, 10) self.assertEqual(depth, 0) From a1735c5e316952e47e719e6e990fd5541dc3017a Mon Sep 17 00:00:00 2001 From: Heckad Date: Thu, 23 Jul 2020 21:17:23 +0300 Subject: [PATCH 09/12] Add doc --- Doc/library/contextlib.rst | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index 0aa4ad76523480..6ae9cd2d5c3296 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -351,6 +351,43 @@ Functions and classes provided: .. versionadded:: 3.2 +.. class:: AsyncContextManager + + Similar as ContextManger only for async + + Example of ``ContextDecorator``:: + + from asyncio import run + from contextlib import AsyncContextDecorator + + class mycontext(AsyncContextDecorator): + async def __aenter__(self): + print('Starting') + return self + + async def __aexit__(self, *exc): + print('Finishing') + return False + + >>> @mycontext() + ... async def function(): + ... print('The bit in the middle') + ... + >>> run(function()) + Starting + The bit in the middle + Finishing + + >>> async def function(): + ... async with mycontext(): + ... print('The bit in the middle') + ... + >>> run(function()) + Starting + The bit in the middle + Finishing + + .. class:: ExitStack() A context manager that is designed to make it easy to programmatically From 81df65c31556f5ac39ba07223faeca31bce973fe Mon Sep 17 00:00:00 2001 From: Heckad Date: Sun, 26 Jul 2020 14:55:18 +0300 Subject: [PATCH 10/12] Improve doc --- Doc/library/contextlib.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index 6ae9cd2d5c3296..4a0dbbf619e917 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -126,6 +126,16 @@ Functions and classes provided: .. versionadded:: 3.7 + :func:`asynccontextmanager` uses :class:`AsyncContextDecorator` so the context managers + it creates can be used as decorators as well as in :keyword:`async with` statements. + When used as a decorator, a new generator instance is implicitly created on + each function call (this allows the otherwise "one-shot" context managers + created by :func:`asynccontextmanager` to meet the requirement that context + managers support multiple invocations in order to be used as decorators). + + .. versionchanged:: 3.10 + Use of :class:`AsyncContextDecorator`. + .. function:: closing(thing) From 1293c87dedc11c85ba70c817b86c4be4318039e6 Mon Sep 17 00:00:00 2001 From: Kazantcev Andrey <45011689+heckad@users.noreply.github.com> Date: Sat, 31 Oct 2020 22:59:32 +0300 Subject: [PATCH 11/12] Update Doc/library/contextlib.rst Co-authored-by: Yury Selivanov --- Doc/library/contextlib.rst | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index 4a0dbbf619e917..45f541fd04bc2b 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -126,15 +126,30 @@ Functions and classes provided: .. versionadded:: 3.7 - :func:`asynccontextmanager` uses :class:`AsyncContextDecorator` so the context managers - it creates can be used as decorators as well as in :keyword:`async with` statements. + Context managers defined with :func:`asynccontextmanager` can be used + either as decorators or with :keyword:`async with` statements:: + + import time + + async def timeit(): + now = time.monotonic() + try: + yield + finally: + print(f'it took {time.monotonic() - now}s to run') + + @timeit() + async def main(): + # ... async code ... + When used as a decorator, a new generator instance is implicitly created on - each function call (this allows the otherwise "one-shot" context managers + each function call. This allows the otherwise "one-shot" context managers created by :func:`asynccontextmanager` to meet the requirement that context - managers support multiple invocations in order to be used as decorators). + managers support multiple invocations in order to be used as decorators. .. versionchanged:: 3.10 - Use of :class:`AsyncContextDecorator`. + Async context managers created with :func:`asynccontextmanager` can + be used as decorators. .. function:: closing(thing) From 1bace398398b36ddc0be4dc063dcb27e0068e94e Mon Sep 17 00:00:00 2001 From: Heckad Date: Sun, 1 Nov 2020 00:27:47 +0300 Subject: [PATCH 12/12] Fix whitespaces --- Doc/library/contextlib.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index 45f541fd04bc2b..6872de382e897c 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -128,16 +128,16 @@ Functions and classes provided: Context managers defined with :func:`asynccontextmanager` can be used either as decorators or with :keyword:`async with` statements:: - + import time - - async def timeit(): + + async def timeit(): now = time.monotonic() try: yield finally: print(f'it took {time.monotonic() - now}s to run') - + @timeit() async def main(): # ... async code ... @@ -377,11 +377,11 @@ Functions and classes provided: .. class:: AsyncContextManager - + Similar as ContextManger only for async Example of ``ContextDecorator``:: - + from asyncio import run from contextlib import AsyncContextDecorator