From ea3cadffc728f5a8262243f6f9cafdc331b12b2b Mon Sep 17 00:00:00 2001 From: Rob Browning Date: Sat, 12 Sep 2020 17:14:21 -0500 Subject: [PATCH] compat: add "with pending_raise(ex)" to simplify nested exceptions Thanks to Johannes Berg for the suggesting the approach. Signed-off-by: Rob Browning Tested-by: Rob Browning --- CODINGSTYLE | 23 ++++++++++++++++++----- Makefile | 1 + lib/bup/compat.py | 34 ++++++++++++++++++++++++++++++++++ lib/bup/t/tcompat.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 lib/bup/t/tcompat.py diff --git a/CODINGSTYLE b/CODINGSTYLE index 69c2cfb..348a2f5 100644 --- a/CODINGSTYLE +++ b/CODINGSTYLE @@ -43,7 +43,6 @@ explicitly add stack traces to any exceptions that are going to be re-raised by anything other than a no-argument raise (otherwise the stack trace will be lost):: - try: ... except ... as ex: @@ -52,11 +51,24 @@ stack trace will be lost):: ... raise pending_ex -If an exception is thrown from an exception handler, the pending +When an exception is thrown from an exception handler, the pending exception should be the `"context" `_ -of the new exception This can be accomplished via -``add_ex_ctx()``:: +of the new exception, which can be accomplished (portably) via +``pending_raise()``:: + + try: + ... + except ... as ex: + with pending_raise(ex): + clean_up() + +This should do roughly the same thing in Python 2 and Python 3, +throwing any exception from ``clean_up()`` after adding ex as the +``__context__`` if clean_up() throws, and throwing ``ex`` otherwise. + +If for some reason, you need more control, you can use +``add_ex_ctx()`` directly:: try: ... @@ -69,4 +81,5 @@ of the new exception This can be accomplished via raise add_ex_ctx(ex2, ex) raise -See the end of ``lib/bup/compat.py`` for a functional example. +See the end of ``lib/bup/compat.py`` for a functional example, and all +of this can be removed once we drop support for Python 2. diff --git a/Makefile b/Makefile index 4d0784b..5e31562 100644 --- a/Makefile +++ b/Makefile @@ -163,6 +163,7 @@ runtests: runtests-python runtests-cmdline python_tests := \ lib/bup/t/tbloom.py \ lib/bup/t/tclient.py \ + lib/bup/t/tcompat.py \ lib/bup/t/tgit.py \ lib/bup/t/thashsplit.py \ lib/bup/t/thelpers.py \ diff --git a/lib/bup/compat.py b/lib/bup/compat.py index 39dd810..2cd6fba 100644 --- a/lib/bup/compat.py +++ b/lib/bup/compat.py @@ -37,6 +37,20 @@ if py3: """Do nothing (already handled by Python 3 infrastructure).""" return ex + class pending_raise: + """Rethrow either the provided ex, or any exception raised by the with + statement body. (Supports Python 2 compatibility.) + + """ + def __init__(self, ex): + self.ex = ex + def __enter__(self): + return None + def __exit__(self, exc_type, exc_value, traceback): + if not exc_type: + raise self.ex + return None + def items(x): return x.items() @@ -103,6 +117,26 @@ else: # Python 2 ex.__context__ = context_ex return ex + class pending_raise: + """Rethrow either the provided ex, or any exception raised by the with + statement body, after making ex the __context__ of the newer + exception (assuming there's no existing __context__). Ensure + the exceptions have __tracebacks__. (Supports Python 2 + compatibility.) + + """ + def __init__(self, ex): + self.ex = ex + def __enter__(self): + add_ex_tb(self.ex) + return None + def __exit__(self, exc_type, exc_value, traceback): + if not exc_type: + raise self.ex + add_ex_tb(exc_value) + add_ex_ctx(exc_value, self.ex) + return None + def dump_traceback(ex): stack = [ex] next_ex = getattr(ex, '__context__', None) diff --git a/lib/bup/t/tcompat.py b/lib/bup/t/tcompat.py new file mode 100644 index 0000000..039eb12 --- /dev/null +++ b/lib/bup/t/tcompat.py @@ -0,0 +1,32 @@ + +from __future__ import absolute_import, print_function + +from wvtest import * + +from bup.compat import pending_raise + +@wvtest +def test_pending_raise(): + outer = Exception('outer') + inner = Exception('inner') + + try: + try: + raise outer + except Exception as ex: + with pending_raise(ex): + pass + except Exception as ex: + WVPASSEQ(outer, ex) + WVPASSEQ(None, getattr(outer, '__context__', None)) + + try: + try: + raise outer + except Exception as ex: + with pending_raise(ex): + raise inner + except Exception as ex: + WVPASSEQ(inner, ex) + WVPASSEQ(None, getattr(outer, '__context__', None)) + WVPASSEQ(outer, getattr(inner, '__context__', None)) -- 2.39.2