Reputation: 114440
I have a bit of code that tries to parse an object as an integer:
long val = PyLong_AsLong(obj);
if(val == -1 && PyErr_Occurred()) {
return -1;
}
Here obj
is a vanilla PyObject *
, and PyLong_AsLong
raises a very generic TypeError
if obj
is not an integer.
I would like to transform the error message into something a bit more informative, so I would like to either modify the existing error object, or to reraise it.
My current solution is to do this:
long val = PyLong_AsLong(obj);
if(val == -1 && PyErr_Occurred()) {
PyErr_Clear();
PyErr_Format(PyExc_TypeError, "Parameter must be an integer type, but got %s", Py_TYPE(obj)->tp_name);
return -1;
}
Is this the proper way to reraise an error? Specifically,
PyErr_Clear
at all? I suspect that it properly decrefs the existing exception object, but I'm not sure.raise new_err from old_err
?I am not sure how to use PyErr_SetExcInfo
for this situation, although my gut tells me it may be relevant somehow.
Upvotes: 5
Views: 1325
Reputation: 281476
Your existing code is fine, but if you want to do the equivalent of exception chaining, you can. If you want to skip to how to do that, jump to point 3 near the end of the answer.
To explain how to do things like modify a propagating exception or perform the equivalent of raise Something() from existing_exception
, first, we'll have to explain how exception state works at C level.
A propagating exception is represented by a per-thread error indicator consisting of a type, value, and traceback. That sounds a lot like sys.exc_info()
, but it's not the same. sys.exc_info()
is for exceptions that have been caught by Python-level code, not exceptions that are still propagating.
The error indicator may be unnormalized, which basically means that the work of constructing an exception object hasn't been performed, and the value in the error indicator isn't an instance of the exception type. This state exists for efficiency; if the error indicator is cleared by PyErr_Clear
before normalization is needed, Python gets to skip much of the work of raising an exception. Exception normalization is performed by PyErr_NormalizeException
, with a bit of extra work in PyException_SetTraceback
to set the exception object's __traceback__
attribute.
PyErr_Clear
is sort of like the C equivalent of an except
block, but it just clears the error indicator, without letting you inspect much of the exception information. To catch an exception and inspect it, you'd want PyErr_Fetch
. PyErr_Fetch
is like catching an exception and examining sys.exc_info()
, but it doesn't set sys.exc_info()
or normalize the exception. It clears the error indicator and gives you the raw contents of the error indicator directly.
Explicit exception chaining (raise Something() from existing_exception
) works by going through PyException_SetCause
to set the new exception's __cause__
to the existing exception. This requires exception objects for both exceptions, so if you want to do the equivalent from C, you'll have to normalize the exceptions and call PyException_SetCause
yourself.
Implicit exception chaining (raise Something()
in an except
block) works by going through PyException_SetContext
to set the new exception's __context__
to the existing exception. Similar to PyException_SetCause
, this requires exception objects and exception normalization. raise Something() from existing_exception
inside an except
block actually sets both __cause__
and __context__
, and if you want to perform explicit exception chaining at C level, you should usually do the same.
PyErr_Format
and other functions that set the error indicator will clear the error indicator first if it's already set, but this isn't documented for most of them.message
attribute, but this won't affect args
or anything else the exception class might do with its arguments, and that could lead to weird problems. Alternatively, you could fetch the error indicator with PyErr_Fetch
and restore it with a new string for the value with PyErr_Restore
, but that will throw away an existing exception object if there is one, and it makes assumptions about the exception class's signature.Yeah, that's possible, but doing it through public C API functions is pretty awkward and manual. You'd have to manually do a lot of normalization, unraising, and raising exceptions.
There are efforts to make C-level exception chaining more convenient, but so far, the more convenient functions are all considered internal. For example, _PyErr_FormatFromCause
is like PyErr_Format
, but it chains the new exception off of an existing, propagating exception (through both __context__
and __cause__
.
I wouldn't recommend calling it directly for now; it's very new (3.6+), and it's very likely to change (specifically, I would be unsurprised to see it lose its leading underscore in a new Python version). Instead, copying the implementation of _PyErr_FormatFromCause
/_PyErr_FormatVFromCause
(and respecting the license) is a good way to make sure you have the fiddly bits of normalization and chaining right.
It's also a useful reference to work from if you want to perform implicit (__context__
-only) exception chaining at C level - just remove the part that handles __cause__
.
Upvotes: 7