-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
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
bpo-41229: Update docs for explicit aclose()-required cases and add contextlib.aclosing() method #21545
Conversation
Adding (I'm not currently +1 or -1 for the feature, I have to spend some more time considering the use cases.) |
I think the major issue for There are currently the following approaches I see:
So, from the perspective of naming, adding Still, I believe that we need a better approach so that people don't be confused about resource cleanup upon generator termination/interruption, and currently the only reliable and predictable way is to explicitly close the generator (for both sync/async generators). Having both For the @njsmith's response to the original issue, I'd like to say relying on the event loop's behavior or GC is counter-intuitive. For instance, even an experienced developer like him was surprised at his first glance on this issue and after some examination he concluded that this was "actually" an expected behavior. I don't know how things are going on with PEP-533, but if it's going to be accepted in the future, I can agree with skipping |
We're going to start prompting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 from me to add this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aeros
Anything more stuff is needed before merging this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly LGTM. A few nits though:
-
The docs mention
aclosing
as a function, when inLib/contextlib.py
it's implemented as a class. As a result, it should use the.. class::
reST role instead of.. function::
. Also, instead of "return an async context manager", it would be more correct to directly call it an async context manager [1]. -
In the docstring for
aclosing()
, I would suggest for it to be changed to something like: "Async context manager for safely finalizing an async generator using its aclose() method". I think the current version of the docstring may cause readers to miss the main point of it, and potentially make incorrect assumptions about its usage. Also, the "end of a block" part is a bit vague [2]. Instead, it makes more sense to me to just refer to it as a context manager. -
I think this would also benefit from a "What's New" entry in 3.10 for
contextlib
to briefly mention the addition ofcontextlib.aclosing()
, as it is a new public member.
Other than the above points, I think it's good to go. Feel free to @ mention me once the changes are made, and I can proceed with merging it (unless @1st1 is 100% content with the current state, and decides to merge it as is).
[1] - Any class that implements __aenter__
and __aexit__
is considered an async context manager, or at the very least is usable as one.
[2] - I get that you're referring to the block/scope within the with statement, but I don't think that will be immediately clear to all readers; especially if they're not intimately familiar with context managers in general. By simply referring to it as an "async context manager", readers can much more easily look up how context managers work rather than trying to guess what "context" and "block" are referring to within the docstring.
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
@aeros Thanks for your feedback. Updated the PR, but I have some questions regarding existing docs/docstrings for
I updated the docs to use
Agreed and applied your suggestion, but the same question: shall I update the docstring for existing
I have already added the news fragment using blurb.
|
Neglecting to mention https://bugs.python.org/issue40213 in the PR description has cost me several hours of duplicated effort in #22640 😦 Anyway I'll add comments here based on thought I've put into it. |
Doc/library/contextlib.rst
Outdated
from contextlib import asynccontextmanager | ||
|
||
@asynccontextmanager | ||
def aclosing(thing): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be async def
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
Doc/library/contextlib.rst
Outdated
And lets you write code like this:: | ||
|
||
async def ticker(delay, to): | ||
for i in range(to): | ||
yield i | ||
await asyncio.sleep(delay) | ||
|
||
with aclosing(ticker(10)) as ticks: | ||
async for tick in ticks: | ||
print(tick) | ||
|
||
without needing to explicitly call the ``aclose()`` method of ``ticks``. | ||
Even if an error occurs, ``ticks.aclose()`` will be called when | ||
the :keyword:`async with` block is exited. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think rather than some vague example, this is the right place to mention how it's useful for deterministic async generator cleanup, and use that as an example.
See https://github.com/python/cpython/pull/22640/files#diff-aa53aa6efed705fd0589ce0fdac405d7R171-R185
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced the example with #22640!
Doc/reference/expressions.rst
Outdated
If an asynchronous generator is not resumed before it reaches the end of function | ||
but the event loop where the generator is bound to continues to run (e.g., when | ||
the caller task is cancelled), the caller must explicitly close the async | ||
generator by calling :meth:`~agen.aclose` method to free the resources allocated | ||
in the async generator function's stack and detach the generator from the event | ||
loop. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be explained more clearly-- most readers won't have in mind how this situation comes up. And the rationale could be more detailed.
If an asynchronous generator happens to exit early by :keyword:
break
, the caller task being cancelled, or other exception, 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 on or the event loop itself. 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Subtlety: It can't happen after the event loop exits, thanks to the async gen gc hooks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated!
|
||
is equivalent to this: | ||
|
||
agen = <module>.fetch(<arguments>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
await fetch()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
Add ``contextlib.aclosing`` analogous to ``contextlib.closing`` but for | ||
async generators and arbitrary objects with an ``aclose`` coroutine method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be useful to mention the primary motivation (deterministic cleanup of async generators)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated!
Sounds good, I'll look through it as soon as I get the chance. If I'm not able to by this weekend, I'll definitely be able to during the core sprint next week. |
Lib/contextlib.py
Outdated
@@ -303,6 +303,31 @@ def __exit__(self, *exc_info): | |||
self.thing.close() | |||
|
|||
|
|||
class aclosing(AbstractAsyncContextManager): | |||
"""Async context manager for safely finalizing an async generator using its ``aclose()`` method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this description too specific? aclosing
can be used with any object having aclose()
, not necessarily a generator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated a little to embrace other cases.
Lib/contextlib.py
Outdated
|
||
is equivalent to this: | ||
|
||
agen = await <module>.fetch(<arguments>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I said the await
should be here, but that was incorrect.
The object passed to aclosing()
is just taken as is, returned by __aenter__
, and has aclose()
called in __aexit__
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I also missed to check it. Reverted here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aeros Gentle ping~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the reminder @corona10, for the suggestions @belm0, and for the changes made @achimnol! LGTM, I'll proceed with merging.
(The only remaining component that would also be nice is a mention of the new contextlib.aclosing()
in the 3.10 whatsnew document. But that can be done in a separate PR -- if you open one, I'd suggest linking to it here and requesting a review from me)
…ontextlib.aclosing() method (pythonGH-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/ * https://github.com/achimnol/aiotools/blob/ef7bf0cea7af/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 is a PR to:
contextlib.aclosing
which ia analogous tocontextlib.closing
but for async-generators with an explicit test case for bpo-41229aclose()
invocation.which are motivated by the following issues, articles, and examples:
Particuarly regarding PEP-533, its acceptance (
__aiterclose__()
) would make this little addition ofcontextlib.aclosing()
unnecessary for most use cases, but until then this could serve as a good counterpart and analogy tocontextlib.closing()
. The same applies forcontextlib.closing
with__iterclose__()
.Also, still there are other use cases, e.g., when working with non-generator objects with
aclose()
methods.https://bugs.python.org/issue41229
Automerge-Triggered-By: GH:aeros