-
-
Notifications
You must be signed in to change notification settings - Fork 2
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
Suggestion: ASYNC119
when using an async context manager in an async generator
#211
Comments
Actually, I'm wondering if this is exactly |
Oh boy, this is much harder than it sounds - see https://peps.python.org/pep-0533/ and python-trio/trio#265 for some of the details. Additionally you can get similar issues if you My personal view is that async generators are simply too dangerous and too difficult to use correctly, and so at work we've banned them entirely via the
Overall, designing around channels instead of generators can feel clumsy at first, but I've found it leads to cleaner and safer designs. And unrelated but relevant, consider using |
nursery/cancelscope are anyio/trio constructs. I can try to clarify that in the docs. The proposed check here would be:
?
This is |
Yes to async context manager, defer to @Zac-HD on CancelScope
Yes it is! Thank you |
Oops, it took me long enough to write my comment that I missed Alice's - sorry! Agree that I'm planning to write up some more detail about each of our error codes in a long doc, hopefully this weekend, which will include much more about why you'd want to enable each of the optional rules in addition to the existing material. For |
OK, writing up some notes on what we can implement here. I'm still pretty dubious about having async generators at all, but if you've already either put
|
ASYNC119
when using an async context manager in an async generator
Just to check, is it not possible for the plugin to only prevent the user awaiting in finally/except blocks that did a yield in the try? I think the following is safe: async def foo():
while True:
foo = foo()
try:
await foo.aopen()
value = await foo.get_result()
finally:
await foo.aclose()
yield value (At this point, it wouldn't surprise me immensely to learn it's actually not safe, but if so I'd love to know why...)
I don't know what an iterable channel is, and people may not have come across asyncio.Queue, so it would be great if these could link to definitions.
Isn't returning an async generator which consumes from an async queue just as safe as returning that queue? (Asking because I own code that does that pattern.) |
Yeah, I think this is safe, though I'm not especially confident.
We should definitely put links in our docs; for the error messages I think using fully-qualified names and matching the error message to the library you're using would be sufficient. Trio and anyio use channels; it's basically decomposing the send-end and recieve-end of a queue into separate objects, and recieve-channels are async iterables.
IIRC the problems with this pattern are if you |
So the check to be implemented is:
And separately, ASYNC102 could be updated to only error if there's a
|
Check to be implemented: yes. I think #210 was the only change needed for |
I haven't checked if this actually passes or fails ASYNC102, but this is an example that the current text looks like it would reject: async def foo():
bar = Bar()
bar.aopen()
try
values = await bar.fetch()
finally:
await bar.aclose()
for value in values:
yield await baz(value) This would be totally fine, as there's no yield in the try block. |
Hmm. I think you can still be cancelled while waiting for More generally this looks like async def foo():
async with Bar() as bar:
values = await bar.fetch()
for value in values:
yield await baz(value) and avoid the lint warning with shorter and less-mistake-prone code. |
Does the cancellation not get raised in the fetch() call in this case though, allowing the cleanup to await safely? I think the root issue is only triggered when cancellation happens during a yield, resulting in cleanup running in the GC.
The finally could also be a catch. |
🤦♂️ my bad, I forgot that asyncio uses edge-triggered rather than anyio/trio's level-triggered cancellation semantics - in the latter, |
If the choice is between having it as-is and removing it completely, I'd say keep it. Better to catch real problems and leave the user to refactor or suppress edge cases.
Wow. That seems deeply problematic. I assume there are ways to schedule asynchronous cleanup code somehow though, even when cancelled? |
Yes, you can |
I could make it not warn on awaits inside
asyncio also has shields: https://docs.python.org/3/library/asyncio-task.html#asyncio.shield that I was going to add support for, but given that this check is not applicable for asyncio I don't think I should do that anymore and instead update the warning text that it can be ignored for asyncio. |
It is applicable, just too wide.
As long as the text is clear that it can only sometimes be ignored (and when), that SGTM
I don't think they're comparable -- a shield doesn't actually prevent the task being cancelled unless you also hold a hard reference to the shielded task. Honestly asyncio.shield is very strange, I can think of a bunch of extra rules to catch issues trying to use it. |
Given that the semantics are substantially different, I think I'd rather split the rule between the current trio/anyio support, and a new 3xx rule specific to asyncio. That's plausibly blocked on finding an asyncio-experienced contributor though, unless you'd be interested in joining the team? |
I can investigate whether that's possible with my current company (I work at Bloomberg), I know they have a process. |
Uh, looking at ASYNC101 for #215 I think ASYNC119 will warn on all instances of ASYNC101 except for sync context managers - of which I can only think of |
I think we should keep both; they're conceptually distinct. e.g. PEP-533 would resolve ASYNC119, but not 101 (that's a at-some-point-forthcoming PEP, cf this thread). And concretely, some users will want to suppress 119 but really shouldn't disable 101. |
cool. I'll see about adding some text to that effect in the docs |
ASYNC119 is added, and the docs have been updated to mention the distinction between 101 and 119. So this issue can be closed, unless we wanna keep it for an eventual 3xx. I'd personally love to have 3xx/etc defined in their own separate issues(s) to try and keep things straight. |
Looks great! I think I've also gotten PEP-789 (python/peps#3782) to the point where it'd make sense to mention that in the I also opened #257, though it's not going to be a priority for me since I don't use asyncio 🙂 |
Antipattern: The following code is unsafe:
Explanation: If the iterator is not fully consumed, the cleanup path will only be triggered by the garbage collector, which will not allow the async cleanup logic to
await
(it will raiseGeneratorExit
when it tries). A similar issue arises if you try toawait
in afinally
block in an async generator, or anexcept
block that can interceptasyncio.exceptions.CancelledError
.The only exception to this that I'm aware of is if the async generator is decorated with
@contextlib.asynccontextmanager
.Fix: I think unfortunately the only fix is to refactor the API to separate the context management from the iteration, e.g.
I'm not aware of any existing lint check for this antipattern (I may have missed it though!) — do the maintainers of this repo think it would be valuable?
The text was updated successfully, but these errors were encountered: