Decorator function not accepting my argument?

I wrote a decorator function for handling database transactions and I want to be able to pass a logger to it.

My decorator function looks like

def handleTransactionAndCreateResponse(inner_func, logger):
	"""
	Runs inner function inside of a transaction, handles errors and returns a response.
	Args:
		inner_func: the function to wrap in a tranasction
		logger: logger, used to log information.
	"""
	def wrapped_func(*args, **kwargs): 
		logger.info("Starting transaction")
		# Other logic here
        return inner_func(*args, **kwargs)
	return wrapped_func

And I am calling it like

LOGGER = system.util.getLogger(__name__)

@forms2.decorators.handleTransactionAndCreateResponse(LOGGER)
def create(data, tx=None):
    #Named queries here
	return newId

However calling that create function, I get TypeError: handleTransactionAndCreateResponse() takes exactly 2 arguments (1 given)

So my decorator is expecting that logger to come through, I thought I was feeding it correctly with @forms2.decorators.handleTransactionAndCreateResponse(LOGGER) but apparently not. What am I doing wrong here?

I was able to make it work following the example here Script Benchmarking with Decorators Example

2 Likes

Because that's not how you make a parameterized decorator.
You need to go one level deeper. let's see if I can get this right !

So, decorating a function is equivalent to passing this function to the decorating function:

@decorator
def foo():
    return "foo"
foo()

# is like
def foo()
    return "foo"
decorator(foo)()

decorator takes a function, and returns a function, which you can call directly.
Now, with parameters... We gotta make sure we're talking about the good ones, because two things here could take parameters: decorator and foo.
let's first make a decorator that doesn't take params, and which decorates a function that does:

def decorator(func):
    def wrapper(*args, **kwargs):
        # do stuff
        ret = func(args, kwargs)
        # more stuff
        return ret
    return wrapper

@decorator
def foo(*args, **kwargs):
    return f"{args}, {kwargs}"

foo("bar", baz="qux")


def foo(*args, **kwargs):
    return f"{args}, {kwargs}"
decorator(foo)("bar", baz="qux")

Now if we want a decorator that takes parameters, the call would look like this:

decorator(deco_args)(foo)()
# and not
decorator(foo, deco_args)()

We need a function that takes parameters, in which we will define a function that takes a function and returns one, that we will return and that can be used as a decorator.

def parameterized_decorator(deco_args):
    def inner_decorator(func):
        def wrapper(*args, **kwargs):
            # do stuff
            ret = func(args, kwargs)
            # more stuff
            return ret
        return wrapper
    return inner_decorator

So, now we can call parameterized_decorator with arguments.

inner_deco = parameterized_decorator(deco_args)

This returns a function that takes a function and also returns one:

deco = inner_deco(foo)

The function returned by this, can then be called with args and kwargs:

deco(args, kwargs)

In the end, we get this:

@decorator(deco_args)
def foo(*args, **kwargs):
    # do the foo
foo(args, kwargs)

# is like
def foo(*args, **kwargs):
    # do the foo
decorator(deco_args)(foo)(args, kwargs)

Let's hope this all makes sense !

3 Likes

I like metaprogramming, and I still have to re-read the documentation half a dozen times every time I try to write a decorator. I maintain it's just a tricky concept to wrap your head around.

2 Likes

This made me laugh. I figured as much when it wasn't working lol.

I found this and it explained what you just did, I encourage anyone else who want's to learn about decorators to read it The best explanation of Python decorators I’ve ever seen. (An archived answer from StackOverflow.) · GitHub

I decided to not have a function that returns a function that returns a function in my codebase though. Even though it makes sense to me today that won't be the case a week from now and this seems like a very appropriate spot to use some pre-built tools like functools to do the heavy lifting for me.

I'm with you @PGriffith . I always need a refresher when writing decorators.

Yes you can use functools's partial to get a similar result.

You can do it with partial but the example I followed uses @functool.wraps inside the __call__ method of a class around the wrapped function. I found it much cleaner than partial as it also offers opportunity to do an __init__ and set things if nothing is fed in if you so desired.

wraps's role is different. It's used to keep the decorated function metadata (docstrings, name, etc...)...
Since decorators return a new function, those things would be lost if we didn't copy them explicitly to the returned function.
It doesn't make decorators itself, it helps with keeping them consistent.