Question

I am creating a decorator that catches a raised error in it's target function, and allows the user to continue executing the script (bypassing the function) or drop out of the script.

def catch_error(func):
    """
    This decorator is used to make sure that if a decorated function breaks 
    in the execution of a script, the script doesn't automatically crash. 
    Instead, it gives you the choice to continue or gracefully exit.    

    """
    def caught(*args):
        try:
            return func(*args)
        except Exception as err:
            question = '\n{0} failed. Continue? (yes/no): '.format(func.func_name)
            answer = raw_input(question)
            if answer.lower() in ['yes','y']:
                pass
            else:
                print "   Aborting! Error that caused failure:\n"
                raise err 
            return None
    return caught

Notice that, if the user chooses to bypass the error-returning function and continue executing the script, the decorator returns None. This works well for functions that only return a single value, but it is crashing on functions that attempt to unpack multiple values. For instance,

# Both function and decorator return single value, so works fine
one_val = decorator_works_for_this_func() 
# Function nominally returns two values, but decorator only one, so this breaks script
one_val, two_val = decorator_doesnt_work_for_this_func()

Is there a way that I can determine the number of values my target function is supposed to return? For instance, something like:

def better_catch_error(func):
    def caught(*args):
        try:
            return func(*args)
        except Exception as err:
            ...
            num_rvals = determine_num_rvals(func)
            if num_rvals > 1:
                return [ None for count in range(num_rvals) ]
            else:
                return None               
    return caught

As always, if there is a better way to do this sort of thing, please let me know. Thanks!

UPDATE:

Thanks for all the suggestions. I decided to narrow the scope of catch_error to a single class of functions, which only return one string value. I just split all the functions returning more than one value into separate functions that return a single value to make them compatible. I had been hoping to make catch_error more generic (and there were several helpful suggestions on how to do that), but for my application it was a little overkill. Thanks again.

Was it helpful?

Solution

Martijn Pieters answer is correct, this is a specific case of the Halting Problem

However you might get around it by passing a error return value to the decorator. Something like this:

def catch_error(err_val):
    def wrapper(func):
        def caught(*args):
            try:
                return func(*args)
            except Exception as err:
                question = '\n{0} failed. Continue? (yes/no): '.format(func.func_name)
                answer = raw_input(question)
                if answer.lower() in ['yes','y']:
                    pass
                else:
                    print "   Aborting! Error that caused failure:\n"
                    raise err 
                return err_val
        return caught
    return wrapper

Then you could decorate using:

@catch_error({})
def returns_a_dict(*args, **kwargs):
    return {'a': 'foo', 'b': 'bar'}

Also as a point of style, if you are grabbing *args in your wrapped function, you should probably also grab **kwargs so that you can properly wrap functions that take keyword arguments. Otherwise your wrapped function will fail if you call wrapped_function(something=value)

Finally, as another point of style, it is confusing to see code that does if a: pass with an else. Try using if !a in these cases. So the final code:

def catch_error(err_val):
    def wrapper(func):
        def caught(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as err:
                question = '\n{0} failed. Continue? (yes/no): '.format(func.func_name)
                answer = raw_input(question)
                if answer.lower() not in ['yes','y']:
                    print "   Aborting! Error that caused failure:\n"
                    raise err
                return err_val
        return caught
    return wrapper

OTHER TIPS

No, there is no way you can determine that.

Python is a dynamic language, and a given function can return an arbitrary sequence of any size or no sequence at all, based on the inputs and other factors.

For example:

import random

def foo():
    if random.random() < 0.5:
        return ('spam', 'eggs')
    return None

This function will return a tuple half of the time, but None the other half, so Python has no way of telling you what foo() will return.

There are many more ways your decorator can fail, btw, not just when the function returns a sequence that the caller then unpacks. What if the caller expected a dictionary and starts trying to access keys, or a list and wants to know the length?

Your decorator can't predict what your function is going to return, but nothing prevents you from telling the decorator what return signature to simulate:

@catch_error([None, None])
def tuple_returner(n):
    raise Exception
    return [2, 3]

Instead of returning None, your decorator will return its argument ([None, None]).

Writing an argument-taking decorator is just slightly tricky: The expression catch_error([None, None]) will be evaluated, and must return the actual decorator that will be applied to the decorated function. It looks like this:

def catch_error(signature=None):
    def _decorator(func):
        def caught(*args):
            try:
                return func(*args)
            except Exception as err:
                # Interactive code suppressed
                return signature

        return caught

    return _decorator

Note that even if you just want it to return None, you need to execute it once:

@catch_error()
def some_function(x):
    ...
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top