You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Better ergonomics for working with MultiErrors #49
Unfortunately, MultiErrors aren't very nice to work with right now.
They don't print backtraces for the individual exceptions. (And unfortunately python doesn't make this particularly easy.) I'm sure we can do better than we are though.
A relatively common pattern is to want to catch a particular exception, or allow particular exceptions through (e.g., this comes up in trio._core._run quite a bit!), or transform particular exceptions, which requires "slicing" a MultiError. Right now we're doing this by hand when we need to, but it's obviously something that would be better handled by some utility functions with the right abstraction. (And there are places where we aren't handling it right, like system_task_wrapper – see #50.)
Unfortunately I'm not entirely sure what this looks like. Maybe it will become clearer as we work with these things more.
Where the semantics are: the __exit__ block effectively maps exception_filter over all the exceptions that were raised (if any).
This will probably need better logic for flattening MultiError (or at least recursing over the tree), now that I think about it... in fact I think cancel_scope might get this wrong :-(
Another weirdness: if an exception gets packed into a MultiError, and then unpacked again later, then part of its traceback just disappears.
In general the packing and nesting is kind of nice because it means shared branches of the stack only get saved once, instead of being repeated in all of the atomic exceptions. But when we revert to the atomic exceptions it's kinda sucky :-(.
...Maybe there's some horrible way to concatenate the traceback objects?
It looks like traceback objects are linked lists with tb_next pointing to the next item. So concatenating them is a simple matter of assigning to tb_next. Unfortunately tb_next is readonly :-(. Of course nothing is really readonly in CPython... though PyPy might be another story. [Edit: the hacky solution for PyPy is transparent proxies. Contra to what that page says, they appear to be enabled by default.]
Also, it's not enough to concatenate them -- we need to be able to copy them, because when reconstructing a MultiError after a multi-catch, the original single MultiError's traceback might need to be concatenated to multiple exceptions.
[Edit: it looks like jinja2 has an implementation of a good chunk of what we need.]
I also wonder if we should go ahead and override sys.excepthook (if not already overridden) to something that handles MultiErrors appropriately? It's unfortunate that this won't help in ipython or other enhanced interpreters, but... (I guess for ipython there's always the monkeypatch approach!) [Edit: oh, here's a real solution for ipython]
There can only get one custom exc handler registered. So we should so something like
ip=IPython.get_ipython()
ifip.custom_exceptions!= ():
warnings.warn("IPython detected, but you already have a custom traceback handler installed ...")
else:
ip.set_custom_exc(...)
We can't return the traceback info to be formatted (I think?), but instead must print it ourselves, because we can only return one traceback (as a list of strings). But tracebacks eventually end up in ip._showtraceback(etype, value, list_of_strings), which we can call if we want.
The way IPython generates the list-of-strings is with ip.InteractiveTB.structured_traceback(etype, value, tb, tb_offset=tb_offset). However, we can't directly call this repeatedly, for two reasons:
It's stateful – it saves the traceback (maybe for %pdb? not sure). Maybe harmless, maybe bad.
It has baked into it that it should discard the top two frames of the traceback that's passed in (though you can have it discard more via tb_offset, it seems to use max(tb_offset, 2). This makes sense for stuff caught at the IPython top-level, but not for intermediate exceptions.
Better tracebacks in pytest would also be really nice. This looks noticeably harder, though. I think it might be possible with a custom collector that defines a custom repr_failure method?
Activity
njsmith commentedon Feb 15, 2017
Idea for catching/filtering exceptions when there are
MultiErrors around:Where the semantics are: the
__exit__block effectively mapsexception_filterover all the exceptions that were raised (if any).This will probably need better logic for flattening
MultiError(or at least recursing over the tree), now that I think about it... in fact I thinkcancel_scopemight get this wrong :-(njsmith commentedon Feb 15, 2017
Another weirdness: if an exception gets packed into a
MultiError, and then unpacked again later, then part of its traceback just disappears.In general the packing and nesting is kind of nice because it means shared branches of the stack only get saved once, instead of being repeated in all of the atomic exceptions. But when we revert to the atomic exceptions it's kinda sucky :-(.
...Maybe there's some horrible way to concatenate the traceback objects?
njsmith commentedon Feb 15, 2017
It looks like traceback objects are linked lists with
tb_nextpointing to the next item. So concatenating them is a simple matter of assigning totb_next. Unfortunatelytb_nextis readonly :-(. Of course nothing is really readonly in CPython... though PyPy might be another story. [Edit: the hacky solution for PyPy is transparent proxies. Contra to what that page says, they appear to be enabled by default.]Also, it's not enough to concatenate them -- we need to be able to copy them, because when reconstructing a
MultiErrorafter a multi-catch, the original singleMultiError's traceback might need to be concatenated to multiple exceptions.[Edit: it looks like jinja2 has an implementation of a good chunk of what we need.]
njsmith commentedon Feb 16, 2017
I also wonder if we should go ahead and override
sys.excepthook(if not already overridden) to something that handlesMultiErrors appropriately? It's unfortunate that this won't help in ipython or other enhanced interpreters, but... (I guess for ipython there's always the monkeypatch approach!) [Edit: oh, here's a real solution for ipython]njsmith commentedon Feb 18, 2017
If this python issue were fixed somehow it would be helpful...: https://bugs.python.org/issue18861
njsmith commentedon Feb 18, 2017
More notes on ipython custom tracebacks:
There can only get one custom exc handler registered. So we should so something like
We can't return the traceback info to be formatted (I think?), but instead must print it ourselves, because we can only return one traceback (as a list of strings). But tracebacks eventually end up in
ip._showtraceback(etype, value, list_of_strings), which we can call if we want.The way IPython generates the list-of-strings is with
ip.InteractiveTB.structured_traceback(etype, value, tb, tb_offset=tb_offset). However, we can't directly call this repeatedly, for two reasons:It's stateful – it saves the traceback (maybe for
%pdb? not sure). Maybe harmless, maybe bad.It has baked into it that it should discard the top two frames of the traceback that's passed in (though you can have it discard more via
tb_offset, it seems to usemax(tb_offset, 2). This makes sense for stuff caught at the IPython top-level, but not for intermediate exceptions.njsmith commentedon Feb 18, 2017
Better tracebacks in pytest would also be really nice. This looks noticeably harder, though. I think it might be possible with a custom collector that defines a custom
repr_failuremethod?njsmith commentedon Feb 20, 2017
This is a messy thread and the main issues are fixed; closing in favor of #27 and #56