| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent e9208f0 commit 6e8dcda
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -154,6 +154,39 @@ Functions and classes provided: | |||
| 154 | 154 | ``page.close()`` will be called when the :keyword:`with` block is exited. | |
| 155 | 155 | ||
| 156 | 156 | ||
| 157 | + .. class:: aclosing(thing) | ||
| 158 | + | ||
| 159 | + Return an async context manager that calls the ``aclose()`` method of *thing* | ||
| 160 | + upon completion of the block. This is basically equivalent to:: | ||
| 161 | + | ||
| 162 | + from contextlib import asynccontextmanager | ||
| 163 | + | ||
| 164 | + @asynccontextmanager | ||
| 165 | + async def aclosing(thing): | ||
| 166 | + try: | ||
| 167 | + yield thing | ||
| 168 | + finally: | ||
| 169 | + await thing.aclose() | ||
| 170 | + | ||
| 171 | + Significantly, ``aclosing()`` supports deterministic cleanup of async | ||
| 172 | + generators when they happen to exit early by :keyword:`break` or an | ||
| 173 | + exception. For example:: | ||
| 174 | + | ||
| 175 | + from contextlib import aclosing | ||
| 176 | + | ||
| 177 | + async with aclosing(my_generator()) as values: | ||
| 178 | + async for value in values: | ||
| 179 | + if value == 42: | ||
| 180 | + break | ||
| 181 | + | ||
| 182 | + This pattern ensures that the generator's async exit code is executed in | ||
| 183 | + the same context as its iterations (so that exceptions and context | ||
| 184 | + variables work as expected, and the exit code isn't run after the | ||
| 185 | + lifetime of some task it depends on). | ||
| 186 | + | ||
| 187 | + .. versionadded:: 3.10 | ||
| 188 | + | ||
| 189 | + | ||
| 157 | 190 | .. _simplifying-support-for-single-optional-context-managers: | |
| 158 | 191 | ||
| 159 | 192 | .. function:: nullcontext(enter_result=None) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -643,6 +643,16 @@ after resuming depends on the method which resumed the execution. If | |||
| 643 | 643 | :meth:`~agen.asend` is used, then the result will be the value passed in to | |
| 644 | 644 | that method. | |
| 645 | 645 | ||
| 646 | + If an asynchronous generator happens to exit early by :keyword:`break`, the caller | ||
| 647 | + task being cancelled, or other exceptions, the generator's async cleanup code | ||
| 648 | + will run and possibly raise exceptions or access context variables in an | ||
| 649 | + unexpected context--perhaps after the lifetime of tasks it depends, or | ||
| 650 | + during the event loop shutdown when the async-generator garbage collection hook | ||
| 651 | + is called. | ||
| 652 | + To prevent this, the caller must explicitly close the async generator by calling | ||
| 653 | + :meth:`~agen.aclose` method to finalize the generator and ultimately detach it | ||
| 654 | + from the event loop. | ||
| 655 | + | ||
| 646 | 656 | In an asynchronous generator function, yield expressions are allowed anywhere | |
| 647 | 657 | in a :keyword:`try` construct. However, if an asynchronous generator is not | |
| 648 | 658 | 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 | |||
| 654 | 664 | coroutine object, thus allowing any pending :keyword:`!finally` clauses | |
| 655 | 665 | to execute. | |
| 656 | 666 | ||
| 657 | - To take care of finalization, an event loop should define | ||
| 658 | - a *finalizer* function which takes an asynchronous generator-iterator | ||
| 659 | - and presumably calls :meth:`~agen.aclose` and executes the coroutine. | ||
| 667 | + To take care of finalization upon event loop termination, an event loop should | ||
| 668 | + define a *finalizer* function which takes an asynchronous generator-iterator and | ||
| 669 | + presumably calls :meth:`~agen.aclose` and executes the coroutine. | ||
| 660 | 670 | This *finalizer* may be registered by calling :func:`sys.set_asyncgen_hooks`. | |
| 661 | 671 | When first iterated over, an asynchronous generator-iterator will store the | |
| 662 | 672 | registered *finalizer* to be called upon finalization. For a reference example | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -303,6 +303,32 @@ def __exit__(self, *exc_info): | |||
| 303 | 303 | self.thing.close() | |
| 304 | 304 | ||
| 305 | 305 | ||
| 306 | + class aclosing(AbstractAsyncContextManager): | ||
| 307 | + """Async context manager for safely finalizing an asynchronously cleaned-up | ||
| 308 | + resource such as an async generator, calling its ``aclose()`` method. | ||
| 309 | + | ||
| 310 | + Code like this: | ||
| 311 | + | ||
| 312 | + async with aclosing(<module>.fetch(<arguments>)) as agen: | ||
| 313 | + <block> | ||
| 314 | + | ||
| 315 | + is equivalent to this: | ||
| 316 | + | ||
| 317 | + agen = <module>.fetch(<arguments>) | ||
| 318 | + try: | ||
| 319 | + <block> | ||
| 320 | + finally: | ||
| 321 | + await agen.aclose() | ||
| 322 | + | ||
| 323 | + """ | ||
| 324 | + def __init__(self, thing): | ||
| 325 | + self.thing = thing | ||
| 326 | + async def __aenter__(self): | ||
| 327 | + return self.thing | ||
| 328 | + async def __aexit__(self, *exc_info): | ||
| 329 | + await self.thing.aclose() | ||
| 330 | + | ||
| 331 | + | ||
| 306 | 332 | class _RedirectStream(AbstractContextManager): | |
| 307 | 333 | ||
| 308 | 334 | _stream = None | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,5 +1,5 @@ | |||
| 1 | 1 | import asyncio | |
| 2 | - from contextlib import asynccontextmanager, AbstractAsyncContextManager, AsyncExitStack | ||
| 2 | + from contextlib import aclosing, asynccontextmanager, AbstractAsyncContextManager, AsyncExitStack | ||
| 3 | 3 | import functools | |
| 4 | 4 | from test import support | |
| 5 | 5 | import unittest | |
@@ -279,6 +279,63 @@ async def woohoo(self, func, args, kwds): | |||
| 279 | 279 | self.assertEqual(target, (11, 22, 33, 44)) | |
| 280 | 280 | ||
| 281 | 281 | ||
| 282 | + class AclosingTestCase(unittest.TestCase): | ||
| 283 | + | ||
| 284 | + @support.requires_docstrings | ||
| 285 | + def test_instance_docs(self): | ||
| 286 | + cm_docstring = aclosing.__doc__ | ||
| 287 | + obj = aclosing(None) | ||
| 288 | + self.assertEqual(obj.__doc__, cm_docstring) | ||
| 289 | + | ||
| 290 | + @_async_test | ||
| 291 | + async def test_aclosing(self): | ||
| 292 | + state = [] | ||
| 293 | + class C: | ||
| 294 | + async def aclose(self): | ||
| 295 | + state.append(1) | ||
| 296 | + x = C() | ||
| 297 | + self.assertEqual(state, []) | ||
| 298 | + async with aclosing(x) as y: | ||
| 299 | + self.assertEqual(x, y) | ||
| 300 | + self.assertEqual(state, [1]) | ||
| 301 | + | ||
| 302 | + @_async_test | ||
| 303 | + async def test_aclosing_error(self): | ||
| 304 | + state = [] | ||
| 305 | + class C: | ||
| 306 | + async def aclose(self): | ||
| 307 | + state.append(1) | ||
| 308 | + x = C() | ||
| 309 | + self.assertEqual(state, []) | ||
| 310 | + with self.assertRaises(ZeroDivisionError): | ||
| 311 | + async with aclosing(x) as y: | ||
| 312 | + self.assertEqual(x, y) | ||
| 313 | + 1 / 0 | ||
| 314 | + self.assertEqual(state, [1]) | ||
| 315 | + | ||
| 316 | + @_async_test | ||
| 317 | + async def test_aclosing_bpo41229(self): | ||
| 318 | + state = [] | ||
| 319 | + | ||
| 320 | + class Resource: | ||
| 321 | + def __del__(self): | ||
| 322 | + state.append(1) | ||
| 323 | + | ||
| 324 | + async def agenfunc(): | ||
| 325 | + r = Resource() | ||
| 326 | + yield -1 | ||
| 327 | + yield -2 | ||
| 328 | + | ||
| 329 | + x = agenfunc() | ||
| 330 | + self.assertEqual(state, []) | ||
| 331 | + with self.assertRaises(ZeroDivisionError): | ||
| 332 | + async with aclosing(x) as y: | ||
| 333 | + self.assertEqual(x, y) | ||
| 334 | + self.assertEqual(-1, await x.__anext__()) | ||
| 335 | + 1 / 0 | ||
| 336 | + self.assertEqual(state, [1]) | ||
| 337 | + | ||
| 338 | + | ||
| 282 | 339 | class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase): | |
| 283 | 340 | class SyncAsyncExitStack(AsyncExitStack): | |
| 284 | 341 | @staticmethod | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,3 @@ | |||
| 1 | + Add ``contextlib.aclosing`` for deterministic cleanup of async generators | ||
| 2 | + which is analogous to ``contextlib.closing`` for non-async generators. | ||
| 3 | + Patch by Joongi Kim and John Belmonte. | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments