mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 23:21:29 +00:00 
			
		
		
		
	bpo-41229: Update docs for explicit aclose()-required cases and add contextlib.aclosing() method (GH-21545)
This is a PR to:
 * Add `contextlib.aclosing` which ia analogous to `contextlib.closing` but for async-generators with an explicit test case for [bpo-41229]()
 * Update the docs to describe when we need explicit `aclose()` invocation.
which are motivated by the following issues, articles, and examples:
 * [bpo-41229]()
 * https://github.com/njsmith/async_generator
 * https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#cleanup-in-generators-and-async-generators
 * https://www.python.org/dev/peps/pep-0533/
 * ef7bf0cea7/src/aiotools/context.py (L152)
Particuarly regarding [PEP-533](https://www.python.org/dev/peps/pep-0533/), its acceptance (`__aiterclose__()`) would make this little addition of `contextlib.aclosing()` unnecessary for most use cases, but until then this could serve as a good counterpart and analogy to `contextlib.closing()`. The same applies for `contextlib.closing` with `__iterclose__()`.
Also, still there are other use cases, e.g., when working with non-generator objects with `aclose()` methods.
			
			
This commit is contained in:
		
							parent
							
								
									e9208f0e74
								
							
						
					
					
						commit
						6e8dcdaaa4
					
				
					 5 changed files with 133 additions and 4 deletions
				
			
		| 
						 | 
				
			
			@ -154,6 +154,39 @@ Functions and classes provided:
 | 
			
		|||
   ``page.close()`` will be called when the :keyword:`with` block is exited.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. class:: aclosing(thing)
 | 
			
		||||
 | 
			
		||||
   Return an async context manager that calls the ``aclose()`` method of *thing*
 | 
			
		||||
   upon completion of the block.  This is basically equivalent to::
 | 
			
		||||
 | 
			
		||||
      from contextlib import asynccontextmanager
 | 
			
		||||
 | 
			
		||||
      @asynccontextmanager
 | 
			
		||||
      async def aclosing(thing):
 | 
			
		||||
          try:
 | 
			
		||||
              yield thing
 | 
			
		||||
          finally:
 | 
			
		||||
              await thing.aclose()
 | 
			
		||||
 | 
			
		||||
   Significantly, ``aclosing()`` supports deterministic cleanup of async
 | 
			
		||||
   generators when they happen to exit early by :keyword:`break` or an
 | 
			
		||||
   exception.  For example::
 | 
			
		||||
 | 
			
		||||
      from contextlib import aclosing
 | 
			
		||||
 | 
			
		||||
      async with aclosing(my_generator()) as values:
 | 
			
		||||
          async for value in values:
 | 
			
		||||
              if value == 42:
 | 
			
		||||
                  break
 | 
			
		||||
 | 
			
		||||
   This pattern ensures that the generator's async exit code is executed in
 | 
			
		||||
   the same context as its iterations (so that exceptions and context
 | 
			
		||||
   variables work as expected, and the exit code isn't run after the
 | 
			
		||||
   lifetime of some task it depends on).
 | 
			
		||||
 | 
			
		||||
   .. versionadded:: 3.10
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _simplifying-support-for-single-optional-context-managers:
 | 
			
		||||
 | 
			
		||||
.. function:: nullcontext(enter_result=None)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -643,6 +643,16 @@ after resuming depends on the method which resumed the execution.  If
 | 
			
		|||
:meth:`~agen.asend` is used, then the result will be the value passed in to
 | 
			
		||||
that method.
 | 
			
		||||
 | 
			
		||||
If an asynchronous generator happens to exit early by :keyword:`break`, the caller
 | 
			
		||||
task being cancelled, or other exceptions, the generator's async cleanup code
 | 
			
		||||
will run and possibly raise exceptions or access context variables in an
 | 
			
		||||
unexpected context--perhaps after the lifetime of tasks it depends, or
 | 
			
		||||
during the event loop shutdown when the async-generator garbage collection hook
 | 
			
		||||
is called.
 | 
			
		||||
To prevent this, the caller must explicitly close the async generator by calling
 | 
			
		||||
:meth:`~agen.aclose` method to finalize the generator and ultimately detach it
 | 
			
		||||
from the event loop.
 | 
			
		||||
 | 
			
		||||
In an asynchronous generator function, yield expressions are allowed anywhere
 | 
			
		||||
in a :keyword:`try` construct. However, if an asynchronous generator is not
 | 
			
		||||
resumed before it is finalized (by reaching a zero reference count or by
 | 
			
		||||
| 
						 | 
				
			
			@ -654,9 +664,9 @@ generator-iterator's :meth:`~agen.aclose` method and run the resulting
 | 
			
		|||
coroutine object, thus allowing any pending :keyword:`!finally` clauses
 | 
			
		||||
to execute.
 | 
			
		||||
 | 
			
		||||
To take care of finalization, an event loop should define
 | 
			
		||||
a *finalizer* function which takes an asynchronous generator-iterator
 | 
			
		||||
and presumably calls :meth:`~agen.aclose` and executes the coroutine.
 | 
			
		||||
To take care of finalization upon event loop termination, an event loop should
 | 
			
		||||
define a *finalizer* function which takes an asynchronous generator-iterator and
 | 
			
		||||
presumably calls :meth:`~agen.aclose` and executes the coroutine.
 | 
			
		||||
This  *finalizer* may be registered by calling :func:`sys.set_asyncgen_hooks`.
 | 
			
		||||
When first iterated over, an asynchronous generator-iterator will store the
 | 
			
		||||
registered *finalizer* to be called upon finalization. For a reference example
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -303,6 +303,32 @@ def __exit__(self, *exc_info):
 | 
			
		|||
        self.thing.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class aclosing(AbstractAsyncContextManager):
 | 
			
		||||
    """Async context manager for safely finalizing an asynchronously cleaned-up
 | 
			
		||||
    resource such as an async generator, calling its ``aclose()`` method.
 | 
			
		||||
 | 
			
		||||
    Code like this:
 | 
			
		||||
 | 
			
		||||
        async with aclosing(<module>.fetch(<arguments>)) as agen:
 | 
			
		||||
            <block>
 | 
			
		||||
 | 
			
		||||
    is equivalent to this:
 | 
			
		||||
 | 
			
		||||
        agen = <module>.fetch(<arguments>)
 | 
			
		||||
        try:
 | 
			
		||||
            <block>
 | 
			
		||||
        finally:
 | 
			
		||||
            await agen.aclose()
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, thing):
 | 
			
		||||
        self.thing = thing
 | 
			
		||||
    async def __aenter__(self):
 | 
			
		||||
        return self.thing
 | 
			
		||||
    async def __aexit__(self, *exc_info):
 | 
			
		||||
        await self.thing.aclose()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class _RedirectStream(AbstractContextManager):
 | 
			
		||||
 | 
			
		||||
    _stream = None
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import asyncio
 | 
			
		||||
from contextlib import asynccontextmanager, AbstractAsyncContextManager, AsyncExitStack
 | 
			
		||||
from contextlib import aclosing, asynccontextmanager, AbstractAsyncContextManager, AsyncExitStack
 | 
			
		||||
import functools
 | 
			
		||||
from test import support
 | 
			
		||||
import unittest
 | 
			
		||||
| 
						 | 
				
			
			@ -279,6 +279,63 @@ async def woohoo(self, func, args, kwds):
 | 
			
		|||
            self.assertEqual(target, (11, 22, 33, 44))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AclosingTestCase(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    @support.requires_docstrings
 | 
			
		||||
    def test_instance_docs(self):
 | 
			
		||||
        cm_docstring = aclosing.__doc__
 | 
			
		||||
        obj = aclosing(None)
 | 
			
		||||
        self.assertEqual(obj.__doc__, cm_docstring)
 | 
			
		||||
 | 
			
		||||
    @_async_test
 | 
			
		||||
    async def test_aclosing(self):
 | 
			
		||||
        state = []
 | 
			
		||||
        class C:
 | 
			
		||||
            async def aclose(self):
 | 
			
		||||
                state.append(1)
 | 
			
		||||
        x = C()
 | 
			
		||||
        self.assertEqual(state, [])
 | 
			
		||||
        async with aclosing(x) as y:
 | 
			
		||||
            self.assertEqual(x, y)
 | 
			
		||||
        self.assertEqual(state, [1])
 | 
			
		||||
 | 
			
		||||
    @_async_test
 | 
			
		||||
    async def test_aclosing_error(self):
 | 
			
		||||
        state = []
 | 
			
		||||
        class C:
 | 
			
		||||
            async def aclose(self):
 | 
			
		||||
                state.append(1)
 | 
			
		||||
        x = C()
 | 
			
		||||
        self.assertEqual(state, [])
 | 
			
		||||
        with self.assertRaises(ZeroDivisionError):
 | 
			
		||||
            async with aclosing(x) as y:
 | 
			
		||||
                self.assertEqual(x, y)
 | 
			
		||||
                1 / 0
 | 
			
		||||
        self.assertEqual(state, [1])
 | 
			
		||||
 | 
			
		||||
    @_async_test
 | 
			
		||||
    async def test_aclosing_bpo41229(self):
 | 
			
		||||
        state = []
 | 
			
		||||
 | 
			
		||||
        class Resource:
 | 
			
		||||
            def __del__(self):
 | 
			
		||||
                state.append(1)
 | 
			
		||||
 | 
			
		||||
        async def agenfunc():
 | 
			
		||||
            r = Resource()
 | 
			
		||||
            yield -1
 | 
			
		||||
            yield -2
 | 
			
		||||
 | 
			
		||||
        x = agenfunc()
 | 
			
		||||
        self.assertEqual(state, [])
 | 
			
		||||
        with self.assertRaises(ZeroDivisionError):
 | 
			
		||||
            async with aclosing(x) as y:
 | 
			
		||||
                self.assertEqual(x, y)
 | 
			
		||||
                self.assertEqual(-1, await x.__anext__())
 | 
			
		||||
                1 / 0
 | 
			
		||||
        self.assertEqual(state, [1])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase):
 | 
			
		||||
    class SyncAsyncExitStack(AsyncExitStack):
 | 
			
		||||
        @staticmethod
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
Add ``contextlib.aclosing`` for deterministic cleanup of async generators
 | 
			
		||||
which is analogous to ``contextlib.closing`` for non-async generators.
 | 
			
		||||
Patch by Joongi Kim and John Belmonte.
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue