# 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
