-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Simplify error_already_set #954
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
57e5695
to
d7ac1f3
Compare
The extra work for exception clearing originates and is explained in 099d6e9. The first part about acquiring the GIL in The same commit also makes mention of always using So I'd say the |
Ah, right, I suppose the |
@wjakob - do you recall what the motivation was to always match |
d7ac1f3
to
b7825c5
Compare
IIRC the restore & clear on destruction was just paranoia regarding reference counting, I wasn't sure if decreasing the refcounts of the three objects representing the exception would be enough. Another part that took a few iterations to work correctly is the propagation of stack traces through nested calls with C++ -> Python -> C++ -> .. transitions. As long as that still works with the proposed change, I'm fine with it. |
b7825c5
to
1ad21a7
Compare
I've added some tests for the exception round-tripping, which seems to work as expected. |
I would very much like to recommend against the I think we should be working in the opposite direction and make sure that |
The point about not exposing it publically makes sense to me. In this particular case, though, I think I'd need either a Another alternative is to make |
Edit: The existance of |
More than you would think: I just tried it to see if it works and it segfaults: the |
`error_already_set` is more complicated than it needs to be, partly because it manages reference counts itself rather than using `py::object`, and partly because it tries to do more exception clearing than is needed. This commit greatly simplifies it, and fixes pybind#927. Using `py::object` instead of `PyObject *` means we can rely on implicit copy/move constructors. The current logic did both a `PyErr_Clear` on deletion *and* a `PyErr_Fetch` on creation. I can't see how the `PyErr_Clear` on deletion is ever useful: the `Fetch` on creation itself clears the error, so the only way doing a `PyErr_Clear` on deletion could do anything if is some *other* exception was raised while the `error_already_set` object was alive--but in that case, clearing some other exception seems wrong. (Code that is worried about an exception handler raising another exception would already catch a second `error_already_set` from exception code). The destructor itself called `clear()`, but `clear()` was a little bit more paranoid that needed: it called `restore()` to restore the currently captured error, but then immediately cleared it, using the `PyErr_Restore` to release the references. That's unnecessary: it's valid for us to release the references manually. This updates the code to simply release the references on the three objects (preserving the gil acquire). `clear()`, however, also had the side effect of clearing the current error, even if the current `error_already_set` didn't have a current error (e.g. because of a previous `restore()` or `clear()` call). I don't really see how clearing the error here can ever actually be useful: the only way the current error could be set is if you called `restore()` (in which case the current stored error-related members have already been released), or if some *other* code raised the error, in which case `clear()` on *this* object is clearing an error for which it shouldn't be responsible. Neither of those seem like intentional or desirable features, and manually requesting deletion of the stored references similarly seems pointless, so I've just made `clear()` an empty method and marked it deprecated. This also fixes a minor potential issue with the destruction: it is technically possible for `value` to be null (though this seems likely to be rare in practice); this updates the check to look at `type` which will always be non-null for a `Fetch`ed exception. This also adds error_already_set round-trip throw tests to the test suite.
1ad21a7
to
5273eef
Compare
error_already_set
is more complicated than it needs to be, partly because it manages reference counts itself rather than usingpy::object
, and partly because it tries to do more exception clearing than is needed. This commit greatly simplifies it, and (hopefully) fixes #927.I hope I'm not overlooking something, but I think the simplifications I'm making here make sense.
First, using
py::object
instead ofPyObject *
means we can rely on implicit copy/move constructors. (This necessitated a move intopytypes.h
, but that doesn't seem a terrible fit given the purpose oferror_already_set
).The current logic did both a
PyErr_Clear
on deletion and aPyErr_Fetch
on creation. I can't see how thePyErr_Clear
on deletion is ever useful: theFetch
on creation itself clears the error, so the only way doing aPyErr_Clear
on deletion could do anything if is something else set the current python exception while theerror_already_set
object was alive--but in that case, clearing some potentially-unrelated other exception seems wrong. (Code that is worried about an exception handler raising another exception would already catch a seconderror_already_set
from exception code).The destructor itself called
clear()
, butclear()
was a little bit convoluted: it calledrestore()
to restore the currently captured error, but then immediately cleared it withPyErr_Clear();
. The only point seems to be becausePyErr_Restore
decrements the reference counts on the held objects, but we can simply do that manually and avoid restoring the error just to clear it again. Or, actually, we can just not decrement them at all and let them be unreferenced (via thepy::object
destructor) when theerror_already_set
is itself destroyed.clear()
also had the side effect of clearing the current error, even if the currenterror_already_set
didn't have a current error (e.g. because of a previousrestore()
orclear()
call). I don't really see how clearing the error here is ever the right thing to do: since the error is cleared on construction, the only way there could be a current error is if you calledrestore()
(in which case the current stored error-related members have already been released), or if some other code raised the error, in which caseclear()
on this object is clearing an error for which it shouldn't be responsible.Neither of those seem like intentional or desirable features, and manually requesting deletion of the stored references similarly seems pointless, so I've just made
clear()
an empty method and marked itdeprecated in case any code out there is calling it.