-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
py/objgenerator: do not allow pend_throw to an unstarted generator. #5288
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
Conversation
pend_throw() to unstarted generators prevents catching the injected exception by the generator code.
Please provide a code example of how you were able to catch all exceptions before the changes in #5242 because I couldn't think of any. But judging from your comments, your changes are incompatible to CPython behaviour and therefore shouldn't be implemented. It would make creating code more confusing. |
@dpgeorge @jimmo , I looked into restoring the original behaviour wrt. exceptions (i.e. always raise exceptions on Right now I am favouring leaving |
So, #5275 did change behaviour of But as I see it If anything, what could be added is a method which lets one test if a generator is already started or not. That would be more powerful than before #5275 |
Thank you for the reply @dpgeorge. Given we do not have a better solution for this issue yet, can 5578182 please be reverted (or this PR applied if you think it is ok) to restore functionality to existing code first and then (hopefully soon) a better solution can be found and applied? About the issue which resulted in the creation of #5275:
I see that as a lesser issue with #5275 IMO. I think the loss of precise and unconditional exception handling within generators is a lot worse: besides breaking code that relies on being able to catch all exceptions injected by pend_throw(), it also prevents writing simpler more compact code with generators because now code using # Different generators do different things in response to different exceptions and their internal state
def gen1():
try:
while True:
baz = yield
except CanceledError:
do_gen1_A()
except SomeOtherError1:
do_gen1_B()
finally:
do_gen1_C()
def gen2():
try:
while True:
baz = yield
except CanceledError:
do_gen2_A()
except SomeOtherError1:
do_gen2_B()
finally:
do_gen2_C()
# Before we could write something generic to inject exceptions
def signal_gen(exc):
gs = [gen1(), gen2()]
for g in gs:
try:
g.pend_throw(exc)
except TypeError:
next(g)
g.pend_throw(exc)
try:
next(g)
except Exception as e:
# check and do something based on e
else:
# Exception swallowed by the generator, do something else
# Now a function coupled with each generator has to be written.
def signal_gen1(g, exc):
try:
g.pend_throw(exc)
next(g)
except CanceledError:
do_gen1_A()
except SomeOtherError1:
do_gen1_B()
finally:
do_gen1_C()
def signal_gen2(g, exc):
try:
g.pend_throw(exc)
next(g)
except CanceledError:
do_gen2_A()
except SomeOtherError1:
do_gen2_B()
finally:
do_gen2_C()
# Additionally when the signaling functions below get an exception
# they have no way to know if the generator already handled the
# exception or not.
# Adding a function to retrieve the state of the generator would allow
# the signaling function to decide if the generator had a chance to process
# the exception or not. Before with just Now instead, I do not see how the current behaviour can be more powerful than the previous one when it needs two functions to get close to doing the job of the previous one and is still not capable to express what was covered by the previous single
I cannot say I agree for the following reasons:
Please see my comment above about why I think current semantics + started gen test is inferior to pre #5275 semantics. |
How would you write that code in CPython? |
CPython has only So something like this would be ~ equivalent to the code above with post #5275 behaviour: def signal_gen1(g, exc):
try:
g.throw(exc)
except CanceledError:
do_gen1_A()
except SomeOtherError1:
do_gen1_B()
finally:
do_gen1_C()
def signal_gen2(g, exc):
try:
g.throw(exc)
except CanceledError:
do_gen2_A()
except SomeOtherError1:
do_gen2_B()
finally:
do_gen2_C() Note that This micropython/micropython-lib#215 (comment) makes for an interesting reading. It may also be interesting to @dpgeorge. Warning: stream of consciousness within. The content of that comment (or the whole thread) doesn't provide a full picture or reference to what |
Interesting, thank you for providing the code examples. I now understand your case a lot better. You were going on about uasyncio so much when you are actually using bare generators and not the uasyncio API. (Maybe you have it mixed in your code of course but the issue you have seems to be solely with using bare generators because even "try: asyncio.cancel(coro) except TypeError: next(coro) ... " is actually a low level workaround and not uasyncio API, therefore the change to pend_throw didn't break uasyncio behaviour at all).
I still don't see why micropython should follow CPython's description instead of the actual behaviour that is not raised as an issue. People expect micropython to behave the same. |
Negative, I realise there is really no documentation about
uh? Does not compute 🤷♀ The workaround you mention is not a workaround at all and the consequent in your statement (the part after the The bottom line of my dive into |
Let's say there's a new generator method called def old_pend_throw(g, v):
if g.is_running():
return g.pend_throw(v)
else:
raise TypeError('can't pend throw to just-started generator') I don't see what other capabilities this is missing? |
Does Issues with Old
|
I think there are three points here (@dxxb please correct me if I'm not summarizing this correctly)
I think you're referring to https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel which doesn't (to my interpretation) say anything about what happens if the generator hasn't started yet. It seems reasonable that the (trivially verifiable) behavior of not starting the generator is compatible with this documentation. Why would you start a generator just so you immediately have to make it clean itself up?
The second part is fairly straightforward (that if a cancellation exception occurs that it must happen in the event loop), but the assumption by uasyncio that the generator must always reach the first yield is quite subtle. It's clear that an already started coroutine must be resumed (obviously -- not just for uasyncio's assumptions but for cleanup etc), and as per micropython/micropython-lib#215 it's even more complicated when you have IO-blocked tasks. But a not-started coroutine is not IO-blocked. It'd be really useful if you could summarise why the new behavior of pend_throw breaks uasyncio's assumptions for not-started coroutines? I can see how it's a breaking change if you assumed that all queued tasks would always attempt to run, but I'm missing how this change fundamentally breaks uasyncio. But there's a lot of history here, between the three current threads and micropython/micropython-lib#215. Seeing as you're making this point I'm guessing you've seen something that I've missed. A concise summary would be really useful in figuring out how to proceed.
I see you've already seen #4217 (comment) |
Sorry for asking again: The generator:
CPython:
Exception on non started generator isn't caught by the generator. Micropython:
It behaves exactly like CPython. You can do the same micropython code with pend_throw and it will still behave in exactly the same way, with the difference that all exceptions are only handled when calling next(g):
So the behaviour of micropython is now the same as CPython unless I'm missing something. To me pend_throw seems to be implemented for uasyncio to handle the exceptions when the coro is scheduled instead of immediately to "mimic" the behaviour of CPython coroutines. |
Hi @jimmo
I do not think we need or should look at CPython to justify or forbid changes to the underlying mechanism used by
Nonetheless, if you plan to work on https://github.com/python-trio/trio/blob/master/docs/source/design.rst
The complete relevant text:
This line:
makes current CPython The paragraph starting with:
makes current CPython In both cases our point of disagreement is summed up by your phrase Those general statements in the docs makes sense for two reasons IMO:
Phrasing the issue this way artificially limits the cases you are considering. To set the stage:
So back to your question: once the tasks is scheduled it can be cancelled at any time. Sure, most of the times it won't be cancelled immediately, but it can and for correctness that case must be handled the same way as any other cancellation. The caller of
The old behaviour of
As you say, given that However, the behaviour enforced by old Regardless of the fate of Note that in event loop shutdown a form of hard cancelling may be required for non-started generator but we always had
So far I reacted to your last comment on that issue but I haven't even looked at what that issue is about (brain cycles: exhausted) but now I am afraid to look! 😆 |
You cannot catch exceptions in non-started generators in CPython and CPython's asyncio wraps generators to turn them into tasks.
This is not my my wish: I have explained elsewhere what I believe its purpose to be and why.
The first is explains why The second cannot be explained away as a bug or an oversight on @pfalcon's part because @pfalcon's comments show it was deliberate and because it required extra code to implement compared to letting You mentioned feature 1 in your comment but you left out feature 2 which IMO shows how the intent was deliberately and precisely to only raise exceptions on |
@dpgeorge , @jimmo any comment about #5288 (comment) , #5288 (comment) and #5288 (comment) ? @dpgeorge I am happy to create a PR on micropython-lib to copy existing |
The new version (v3) of uasyncio has been in use (and in production code) for some time now and has shown that cancellation is a very tricky thing to get right, and it works well in uasyncio v3. So |
…ixel_fix Update Adafruit_CircuitPython_NeoPixel commit to 0f4661c
pend_throw() to unstarted generators prevents catching the injected
exception by the generator's code breaking existing code that relies on
being able to catch all exceptions. See #5242 (comment)