1

I would like to create a decorator adding a logger to any decorated class. I succeeded with:

def logged(cls_to_decorate):
    log_name = cls_to_decorate.__module__
    logger = logging.getLogger(log_name)

    setattr(cls_to_decorate, 'logger', logger)
    return cls_to_decorate

Its usage:

@logged
class TestFoo(TestCase):
    def test_foo_function(self):
         self.logger.debug("ciao!!")

Now let's suppose I want to pass to this decorator an additional parameter, so to use it as follows:

@logged(logging_level=logging.DEBUG)
class TestFoo(TestCase):
    pass

I tried to use the syntax of the decorators for functions/methods (with wrapped function), but of course since we're talking about a class as parameter it doesn't work.

The decorator should be something like this:

...
def logged(cls_to_decorate, logging_level=None):
    logging.basicConfig(level=logging_level)

    log_name = cls_to_decorate.__module__
    logger = logging.getLogger(log_name)

    setattr(cls_to_decorate, 'logger', logger)
    return cls_to_decorate
...

NOTE: the object to be decorated is a class and not a function.

Solution python decorators with parameters should apply, but I tried the following:

def logged(logging_level=None):
    def class_decorator(cls_to_decorate):

        # Do something with arguments
        logging.basicConfig(level=logging_level)
        log_name = cls_to_decorate.__module__
        logger = logging.getLogger(log_name)

        setattr(cls_to_decorate, 'logger', logger)
        return cls_to_decorate
    return class_decorator

But I have an error every time I wrap a class takling some argument:

E               TypeError: class_decorator() takes exactly 1 argument (2 given)

Thanks for your help!

Community
  • 1
  • 1
Alex Gidan
  • 2,619
  • 17
  • 29

1 Answers1

2

the answer here works the exact same way for a class as a function, create a nested factory function:

def decorator(argument):
    def real_decorator(class):
        <DECORATOR CODE HERE>
    return real_decorator

so you would do:

def logged(logging_level):
    def class_decorator(cls_to_decorate):
        log_name = cls_to_decorate.__module__
        logger = logging.getLogger(log_name)

        setattr(cls_to_decorate, 'logger', logger)
        return cls_to_decorate
    return class_decorator

Note that this means that you must call the decorator to use it as such, just using @logged will think that the class is the logging_level argument which makes no sense, you would need to at least do @logged() for it to work.


Although since you are using keywords anyway I'd like to offer an alternative using functools.partial when the call is missing information:

def logger(cls_to_decorate=None, logging_level=logging.DEBUG):
    if cls_to_decorate is None:
        return functools.partial(logger, logging_level=logging_level)

    ... #other code here

that way when you do logger(existing_cls) it works correctly and when you do @logger(logging_level=logging.INFO) it works correctly and just @logged works correctly, the only pitfall is if you accidentally specify the option in place of the class.

@logger(logging.INFO) #on no we passed a positional argument!!
class Test():pass

Traceback (most recent call last):
  File "/Users/Tadhg/Documents/codes/test.py", line 25, in <module>
    @logged(logging.INFO)
  File "/Users/Tadhg/Documents/codes/test.py", line 19, in logged
    log_name = cls_to_decorate.__module__
AttributeError: 'int' object has no attribute '__module__'

This is not the most informative error message although it can be improved by adding an assert inspect.isclass(cls_to_decorate) at some point.

Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59