use PyErr_SetRaisedException in raise_lazy#5876
use PyErr_SetRaisedException in raise_lazy#5876bschoenmaeckers wants to merge 3 commits intoPyO3:mainfrom
PyErr_SetRaisedException in raise_lazy#5876Conversation
c155ece to
b727b3d
Compare
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks for working on this! I've wanted to move forward with this for some time, it will allow us to (hopefully) improve the exception story piece by piece.
I think last time I looked at this I thought it was hard. See #4669 where I once pushed something similar, I forget why exactly I never got back to finishing it off...
Reading through #4669 makes me remember we have safe api's to do things like this. 😅 |
davidhewitt
left a comment
There was a problem hiding this comment.
I think as long as we're confident that the subtle difference with exception chaining will not cause us problems, I'm happy to proceed with this.
src/err/err_state.rs
Outdated
| #[cfg(not(Py_3_12))] | ||
| ffi::PyErr_SetObject(ptype.as_ptr(), pvalue.as_ptr()); |
There was a problem hiding this comment.
To be more sure that the difference in exception chaining on the legacy path can't lead to diverging behavior, we could change the legacy path to call PyErr_Restore after creating a fully normalized exception object?
There was a problem hiding this comment.
I think it is valuable to compare the PyErr_SetObject behaviour in our test suite so can verify our code path with PyErr_SetRaisedException is correct.
5c6319e to
0648d71
Compare
|
Tests are passing, so could you please take another look. |
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, good to test all cases here and be sure we're not accidentally regressing something. Looks like this is shaping up well, just a few smaller thoughts...
| let context = err.context(py).unwrap(); | ||
| assert!(context.is_instance_of::<PyRuntimeError>(py)) | ||
| }) | ||
| } |
There was a problem hiding this comment.
I wonder if we should have a test which directly compares the result against against ffi::PyErr_SetObject so that we can keep up to date with any future changes too?
There was a problem hiding this comment.
I've added a test that compare's all the different variants I'm aware of.
77314c1 to
6f55717
Compare
Add context test fix context test & use safe api's Changelog Remove `compat::PyObject_CallOneArg` fix clippy Add test for 'PyErr::set_context' Return `Bound<'py, PyBaseException>` from `create_normalized_exception`
6f55717 to
6ef5328
Compare
8c9ec99 to
8194fe0
Compare
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, I would like to see a couple more tests around the context case when comparing to PyErr_SetObject, and then hopefully we're good to merge 👍
| let err2 = PyErr::from_value(exc2.into_any()); | ||
|
|
||
| assert!(err1.get_type(py).is(err2.get_type(py))); | ||
| assert!(err1.context(py).xor(err2.context(py)).is_none()); |
There was a problem hiding this comment.
I guess there is another test where if we call PyErr_SetHandledException first, we expect that value to be placed in context? Looking at PyErr_SetObject implementation, there is some logic to avoid creating cycles in the context too, I feel like we should probably be careful to behave similarly?
There was a problem hiding this comment.
I wonder, thinking about the context cycles, does that imply this approach is getting too complex? Maybe there is an alternative where we take control of the exception object creation as this PR already does (as that'll be helpful for improving PyErr::new as well as eventually removing the "lazy" state as per #4859), but we continue to use PyErr_SetObject to populate the interpreter state? Not as efficient as PyErr_SetRaisedException but would guarantee that we always use the standard behavior.
There was a problem hiding this comment.
Maybe we should just be calling the new create_normalized_exception directly from PyErrStateInner::normalize, as a first step towards #4859? (And avoid touching the interpreter's exception state at all.)
I think that would be slightly breaking in that creating a PyErr would no longer automatically pick up any __context__ (you'd have to actually call PyErr::restore to do that). But maybe that's actually more correct?
There was a problem hiding this comment.
(Sorry to keep having slightly new / different ideas here.)
There was a problem hiding this comment.
but we continue to use PyErr_SetObject to populate the interpreter state? Not as efficient as PyErr_SetRaisedException but would guarantee that we always use the standard behavior.
Could be an option, I will play around with cycle detection first to see if it's that horrible to implement. Maybe I will find a better solution on the way.
This fixes a TODO in
raise_lazy