This message is longer than I had anticipated.
To aid comprehension, I'm:
- accepting that .split and .subgroup help my "handle some excpetions
but not others" situation, barely
- arguing for ExceptionGroups acting like other containers: truthy if
nonempty, falsey if empty; iterable; .subgroup and .split _not_
returning None for an empty subgroup, so that the container-like
aspects can be used directly
On 28Feb2021 10:40, Irit Katriel
split() and subgroup() take care to preserve the correct metadata on all the internal nodes, and if you just use them you only make safe operations. This is why I am hesitating to add iteration utilities to the API. Like we did, people will naturally try that first, and it's not the safest API.
Wouldn't it be safe if the ExceptionGroup were immutable, as you plan? Or have you another hazard in mind?
We actually have the OSErrors example in the PEP, just above https://www.python.org/dev/peps/pep-0654/#caught-exception-objects:
try: low_level_os_operation() except *OSerror as errors: raise errors.subgroup(lambda e: e.errno != errno.EPIPE) from None
Indeed. That basicly addresses my pattern. Along with:
On Sun, Feb 28, 2021 at 6:30 AM Guido van Rossum
wrote: There’s a pattern for what you propose using the split() method and a lambda, or you can keep the exceptions in a list and re-wrap them at the end.
The keep-a-list approach was my fallback, absent a way to push an unhandled exception back in some sense. Guido:
We really don’t want users pushing non-exceptions into the list, nor do we want e.g. KeyboardInterrupt to be added to a (non-Base-) ExceptionGroup.
I was only imagining pushing exceptions from the original group back in.
Enforcing that would be tedious and confusing though, so I was imagining
some way of marking specific subexeceptions as handled or not handled.
But I had not understood the subgroup method.
I think my handled/unhandled concerns are (barely) sufficient addressed
above. If I wanted to sweep the group for handled exceptions and then
reraise the unhandled ones in their own ExceptionGroup at the end, that
seems tractable.
But all that said, being able to iterable the subexceptions seems a
natural way to manage that:
unhandled = []
try:
.........
except *OSError as eg:
for e in eg:
if an ok exception:
handle it
else:
unhandled.append(e)
if unhandled:
raise ExceptionGroup("unhandled", unhandled)
There are some immediate shortcomings above. In particular, I have no
way of referencing the original ExceptionGroup without surprisingly
cumbersome:
try:
.........
except ExceptionGroup as eg0:
unhandled = []
eg, os_eg = eg0.split(OSError)
if os_eg:
for e in os_eg:
if an ok exception:
handle it
else:
unhandled.append(e)
if eg:
eg, value_eg = eg.split(ValueError)
if value_eg:
for e in value_eg:
if some_ValueError_we_understand:
handle it
else:
unhandled.append(e)
if eg:
unhandled.append(eg)
if unhandled:
raise ExceptionGroup("unhandled", unhandled) from eg0
I have the following concerns with the pattern above:
There's no way to make a _new_ ExceptionGroup with the same __cause__
and __context__ and message as the original group: not that I can't
assign to these, but I'd need to enuerate them; what if the exceptions
grew new attributes I wanted to replicate?
This cries out for another factory method like .subgroup but which makes
a new ExceptionGroup from an original group, containing a new sequence
of exceptions but the same message and coontext. Example:
unhandled_eg = eg0.with_exceptions(unhandled)
I don't see a documented way to access the group's message.
I'm quite unhappy about .subgroup (and presumably .split) returning None
when the group is empty. The requires me to have the gratuitous "if eg:"
and "if value_eg:" if-statements in the example above.
If, instead, ExceptionGroups were like any other container I could just
test if they were empty:
if eg:
_and_ even if they were empty, iterate over them. Who cares if the loop
iterates zero times? The example code would become:
try:
.........
except ExceptionGroup as eg0:
unhandled = []
eg, os_eg = eg0.split(OSError)
for e in os_eg:
if an ok exception:
handle it
else:
unhandled.append(e)
eg, value_eg = eg.split(ValueError)
for e in value_eg:
if some_ValueError_we_understand:
handle it
else:
unhandled.append(e)
if eg:
unhandled.append(eg)
if unhandled:
raise eg0.with_exceptions(unhandled)
You'll note that "except*" is not useful in this pattern. However...
If a subgroup had a reference to its parent this gets cleaner again:
unhandled = []
eg0 = None
try:
.........
except* OSError as os_eg:
eg0 = os_eg.__original__ # or __parent__ or something
for e in os_eg:
if an ok exception:
handle it
else:
except* ValueError as value_eg:
eg0 = os_eg.__original__ # or __parent__ or something
for e in value_eg:
if some_ValueError_we_understand:
handle it
else:
unhandled.append(e)
except* Exception as eg:
eg0 = os_eg.__original__ # or __parent__ or something
unhandled.extend(eg)
if unhandled:
raise eg0.with_exceptions(unhandled)
Except that here I have no way to get "eg0", the original
ExceptionGroup, for the final raise without the additional .__original__
attribute.
Anyway, I'm strongly of the opinion that ExceptionGroups should look
like containers, be iterable, be truthy/falsey based on empty/nonempty
and that .split and .subgroup should return empty subgroups instead of
None.
Cheers,
Cameron Simpson