Repository navigation
PEP 678: Make it possible to enrich an exception's error message #89770
Description
Activity
The requirement comes from Hypothesis, see
#28569 (comment)It is necessary there to add a note to an exception describing which test case it comes from. The note should be printed by __str__ of this exception.
class Explanation(Exception): __module__ = "builtins" def __str__(self) -> str: return f"\n{self.args[0]}"
try:
why = "Failed!"
raise AssertionError(why)
except Exception as e:
msg = " You can reproduce this error by ...\n ..."
raise Explanation(msg) from e# Ideally something more like: e.__note__ = msg raise
- addedinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)type-featureA feature request or enhancementA feature request or enhancement
on Oct 25, 2021 This code shows my current best workaround based on a wrapper exception, with the traceback below annotating the additional details that I'd prefer to omit for clarity:
$ python example.py Traceback (most recent call last): File "example.py", line 8, in <module> raise AssertionError(why) AssertionError: Failed! # These lines are The above exception was the direct cause of the following exception: # confusing for new # users, and they Traceback (most recent call last): # only exist due File "example.py", line 10, in <module> # to implementation raise Explanation(msg) from e # via the Explanation Explanation: # wrapper type :-( You can reproduce this error by ... ...
The motivation for this is that we'd like to use ExceptionGroup to indicate that
MultipleFailuresis a group of exceptions, and replace our current print()-based method of reporting the details of the inner exceptions.bpo-28953 is another use case for this feature.
Reopening to implement PEP 678.
2 remaining items
- moved this from Release blocker to Deferred Blocker in Release and Deferred blockers 🚫
on Apr 14, 2022 - moved this from Deferred Blocker to Release blocker in Release and Deferred blockers 🚫
on Apr 14, 2022 The implementation is complete, I am leaving this open for one more documentation PR (but that should not block the release).
Was it a deliberate choice to not add a C API for exception notes? It seems to not be trivial to safely and correctly call
add_notefrom a C extension module. I think it requires all this code:PyObject *type, *exc, *tb; PyErr_Fetch(&type, &exc, &tb); PyErr_NormalizeException(&type, &exc, &tb); PyObject* ret = PyObject_CallMethod(exc, "add_note", "s", "Some message"); Py_XDECREF(ret); if (ret) { // Note successfully added, restore the modified exception. PyErr_Restore(type, exc, tb); } else { // Failed to add the note, set `__context__` on the new exception. Py_XDECREF(type); Py_XDECREF(tb); PyObject *context = exc; PyErr_Fetch(&type, &exc, &tb); PyErr_NormalizeException(&type, &exc, &tb); PyException_SetContext(exc, context); PyErr_Restore(type, exc, tb); }
If we added a dedicated C API, you'd still need to do error checking, and such an API wouldn't be taking care of fetching/restoring the exception (you'd just be passing it an exception object), so the only line that would be simpler would be
PyObject* ret = PyObject_CallMethod(exc, "add_note", "s", "Some message");Hm. I was hoping for a
void PyException_AddNote(PyObject*, const char*)API that callsadd_noteand, on failure, instead sets__context__, which would shorten that to:PyObject *type, *exc, *tb; PyErr_Fetch(&type, &exc, &tb); PyErr_NormalizeException(&type, &exc, &tb); PyException_AddNote(exc, "Some message"); PyErr_Restore(type, exc, tb);
but perhaps I'm being too focused on my own use case, and that wouldn't be sufficiently generic. I'm picturing
add_notealmost always being used during exception handling, and I'm imagining that a failure to calladd_noteshould always lead to exception chaining.Setting
exc.__context__would reverse the relationship between the exception being handled and the one raised byadd_note()though, right?Yes, you're right
__context__would need to be set on the newly raised exception rather than the one passed toadd_notein the case whereadd_notefails. And making that work would mean making a function that takes aPyObject**for the exception, instead, and allowing it to replace it if another exception occurs. And that's getting weird...OK, I see your point. I still think this is quite hard to use, but maybe the complexity is irreducible.
Thanks for leaving a complete snippet showing how to do this. If in the future we find that many people are trying to do this from C (currently we only have a few use cases, all of which are Python code) we may consider adding a C API to reduce the complexity for those people, and your snippet will be helpful in designing the right API then.
Reacted by Matt Wozniski
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsTodo
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields: