diff options
author | 2021-09-23 14:36:03 -0700 | |
---|---|---|
committer | 2021-09-23 23:36:03 +0200 | |
commit | 86b833badd3d6864868404ead2f8c7994d24f85c (patch) | |
tree | 0610a6e8c1b1ca5e527181d58d29227b749ab484 | |
parent | [docs] Update documentation for `multiprocessing.get_start_method` (GH-18170) (diff) | |
download | cpython-86b833badd3d6864868404ead2f8c7994d24f85c.tar.gz cpython-86b833badd3d6864868404ead2f8c7994d24f85c.tar.bz2 cpython-86b833badd3d6864868404ead2f8c7994d24f85c.zip |
bpo-38415: Allow using @asynccontextmanager-made ctx managers as decorators (GH-16667)
-rw-r--r-- | Lib/contextlib.py | 8 | ||||
-rw-r--r-- | Lib/test/test_contextlib_async.py | 76 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2019-10-08-14-08-59.bpo-38415.N1bUw6.rst | 3 |
3 files changed, 87 insertions, 0 deletions
diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 8343d7e519..1384d8903d 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -191,6 +191,14 @@ class _AsyncGeneratorContextManager( ): """Helper for @asynccontextmanager decorator.""" + def __call__(self, func): + @wraps(func) + async def inner(*args, **kwds): + async with self.__class__(self.func, self.args, self.kwds): + return await func(*args, **kwds) + + return inner + async def __aenter__(self): # do not keep args and kwds alive unnecessarily # they are only needed for recreation, which is not possible anymore diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 74fddef3f3..c738bf3c0b 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -318,6 +318,82 @@ class AsyncContextManagerTestCase(unittest.TestCase): self.assertEqual(ncols, 10) self.assertEqual(depth, 0) + @_async_test + async def test_decorator(self): + entered = False + + @asynccontextmanager + async def context(): + nonlocal entered + entered = True + yield + entered = False + + @context() + async def test(): + self.assertTrue(entered) + + self.assertFalse(entered) + await test() + self.assertFalse(entered) + + @_async_test + async def test_decorator_with_exception(self): + entered = False + + @asynccontextmanager + async def context(): + nonlocal entered + try: + entered = True + yield + finally: + entered = False + + @context() + async def test(): + self.assertTrue(entered) + raise NameError('foo') + + self.assertFalse(entered) + with self.assertRaisesRegex(NameError, 'foo'): + await test() + self.assertFalse(entered) + + @_async_test + async def test_decorating_method(self): + + @asynccontextmanager + async def context(): + yield + + + class Test(object): + + @context() + async def method(self, a, b, c=None): + self.a = a + self.b = b + self.c = c + + # these tests are for argument passing when used as a decorator + test = Test() + await test.method(1, 2) + self.assertEqual(test.a, 1) + self.assertEqual(test.b, 2) + self.assertEqual(test.c, None) + + test = Test() + await test.method('a', 'b', 'c') + self.assertEqual(test.a, 'a') + self.assertEqual(test.b, 'b') + self.assertEqual(test.c, 'c') + + test = Test() + await test.method(a=1, b=2) + self.assertEqual(test.a, 1) + self.assertEqual(test.b, 2) + class AclosingTestCase(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2019-10-08-14-08-59.bpo-38415.N1bUw6.rst b/Misc/NEWS.d/next/Library/2019-10-08-14-08-59.bpo-38415.N1bUw6.rst new file mode 100644 index 0000000000..f99bf0d19b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-10-08-14-08-59.bpo-38415.N1bUw6.rst @@ -0,0 +1,3 @@ +Added missing behavior to :func:`contextlib.asynccontextmanager` to match +:func:`contextlib.contextmanager` so decorated functions can themselves be +decorators. |