From: Rob Browning Date: Fri, 7 Apr 2017 00:32:50 +0000 (-0500) Subject: Support exception chaining and tracebacks X-Git-Tag: 0.30~196 X-Git-Url: https://arthur.barton.de/cgi-bin/gitweb.cgi?p=bup.git;a=commitdiff_plain;h=7a339a74732eb8e12add7c5af01e5bdc2135dc08 Support exception chaining and tracebacks Python 3 has added support for exception chaining https://www.python.org/dev/peps/pep-3134/ Which makes it possible to avoid losing information when an exception is thrown from within an exception handler: try: ... raise lp0_on_fire() except Exception as ex: # Imagine the disk is also full and close() throws too some_output_file.close() In this situation, you'll never find out the printer's on fire. With chaining, the first exception will be attached to the second as its __context__. (Note that "finally" blocks suffer from the same issue.) The PEP also describes adding a __traceback__ attribute to exceptions so that they're more self-contained. Python 3 handles all of this automatically, and includes any chained exceptions in its tracebacks. For this: def inner(): raise Exception('first') def outer(): try: inner() except Exception as ex: raise chain_ex(Exception('second'), ex) wrap_main(outer) Python 3 produces: $ python3 lib/bup/compat.py Traceback (most recent call last): File "lib/bup/compat.py", line 74, in outer inner() File "lib/bup/compat.py", line 70, in inner raise Exception('first') Exception: first During handling of the above exception, another exception occurred: Traceback (most recent call last): File "lib/bup/compat.py", line 78, in wrap_main(outer) File "lib/bup/compat.py", line 50, in wrap_main sys.exit(main()) File "lib/bup/compat.py", line 76, in outer raise chain_ex(Exception('second'), ex) Exception: second Add a compat.py supporting something similar for Python 2, and use it in main.py. Signed-off-by: Rob Browning Tested-by: Rob Browning --- diff --git a/lib/bup/compat.py b/lib/bup/compat.py new file mode 100644 index 0000000..d0e2c5d --- /dev/null +++ b/lib/bup/compat.py @@ -0,0 +1,83 @@ + +from __future__ import print_function +from traceback import print_exception +import sys + +py_maj = sys.version_info[0] +py3 = py_maj >= 3 + +if py3: + + def add_ex_tb(ex): + pass + + def chain_ex(ex, context_ex): + return ex + +else: # Python 2 + + def add_ex_tb(ex): + if not getattr(ex, '__traceback__', None): + ex.__traceback__ = sys.exc_info()[2] + + def chain_ex(ex, context_ex): + if context_ex: + add_ex_tb(context_ex) + if not getattr(ex, '__context__', None): + ex.__context__ = context_ex + return ex + + def dump_traceback(ex): + stack = [ex] + next_ex = getattr(ex, '__context__', None) + while next_ex: + stack.append(next_ex) + next_ex = getattr(next_ex, '__context__', None) + stack = reversed(stack) + ex = next(stack) + tb = getattr(ex, '__traceback__', None) + print_exception(type(ex), ex, tb) + for ex in stack: + print('\nDuring handling of the above exception, another exception occurred:\n', + file=sys.stderr) + tb = getattr(ex, '__traceback__', None) + print_exception(type(ex), ex, tb) + +def wrap_main(main): + """Run main() and raise a SystemExit with the return value if it + returns, pass along any SystemExit it raises, convert + KeyboardInterrupts into exit(130), and print a Python 3 style + contextual backtrace for other exceptions in both Python 2 and + 3).""" + try: + sys.exit(main()) + except KeyboardInterrupt as ex: + sys.exit(130) + except SystemExit as ex: + raise + except BaseException as ex: + if py3: + raise + add_ex_tb(ex) + dump_traceback(ex) + sys.exit(1) + + +# Excepting wrap_main() in the traceback, these should produce the same output: +# python2 lib/bup/compat.py +# python3 lib/bup/compat.py +# i.e.: +# diff -u <(python2 lib/bup/compat.py 2>&1) <(python3 lib/bup/compat.py 2>&1) + +if __name__ == '__main__': + + def inner(): + raise Exception('first') + + def outer(): + try: + inner() + except Exception as ex: + raise chain_ex(Exception('second'), ex) + + wrap_main(outer) diff --git a/main.py b/main.py index ecc975f..33d21df 100755 --- a/main.py +++ b/main.py @@ -32,6 +32,7 @@ os.environ['BUP_RESOURCE_PATH'] = resourcepath from bup import helpers +from bup.compat import wrap_main from bup.helpers import atoi, columnate, debug1, log, tty_width @@ -178,37 +179,40 @@ def handler(signum, frame): signal.signal(signal.SIGTSTP, handler) ret = 94 -signal.signal(signal.SIGTERM, handler) -signal.signal(signal.SIGINT, handler) -signal.signal(signal.SIGTSTP, handler) -signal.signal(signal.SIGCONT, handler) +def main(): + signal.signal(signal.SIGTERM, handler) + signal.signal(signal.SIGINT, handler) + signal.signal(signal.SIGTSTP, handler) + signal.signal(signal.SIGCONT, handler) -try: try: - c = (do_profile and [sys.executable, '-m', 'cProfile'] or []) + subcmd - if not n and not outf and not errf: - # shortcut when no bup-newliner stuff is needed - os.execvp(c[0], c) - else: - p = subprocess.Popen(c, stdout=outf, stderr=errf, - preexec_fn=force_tty) - while 1: - # if we get a signal while waiting, we have to keep waiting, just - # in case our child doesn't die. - ret = p.wait() - forward_signals = False - break - except OSError as e: - log('%s: %s\n' % (subcmd[0], e)) - ret = 98 -finally: - if p and p.poll() == None: - os.kill(p.pid, signal.SIGTERM) - p.wait() - if n: - n.stdin.close() try: - n.wait() - except: - pass -sys.exit(ret) + c = (do_profile and [sys.executable, '-m', 'cProfile'] or []) + subcmd + if not n and not outf and not errf: + # shortcut when no bup-newliner stuff is needed + os.execvp(c[0], c) + else: + p = subprocess.Popen(c, stdout=outf, stderr=errf, + preexec_fn=force_tty) + while 1: + # if we get a signal while waiting, we have to keep waiting, just + # in case our child doesn't die. + ret = p.wait() + forward_signals = False + break + except OSError as e: + log('%s: %s\n' % (subcmd[0], e)) + ret = 98 + finally: + if p and p.poll() == None: + os.kill(p.pid, signal.SIGTERM) + p.wait() + if n: + n.stdin.close() + try: + n.wait() + except: + pass + sys.exit(ret) + +wrap_main(main)