-
-
Couldn't load subscription status.
- Fork 2.1k
Accept async context managers for cleanup contexts (#11681) #11704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
7eaad2f
795bd0a
91e83fe
4c6781d
a3488da
dd1fe24
32a64ba
57802d7
6bd76a2
b830b7e
fba2864
e38685c
8186737
8291fa9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| Started accepting :term:`asynchronous context managers <asynchronous context manager>` for cleanup contexts. | ||
| Legacy single-yield :term:`asynchronous generator` cleanup contexts continue to be | ||
| supported; async context managers are adapted internally so they are | ||
| entered at startup and exited during cleanup. | ||
|
|
||
| -- by :user:`MannXo`. | ||
Dreamsorcerer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import contextlib | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import warnings | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from collections.abc import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -140,7 +141,7 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init_subclass__(cls: type["Application"]) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise TypeError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Inheritance class {cls.__name__} from web.Application " "is forbidden" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Inheritance class {cls.__name__} from web.Application is forbidden" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # MutableMapping API | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -416,33 +417,102 @@ def exceptions(self) -> list[BaseException]: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _CleanupContextBase = FrozenList[Callable[[Application], AsyncIterator[None]]] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # cleanup contexts may be either async generators (async iterator) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # or async context managers (contextlib.asynccontextmanager). For | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # the purposes of type checking we keep the callback return type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # permissive (Any) so that downstream checks which attempt to | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # detect return value shapes at runtime are not considered | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # unreachable by the type checker. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _CleanupContextBase = FrozenList[Callable[[Application], Any]] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _CleanupContextBase = FrozenList | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class CleanupContext(_CleanupContextBase): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super().__init__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._exits: list[AsyncIterator[None]] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # _exits stores either async iterators (legacy async generators) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # or async context manager instances. On cleanup we dispatch to | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # the appropriate finalizer (``__anext__`` for iterators and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ``__aexit__`` for context managers). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._exits: list[object] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def _on_startup(self, app: Application) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Run registered cleanup context callbacks at startup. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Each callback may return either an async iterator (an async | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| generator that yields exactly once) or an async context manager | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (from :func:`contextlib.asynccontextmanager`). If a context manager | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is returned it will be entered on startup (``__aenter__``) and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exited during cleanup (``__aexit__``). Legacy single-yield async | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| generator cleanup contexts continue to be supported. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for cb in self: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it = cb(app).__aiter__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await it.__anext__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._exits.append(it) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Call the registered callback and inspect its return value. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If the callback returned a context manager instance, use it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # directly. Otherwise (legacy async generator callbacks) we | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # convert the callback into an async context manager and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # call it to obtain a context manager instance. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx = cb(app) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If the callback returned an async iterator (legacy async | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # generator), use it directly. If it returned an async | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # context manager instance, enter it and remember the manager | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # for later exit. As a final fallback, convert the callback | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # into an async context manager (covers some edge cases) and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # enter that. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(ctx, AsyncIterator): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Legacy async generator cleanup context: advance it once | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # (equivalent to entering) and remember the iterator for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # finalization. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it = cast(AsyncIterator[None], ctx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await it.__anext__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._exits.append(it) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif isinstance(ctx, contextlib.AbstractAsyncContextManager): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If ctx is an async context manager: enter it and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # remember the manager for later exit. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cm = ctx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await cm.__aenter__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._exits.append(cm) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # cb may have a broader annotated return type; adapt the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # callable into an async context manager and enter it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cm = contextlib.asynccontextmanager( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cast(Callable[[Application], AsyncIterator[None]], cb) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )(app) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await cm.__aenter__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._exits.append(cm) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+459
to
+485
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I mentioned originally, can't this be reduced to just this?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to reduce it to your suggested version, but I think it changes semantics in a way that breaks cleanup-context lifecycle invariants. We end up with two generator instances because we also call For async-generator style cleanup callbacks this yields two generator instances. The instance you advance on startup is not the same instance you finalize on cleanup. I might have missunderstood your comment. Did you intend to change semantics so that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, I had realised that, but not sure it causes any actual problems. We can then probably deprecate the generator fallback in future to remove this, after a couple of years.
That shouldn't be true. The second instance replaces the first one before it is used. So, if the first instance is a generator object, it gets discarded without being used at all. The second instance is then used for both startup and cleanup. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def _on_cleanup(self, app: Application) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errors = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for it in reversed(self._exits): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Run cleanup for all registered contexts in reverse order. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Collects and re-raises exceptions similarly to previous | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| implementation: a single exception is propagated as-is, multiple | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exceptions are wrapped into CleanupError. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errors: list[BaseException] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for entry in reversed(self._exits): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await it.__anext__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except StopAsyncIteration: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(entry, AsyncIterator): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Legacy async generator: expect it to finish on second | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # __anext__ call. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await cast(AsyncIterator[None], entry).__anext__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except StopAsyncIteration: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errors.append( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RuntimeError(f"{entry!r} has more than one 'yield'") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif isinstance(entry, contextlib.AbstractAsyncContextManager): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If entry is an async context manager: call __aexit__. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await entry.__aexit__(None, None, None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Unknown entry type: skip but record an error. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errors.append(RuntimeError(f"Unknown cleanup entry {entry!r}")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except (Exception, asyncio.CancelledError) as exc: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errors.append(exc) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errors.append(RuntimeError(f"{it!r} has more than one 'yield'")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if errors: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(errors) == 1: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise errors[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.