psygo
psygo

Reputation: 7583

How to Create a Decorator Class that takes Arguments while Using `functools.wraps` and Preserving Access to the Decorated Instance

1. My Requirements

  1. The Decorator Class should use functools.wraps so it has proper introspection and organization for later.
  2. Access to the decorated instance should be possible.
    • In the example below, I do it by passing a wrapped_self argument to the __call__ method.
  3. As the title states, the Decorator Class must have parameters that you can tune for for each method.

2. An Example of What It Would Look Like

The ideal situation should look something like this:

class A():

    def __init__(self):
        ...

    @LoggerDecorator(logger_name='test.log')
    def do_something(self):
        ...

with the Decorator Class being, so far (basic logger decorator based on a recipe coming from David Beazley's Python Cookbook):

class LoggerDecorator():

    def __init__(self, func, logger_name):
        wraps(func)(self)
        self.logger_name = logger_name

    def config_logger(self):
        ... # for example, uses `self.logger_name` to configure the decorator

    def __call__(self, wrapped_self, *args, **kwargs):
        self.config_logger()
        wrapped_self.logger = self.logger
        func_to_return = self.__wrapped__(wrapped_self, *args, **kwargs)
        return func_to_return

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

3. How Do I Fix It?

The error I'm getting refers to __init__ not recognizing a third argument apparently:

TypeError: __init__() missing 1 required positional argument: 'func'

It has been suggested to me that I should be putting func in the __call__ method. However, if I put it there as a parameter, wrapped_self isn't properly read as a parameter and I get this error:

__call__() missing 1 required positional argument: 'wrapped_self'

I've tried many things to fix this issue, including: putting wraps(func)(self) inside __call__; and many variations of this very close but not quite filling all of the requirements solution (the problem with it is that I can't seem to be able to access wrapped_self anymore).

Upvotes: 1

Views: 2197

Answers (2)

Yogaraj
Yogaraj

Reputation: 320

from functools import wraps


class LoggerDecorator:

    def __init__(self, logger):
        self.logger = logger

    def __call__(self, func, *args, **kwargs):
        print func, args, kwargs
        # do processing
        return func

@LoggerDecorator('lala')
def a():
    print 1

The above should work as expected. If you're planning to call the decorator using keyword arguments you can remove the logger from __init__ and use **kwargs which will return a dict of the passed keywork arguments.

Upvotes: 0

blhsing
blhsing

Reputation: 106648

Since you're implementing a decorator that takes parameters, the __init__ method of LoggerDecorator should take only the parameters that configures the decorator, while the __call__ method instead should become the actual decorator that returns a wrapper function:

class LoggerDecorator():
    def __init__(self, logger_name):
        self.logger_name = logger_name
        self.config_logger()

    def __call__(self, func):
        @wraps(func)
        def wrapper(wrapped_self, *args, **kwargs):
            wrapped_self.logger = self.logger
            func_to_return = func(wrapped_self, *args, **kwargs)
            return func_to_return
        return wrapper

Upvotes: 6

Related Questions