-
-
Notifications
You must be signed in to change notification settings - Fork 33.2k
gh-125400: add async generator return value #125401
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: main
Are you sure you want to change the base?
Conversation
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
|
Thanks for writing a reference implementation for this idea as was requested in the Discourse thread; it makes it much easier to evaluate the proposal! I'm marking the PR as |
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
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.
Some initial comments. I do realize that a lot of this is copied from the StopIteration implementation, so it might be better to ignore some of my comments for consistency (or better yet, apply my comments to StopIteration as well 😄)
Overall, I think this is nice for consistency with StopIteration, but I don't think this is particularly useful without support for yield from in async generators, because you can implement this just fine in current versions--just define an exception with a value attribute, and raise it.
| return -1; | ||
| } | ||
| if (value == NULL) { | ||
| value = Py_NewRef(Py_None); |
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.
Py_None is immortal, I don't think we need to incref it. There's some contention about this, though.
| value = Py_NewRef(Py_None); | |
| value = Py_None; |
| PyObject *value = NULL; | ||
| if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) { | ||
| PyObject *exc = PyErr_GetRaisedException(); | ||
| value = Py_NewRef(((PyStopAsyncIterationObject *)exc)->value); | ||
| Py_DECREF(exc); | ||
| } else if (PyErr_Occurred()) { | ||
| return -1; | ||
| } |
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.
There's going to be a lot of internal reusing of _PyThreadState_GET with this approach. You can use the private API:
| PyObject *value = NULL; | |
| if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) { | |
| PyObject *exc = PyErr_GetRaisedException(); | |
| value = Py_NewRef(((PyStopAsyncIterationObject *)exc)->value); | |
| Py_DECREF(exc); | |
| } else if (PyErr_Occurred()) { | |
| return -1; | |
| } | |
| PyObject *value = NULL; | |
| PyThreadState *tstate = _PyThreadState_GET(); | |
| PyObject *occurred = _PyErr_Occurred(tstate); | |
| if (PyErr_GivenExceptionMatches(occurred, PyExc_StopAsyncIteration)) { | |
| PyObject *exc = _PyErr_GetRaisedException(tstate); | |
| value = Py_NewRef(((PyStopAsyncIterationObject *)exc)->value); | |
| Py_DECREF(exc); | |
| } else if (occurred) { | |
| return -1; | |
| } |
| if (value == NULL || | ||
| (!PyTuple_Check(value) && !PyExceptionInstance_Check(value))) | ||
| { | ||
| /* Delay exception instantiation if we can */ | ||
| PyErr_SetObject(PyExc_StopAsyncIteration, value); | ||
| return 0; | ||
| } | ||
| /* Construct an exception instance manually with | ||
| * PyObject_CallOneArg and pass it to PyErr_SetObject. | ||
| * | ||
| * We do this to handle a situation when "value" is a tuple, in which | ||
| * case PyErr_SetObject would set the value of StopIteration to | ||
| * the first element of the tuple. | ||
| * | ||
| * (See PyErr_SetObject/_PyErr_CreateException code for details.) | ||
| */ | ||
| e = PyObject_CallOneArg(PyExc_StopAsyncIteration, value); | ||
| if (e == NULL) { | ||
| return -1; | ||
| } | ||
| PyErr_SetObject(PyExc_StopAsyncIteration, e); | ||
| Py_DECREF(e); |
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.
Same story here--use the thread state.
| if (value == NULL || | |
| (!PyTuple_Check(value) && !PyExceptionInstance_Check(value))) | |
| { | |
| /* Delay exception instantiation if we can */ | |
| PyErr_SetObject(PyExc_StopAsyncIteration, value); | |
| return 0; | |
| } | |
| /* Construct an exception instance manually with | |
| * PyObject_CallOneArg and pass it to PyErr_SetObject. | |
| * | |
| * We do this to handle a situation when "value" is a tuple, in which | |
| * case PyErr_SetObject would set the value of StopIteration to | |
| * the first element of the tuple. | |
| * | |
| * (See PyErr_SetObject/_PyErr_CreateException code for details.) | |
| */ | |
| e = PyObject_CallOneArg(PyExc_StopAsyncIteration, value); | |
| if (e == NULL) { | |
| return -1; | |
| } | |
| PyErr_SetObject(PyExc_StopAsyncIteration, e); | |
| Py_DECREF(e); | |
| PyThreadState *tstate = _PyThreadState_GET(); | |
| if (value == NULL || | |
| (!PyTuple_Check(value) && !PyExceptionInstance_Check(value))) | |
| { | |
| /* Delay exception instantiation if we can */ | |
| _PyErr_SetObject(tstate, PyExc_StopAsyncIteration, value); | |
| return 0; | |
| } | |
| /* Construct an exception instance manually with | |
| * PyObject_CallOneArg and pass it to PyErr_SetObject. | |
| * | |
| * We do this to handle a situation when "value" is a tuple, in which | |
| * case PyErr_SetObject would set the value of StopIteration to | |
| * the first element of the tuple. | |
| * | |
| * (See PyErr_SetObject/_PyErr_CreateException code for details.) | |
| */ | |
| e = PyObject_CallOneArg(PyExc_StopAsyncIteration, value); | |
| if (e == NULL) { | |
| return -1; | |
| } | |
| _PyErr_SetObject(tstate, PyExc_StopAsyncIteration, e); | |
| Py_DECREF(e); |
| Py_ssize_t size = PyTuple_GET_SIZE(args); | ||
| PyObject *value; | ||
|
|
||
| if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) == -1) |
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.
Typically, the convention is to use < 0 rather than == -1
| if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) == -1) | |
| if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) < 0) |
| if (size > 0) | ||
| value = PyTuple_GET_ITEM(args, 0); | ||
| else | ||
| value = Py_None; | ||
| self->value = Py_NewRef(value); |
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.
This can get simplified a little, as None is immortal like I mentioned:
| if (size > 0) | |
| value = PyTuple_GET_ITEM(args, 0); | |
| else | |
| value = Py_None; | |
| self->value = Py_NewRef(value); | |
| if (size > 0) { | |
| self->value = Py_NewRef(PyTuple_GET_ITEM(args, 0)); | |
| } | |
| else { | |
| self->value = Py_None; | |
| } |
|
|
||
| if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) == -1) | ||
| return -1; | ||
| Py_CLEAR(self->value); |
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.
Why CLEAR it here? It's an extra operation of setting it to NULL when nothing will touch it in between calls here anyway.
| Py_CLEAR(self->value); | |
| Py_DECREF(self->value); |
| if (result == Py_None) { | ||
| PyErr_SetNone(PyExc_StopAsyncIteration); | ||
| } | ||
| else { | ||
| _PyGen_SetStopAsyncIterationValue(result); | ||
| } |
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.
Consider acquiring the thread state here, and then passing that to _PyGen_SetStopAsyncIterationValue (to omit an extra call there)
| async def gen(): | ||
| yield 1 | ||
| yield 2 | ||
| return 3 |
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 might be a good idea to add a dummy coroutine (something like asyncio.sleep(0) should work) here, as that needs to get yielded in a different way--in my experience with dealing with the coroutine implementation, a coroutine that awaits nothing behaves differently than one that does.
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 so much for your thorough and prompt review. I've updated this to include awaited calls to asyncio.sleep.
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
I believe the strongest argument in favor of this feature even without a Separately from the above, IIUC, @MadcowD might have been interested in writing a reference implementation for |
📚 Documentation preview 📚: https://cpython-previews--125401.org.readthedocs.build/