Search code examples
pythonpython-decorators

Accessing default value of decorator argument in Python


I am trying to write a custom Python decorator which wraps the decorated function in a try ... except block and adds a message with additional context to make debugging easier.

Based on different resources (see here and here for example) I so far built the following:

def _exception_handler(msg):
    """Custom decorator to return a more informative error message"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            try:
                # this is the actual decorated function
                func(*args, **kwargs)
            except Exception as e:
                # we catch the exception and raise a new one with the custom
                # message and the original exception as cause
                raise Exception(f"{msg}: {e}") from e
        return wrapper
    return decorator

This works as expected - if I run:

@_exception_handler("Foo")
def test():
    raise ValueError("Bar")

test()

This returns:

Exception: Foo: Bar

Now, I do not always want to pass a custom msg because that's sometimes a bit redundant. So I set a default value of msg="" and I want to check if msg=="" and if that is the case I would just like to re-create the msg inside the decorator based on the function name, something like msg = f"An error occurred in {func.__name__}".

I would then like to use the decorator without any msg argument. I do not care about the empty parantheses, using @_exception_handler() is perfectly fine for me.

But somehow this does not seem to work:

def _exception_handler(msg=""):
    """Custom decorator to return a more informative error message"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            try:
                # this is the actual decorated function
                func(*args, **kwargs)
            except Exception as e:
                if msg=="":
                    # if no custom message is passed, we just use the function name
                    msg = f"An error occurred in {func.__name__}"
                # we catch the exception and raise a new one with the message
                # and the original exception as cause
                raise Exception(f"{msg}: {e}") from e
        return wrapper
    return decorator

If I run:

@_exception_handler()
def test():
    raise ValueError("Bar")

test()

I get

UnboundLocalError: local variable 'msg' referenced before assignment

If I put global message right below the def decorator(func): line, I get the same errors. If I put it below the def wrapper(*args, **kwargs):, I instead get:

NameError: name 'msg' is not defined

Any ideas how I can get this to work? If possible, I would like to avoid any third-party modules such as wrapt. Using wraps from functools from the standard library is fine of course (although I did not have any luck with that so far either).


Solution

  • add this code nonlocal msg.

    def _exception_handler(msg=""):
        """Custom decorator to return a more informative error message"""
        def decorator(func):
            def wrapper(*args, **kwargs):
                nonlocal msg
                try:
                    # this is the actual decorated function
                    func(*args, **kwargs)
                except Exception as e:
                    if msg=="":
                        # if no custom message is passed, we just use the function name
                        msg = f"An error occurred in {func.__name__}"
                    # we catch the exception and raise a new one with the message
                    # and the original exception as cause
                    raise Exception(f"{msg}: {e}") from e
            return wrapper
        return decorator