1

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).

Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
  • What version of Python are you using? When I run your code I get `TypeError: __init__() missing 1 required positional argument: 'func'`, which is the error I would expect. – Patrick Haugh Sep 06 '19 at 18:27
  • @PatrickHaugh I'm using `Python 3.7.2`. I'm gonna double check it as soon as I get home (about 20 minutes). – Philippe Fanaro Sep 06 '19 at 18:34
  • @PatrickHaugh, you are correct. That error should have been `TypeError: __init__() missing 1 required positional argument: 'func'`. I'm gonna fix this in the question. – Philippe Fanaro Sep 06 '19 at 19:15

2 Answers2

6

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
blhsing
  • 91,368
  • 6
  • 71
  • 106
  • So it is not possible to do it with the, in my opinion more elegant, `wraps(func)(self)` + `self.__wrapped__()` combo? – Philippe Fanaro Sep 06 '19 at 19:18
  • 1
    It is not possible because that only works for a decorator that takes no parameter, where the class when used as a callable is a decorator itself. To make a decorator take parameters, you have to make the class a configurator to the decorator, instead of making the class itself a decorator, so the `@wraps` decorator should be applied to the actual wrapper function within the `__call__` method, which itself becomes the true decorator that takes configuration from attributes initialized by the `__init__` method. – blhsing Sep 06 '19 at 19:26
  • Dang it, @blhsing, you attack again! Thank you very much for the answer! One last thing though: would you say it's better that I keep or ditch the `__get__` method as it is programmed in the question? – Philippe Fanaro Sep 06 '19 at 19:43
  • I'm gonna grant you the official answer status, but I think it would be cool to add your comments about it not being possible to use the previously mentioned combo and the `__get__` method. Also, as a I can see that you know a lot about decorators, do you have any suggestions of books on the topic? – Philippe Fanaro Sep 07 '19 at 15:28
  • This was put into the edit suggestion, but, in case it is not accepted: The `__get__` method was fixing the fact that the `self` from the decorator class was getting mixed up with `self` from the decorated method. That happens because in the original question, the decorator itself was a class, but now the decorator is a returned function inside a class. – Philippe Fanaro Sep 14 '19 at 19:46
0
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.

Yogaraj
  • 322
  • 1
  • 4
  • 17
  • You mean I didn't decare a *default value* to the parameter after `func`? Because I tried what you posted before (and now), still not quite working... – Philippe Fanaro Sep 06 '19 at 18:21