diff options
Diffstat (limited to 'catapult/common/py_utils/py_utils/exc_util.py')
-rw-r--r-- | catapult/common/py_utils/py_utils/exc_util.py | 84 |
1 files changed, 84 insertions, 0 deletions
diff --git a/catapult/common/py_utils/py_utils/exc_util.py b/catapult/common/py_utils/py_utils/exc_util.py new file mode 100644 index 00000000..538ced2a --- /dev/null +++ b/catapult/common/py_utils/py_utils/exc_util.py @@ -0,0 +1,84 @@ +# Copyright 2019 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import functools +import logging +import sys + + +def BestEffort(func): + """Decorator to log and dismiss exceptions if one if already being handled. + + Note: This is largely a workaround for the lack of support of exception + chaining in Python 2.7, this decorator will no longer be needed in Python 3. + + Typical usage would be in |Close| or |Disconnect| methods, to dismiss but log + any further exceptions raised if the current execution context is already + handling an exception. For example: + + class Client(object): + def Connect(self): + # code to connect ... + + @exc_util.BestEffort + def Disconnect(self): + # code to disconnect ... + + client = Client() + try: + client.Connect() + except: + client.Disconnect() + raise + + If an exception is raised by client.Connect(), and then a second exception + is raised by client.Disconnect(), the decorator will log the second exception + and let the original one be re-raised. + + Otherwise, in Python 2.7 and without the decorator, the second exception is + the one propagated to the caller; while information about the original one, + usually more important, is completely lost. + + Note that if client.Disconnect() is called in a context where an exception + is *not* being handled, then any exceptions raised within the method will + get through and be passed on to callers for them to handle in the usual way. + + The decorator can also be used on cleanup functions meant to be called on + a finally block, however you must also include an except-raise clause to + properly signal (in Python 2.7) whether an exception is being handled; e.g.: + + @exc_util.BestEffort + def cleanup(): + # do cleanup things ... + + try: + process(thing) + except: + raise # Needed to let cleanup know if an exception is being handled. + finally: + cleanup() + + Failing to include the except-raise block has the same effect as not + including the decorator at all. Namely: exceptions during |cleanup| are + raised and swallow any prior exceptions that occurred during |process|. + """ + @functools.wraps(func) + def Wrapper(*args, **kwargs): + exc_type = sys.exc_info()[0] + if exc_type is None: + # Not currently handling an exception; let any errors raise exceptions + # as usual. + func(*args, **kwargs) + else: + # Otherwise, we are currently handling an exception, dismiss and log + # any further cascading errors. Callers are responsible to handle the + # original exception. + try: + func(*args, **kwargs) + except Exception: # pylint: disable=broad-except + logging.exception( + 'While handling a %s, the following exception was also raised:', + exc_type.__name__) + + return Wrapper |