Skip to content

Commit 86b833b

Browse files
authored
bpo-38415: Allow using @asynccontextmanager-made ctx managers as decorators (GH-16667)
1 parent af90b54 commit 86b833b

File tree

3 files changed

+87
-0
lines changed

3 files changed

+87
-0
lines changed

Lib/contextlib.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ class _AsyncGeneratorContextManager(
191191
):
192192
"""Helper for @asynccontextmanager decorator."""
193193

194+
def __call__(self, func):
195+
@wraps(func)
196+
async def inner(*args, **kwds):
197+
async with self.__class__(self.func, self.args, self.kwds):
198+
return await func(*args, **kwds)
199+
200+
return inner
201+
194202
async def __aenter__(self):
195203
# do not keep args and kwds alive unnecessarily
196204
# they are only needed for recreation, which is not possible anymore

Lib/test/test_contextlib_async.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,82 @@ async def recursive():
318318
self.assertEqual(ncols, 10)
319319
self.assertEqual(depth, 0)
320320

321+
@_async_test
322+
async def test_decorator(self):
323+
entered = False
324+
325+
@asynccontextmanager
326+
async def context():
327+
nonlocal entered
328+
entered = True
329+
yield
330+
entered = False
331+
332+
@context()
333+
async def test():
334+
self.assertTrue(entered)
335+
336+
self.assertFalse(entered)
337+
await test()
338+
self.assertFalse(entered)
339+
340+
@_async_test
341+
async def test_decorator_with_exception(self):
342+
entered = False
343+
344+
@asynccontextmanager
345+
async def context():
346+
nonlocal entered
347+
try:
348+
entered = True
349+
yield
350+
finally:
351+
entered = False
352+
353+
@context()
354+
async def test():
355+
self.assertTrue(entered)
356+
raise NameError('foo')
357+
358+
self.assertFalse(entered)
359+
with self.assertRaisesRegex(NameError, 'foo'):
360+
await test()
361+
self.assertFalse(entered)
362+
363+
@_async_test
364+
async def test_decorating_method(self):
365+
366+
@asynccontextmanager
367+
async def context():
368+
yield
369+
370+
371+
class Test(object):
372+
373+
@context()
374+
async def method(self, a, b, c=None):
375+
self.a = a
376+
self.b = b
377+
self.c = c
378+
379+
# these tests are for argument passing when used as a decorator
380+
test = Test()
381+
await test.method(1, 2)
382+
self.assertEqual(test.a, 1)
383+
self.assertEqual(test.b, 2)
384+
self.assertEqual(test.c, None)
385+
386+
test = Test()
387+
await test.method('a', 'b', 'c')
388+
self.assertEqual(test.a, 'a')
389+
self.assertEqual(test.b, 'b')
390+
self.assertEqual(test.c, 'c')
391+
392+
test = Test()
393+
await test.method(a=1, b=2)
394+
self.assertEqual(test.a, 1)
395+
self.assertEqual(test.b, 2)
396+
321397

322398
class AclosingTestCase(unittest.TestCase):
323399

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added missing behavior to :func:`contextlib.asynccontextmanager` to match
2+
:func:`contextlib.contextmanager` so decorated functions can themselves be
3+
decorators.

0 commit comments

Comments
 (0)