Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should I catch exceptions in a decorator function that can raise, if the decorator function is in a library I can't modify?

I'm working the python statsd library on Google App Engine (GAE). Unfortunately, GAE can raise ApplicationError: 4 Unknown error. from time to time when using sockets. The error is an apiproxy_errors.ApplicationError.

The statsd client is already setup to catch socket.error, but not the ApplicationError that sockets can raise on GAE.

I'm specifically working with timer, which returns an instance of Timer: https://github.com/jsocol/pystatsd/blob/master/statsd/client.py#L13

The __call__ method of Timer allows it to be used as a decorator, like so:

from statsd import StatsClient

statsd = StatsClient()

@statsd.timer('myfunc')
def myfunc(a, b):
    """Calculate the most complicated thing a and b can do."""

I don't have easy ability to modify the Timer.__call__ method itself to simply also catch ApplicationError.

How should I write a wrapper or additional decorator that still allows clean decoration like @my_timer_wrapper('statsd_timer_name') but which catches additional exceptions that may occur in the wrapped/decorated timer method?

This is in a foundation module in my codebase that will be used in many places (wherever we want to time something). So although this SO answer might work, I really want to avoid forcing all uses of @statsclient.timer in my codebase to themselves be defined within try-except blocks.

I'm thinking of doing something like the following:

def my_timer_wrapper(wrapped_func, *args, **kwargs):
  @functools.wraps(wrapped_func)
  class Wat(object):
    def __call__(self, *args, **kwargs):
      timer_instance = stats_client.timer(*args, **kwargs)
      try:
        return timer_instance.__call__(wrapped_func)(*args, **kwargs)
      except Exception:
        logger.warning("Caught exception", exc_info=True)
        def foo():
          pass
        return foo

  return Wat()

which would then be used like:

@my_timer_wrapper('stastd_timer_name')
def timed_func():
  do_work()

Is there a better or more pythonic way?

like image 733
Bodhi Avatar asked Nov 27 '25 16:11

Bodhi


1 Answers

It looks like it is a case for an "as straightforward as possible" new decorator adding an extra try/except around your timer decorator.

The only matter being that decorators that take parameters needing 2 levels of nested functions to be defined, almost always makes them look complicated, even when they are not:

from functools import wraps

def  shielded_timer(statsd, 
                    exceptions=(apiproxy_errors.ApplicationError),
                    name=None):

    def decorator(func):
        timer_decorator = statsd.timer(name or func.__name__)
        new_func = timer_decorator(func)
        @wraps(func)
        def wrapper(*args, **kw):
            try:
                return new_func(*args, **kw)
            except BaseException as error:
                if isinstance (error, exceptions):
                    # Expected error (ApplicationError by default) ocurred
                    pass
                else:
                    raise
        return wrapper
    return decorator



#####################
statsd = StatsClient()
@shielded_timer(statsd)
def my_func(a,b):
    ...

As you can see it is easy enough to even include extra niceties - in this case I've made the wanted exceptions configurable at decoration time, and optionally, it uses the name of the decorated function automatically in the call to statsd.timer.

like image 69
jsbueno Avatar answered Nov 29 '25 06:11

jsbueno



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!