-
-
Notifications
You must be signed in to change notification settings - Fork 8
Consider decorator-based exception catch API #5
Comments
Pasting from that comment: try:
...
except:
@ExceptionGroup.handle(FooException)
def handler(exc):
... One limitation of this approach is that try:
...
except:
handler_chain = exceptiongroup.HandlerChain()
@handler_chain(FooException)
def handler(exc):
...
@handler_chain(BarException)
def handler(exc):
...
handler_chain.run() This version could support async handlers, with a bit of extra complexity. (I guess we need both sync and async versions of |
You can probably use assignment expressions too) try:
except:
if exc := match(group := get_exception(), MyException):
print(exc)
elif match(group, SecondException):
print('second exception') |
Unfortunately, the ExceptionGroup catching code is extremely complex, due to all the hacks it has to use to convince python's hard-coded exception-handling not to interfere. For example: if the custom handler itself raises an exception, then that needs to be merged back into the So in practice, there's really no way to avoid putting the handler into something heavyweight like a function, or mayyybe a with block. Actually, I don't think even a with block is quite powerful enough, because there's no way for |
Imho this can't compete with context manager version: try:
except:
with get_exception() as group:
if ex := handle(group, Exception):
raise OtherException In the above example |
Is the idea that you would chain these to handle multiple exceptions, like this? try:
...
except:
with get_exception() as group:
if ex := handle(group, OSError):
raise OtherException
if ex := handle(group, ValueError):
log(ex) That won't work because after the first Also, it's very difficult to get |
Exactly, then probably only replacing both |
You mean, something like this? try:
...
except:
with open_handler() as handler:
with handler.handle(OSError) as exc:
raise OtherException
with handler.handle(ValueError) as exc:
log(exc) This version does have a lot going for it. But I'm still not sure we can get acceptable handling of implicit chaining and re-raising. |
Another interesting trade-off between these different approaches: normally in py3, when you write In the handler-function approaches, you get this effect automatically, because the |
Well, anyway it doesn't look too good. There is a crazy idea how to replace a couple of functions with one: try:
except:
def handler(ex):
if match(MyException, ex):
...
elif match(OtherException, ex):
...
run(handler) You can make that work if the Of course, good |
Another option to consider is a hybrid: try:
...
except:
async with open_handler() as handler:
@handler.handle(OSError)
def handler(exc):
...
@handler.handle(RuntimeError)
async def handler(exc):
... |
Btw, there is no reliable way to get from a function object to its AST :-( |
@WindSoilder asked for more details on the actual semantics of these different ideas. Fair question! :-) Normally when people want to handle an exception, they write something like: try:
...
except ExcType as exc:
# some arbitrary code involving exc, possibly re-raising it or raising a new exception...
... In the original "MultiError v2" proposal (python-trio/trio#611), these was the idea of a with catch(ExcType, my_handler, match=my_predicate):
... and this would be an "exception-group aware" version of this code: try:
...
except ExcType as exc:
if my_predicate(exc):
# handler is some arbitrary code involving exc, possibly re-raising it or raising a new exception...
handler(exc) In particular:
Actually implementing this is pretty complicated:
There's a first (untested) attempt here: https://github.com/python-trio/exceptiongroup/blob/master/exceptiongroup/_tools.py#L106-L146 Here's an actual example. Suppose you're writing an HTTP server. You'll probably want to have a "catch-all" handler around each request, that catches any try:
...
except Exception as exc:
logger.log_exception(exc) With def handler(exc):
logger.log_exception(exc)
with catch(Exception, handler):
... This is annoying, though, because we've had to flip our code upside down: the handler comes first, instead of at the end. So the first proposal here was that we could instead write it like: try:
...
except:
@handle(Exception)
def handler(exc):
logger.log_exception(exc) This would have exactly the same semantics as (Reminder: def handler(exc):
logger.log_exception(exc)
handler = handle(Exception)(handler) Here we're abusing this as a way to run Multiple except blocksBut, what if we have some code like this, that we want to convert to handle exception groups? try:
...
except OSError as exc:
do_one_thing(exc)
except RuntimeError as exc:
do_something_different(exc) That's the idea of the last proposal – you would write it like: try:
...
except:
with open_handler() as handler:
@handler.handle(OSError)
def handler1(exc):
do_one_thing(exc)
@handler.handle(RuntimeError)
def handler2(exc):
do_something_different(exc) Now, there's a subtlety: what if our exception is something like I think the right semantics are: we should walk through the handlers from top to bottom. At each moment we have a "remaining" exception – initially this is the entire remaining = sys.exc_info()[0]
to_rethrow = []
for this_exc_type, this_handler, this_match in registered_handlers:
if remaining is None:
break
this_handler_caught, remaining = split(exc_type, remaining, match=match)
if this_handler_caught is not None:
try:
this_handler(this_handler_caught)
except BaseException as this_handler_raised:
to_rethrow.append(this_handler_raised)
if remaining is not None:
to_rethrow.append(remaining)
raise ExceptionGroup(to_rethrow) I'm sure the implementation will be more complicated than this in practice! But hopefully that gives the idea. One nice thing about this part: we don't necessarily have to get this right for the first release, or implement all the features. This is a separate piece of code from the core |
I fell into trouble to think about the following case: try:
...
except:
with open_handler() as handler:
@handler.handle(RuntimeError)
def handler1(exc):
do_one_thing(exc)
raise exc
@handler.handle(ValueError)
def handler2(exc):
do_another_thing(exc)
raise exc
# do something that raise
# ExceptionGroup(
# "many error",
# [RuntimeError("error1"), ValueError("error2")],
# ["error1", "error2"]
# ) The function |
IMO the most pythonic-looking (and ergonomic!) API proposal was from @maxfischer2781 in this comment: try:
async with something:
something.raise_concurrent()
except MultiError[A]:
print("Handled concurrent A")
except MultiError[A, B] as err:
print("Handled concurrent A and B")
except MultiError[B], MultiError[C] as err:
print("Handled concurrent B or C:", err)
except A:
print("Handled non-concurrent A") This hasn't received an answer in that other thread, but it should be reflected here as well in any case, I think. |
I wouldn't mind contributing a proposal for that API via PR if there is any interest. Is it realistic that this would be reviewed/used? |
how about with PEP 622 try:
async with something:
something.raise_concurrent()
except MultiError as e:
match e:
case MultiError(OSError(errno)):
print(f"{errno=}") |
I think if exceptiongroup lands in CPython, supporting the following syntax will be quite persuasive: eg try:
async with something:
something.raise_concurrent()
match MultiError as e:
case MultiError(OSError(errno)):
print(f"{errno=}") or even: try:
async with something:
something.raise_concurrent()
except case MultiError(OSError(errno)):
print(f"{errno=}") |
See python-trio/trio#611 (comment)
This is probably nicer than
with catch
? @1st1 hateswith catch
so maybe he'll like this better :-)The text was updated successfully, but these errors were encountered: