Thanks for the ping. I agree that some utilities to help manipulate ExceptionGroups
would be handy, so thanks for opening this up for discussion. I will focus for now on the first proposal, leaf_exceptions
.
The appeal of leaf_exceptions()
is in its apparent simplicity, but I think the PEP is glossing over some of the complexities in it. For example, ExceptionGroups
should be trees of exceptions, but then again the context chain should be a chain and the interpreter still handles cycles in it. What will these utilities do when the tree is not a tree? The PEP should specify that.
The destructive nature of leaf_exceptions
seems problematic. You wouldn’t necessarily want this:
>>> from leaf import leaf_exceptions
...
... def raise_group():
... excs = []
... for t in [ValueError, TypeError]:
... try:
... raise t()
... except Exception as e:
... excs.append(e)
... raise ExceptionGroup("eg", excs)
...
... try:
... raise_group()
... except* Exception as e:
... leaf_exceptions(e)
... raise
...
[ValueError(), TypeError()]
+ Exception Group Traceback (most recent call last):
| File "<python-input-0>", line 13, in <module>
| raise_group()
| ~~~~~~~~~~~^^
| File "<python-input-0>", line 10, in raise_group
| raise ExceptionGroup("eg", excs)
| ExceptionGroup: eg (2 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<python-input-0>", line 13, in <module>
| raise_group()
| ~~~~~~~~~~~^^
| File "<python-input-0>", line 10, in raise_group
| raise ExceptionGroup("eg", excs)
| File "<python-input-0>", line 7, in raise_group
| raise t()
| ValueError
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<python-input-0>", line 13, in <module>
| raise_group()
| ~~~~~~~~~~~^^
| File "<python-input-0>", line 10, in raise_group
| raise ExceptionGroup("eg", excs)
| File "<python-input-0>", line 7, in raise_group
| raise t()
| TypeError
+------------------------------------
>>>
In order to predict how leaf_exceptions
will impact the different tracebacks in an exception group, you need to know how it is implemented. How will this be documented?
I think an iterator utility should be non-destructive, returning immutable views or complete copies. It’s not hard to copy the traceback, but it may not be possible to copy leaf exceptions (see all the problems with pickling exceptions). We presented a non-destructive iterator version of leaf_exceptions
in PEP 654. It emits exc, tb
pairs, without modifying the traceback on any exceptions. It has its problems too.
Our view when we wrote PEP 654 is that iteration is not, typically, the right tool for handling ExceptionGroups
. This is why we decided not to make them iterable containers. Our thinking was that split
and subgroup
will be used to sift through the groups. That this would be less error prone, and usually more efficient.
In the motivating example, you wanted to split out the “first” exception from the subgroup of HTTPExceptions
. Split/subgroup can do things like this, because they can take a predicate function. Can we design some utilities that will make split and subgroup easier to use for such tasks? Here’s one for your case:
>>> class OneItem:
... def __init__(self):
... self.count = 0
... def __call__(self, e):
... if not isinstance(e, BaseExceptionGroup):
... self.count += 1
... return self.count == 1
... return 0
...
>>> g
ExceptionGroup('eg', [HTTPException(1), HTTPException(2)])
>>> g.split(OneItem())
(ExceptionGroup('eg', [HTTPException(1)]), ExceptionGroup('eg', [HTTPException(2)]))
Rather than (destructively) denormalising the whole tree just to determine whether there are multiple HTTPException
instances, we could provide a Count()
utility to pass to subgroup
or split
.
I can also see us adding a utility to (destructively) transform a single-leaf exception group into a singleton exception with a flat traceback. It should look like a destructive transformation rather than an iteration utility.