Search code examples
pythonclassdecoratorpython-decoratorspython-3.8

Creating a Logging Class Wrapper


I am attempting to write a quick decorator to manage logging returns of various functions. I am not super well versed in decorators so any help you can provide would be very helpful!

from functools import update_wrapper
from typing import Any, Optional
from logging import getLogger
from time import perf_counter
from datetime import datetime

class logger:

    def __init__(self, func:callable, response:str = "debug"):
        self.logger = getLogger()
        self.func = func
        self.response = response

        update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        return getattr(self, self.response)

    def debug(self, *args, **kwargs):
        self.logger.debug(f"Running {__name__} with id: {id(self)} at {datetime.now()}")
        start = perf_counter()
        value = self.func(*args, **kwargs)
        end = perf_counter()
        self.logger.debug(f"""Completed {__name__} with id: {id(self)} at {datetime.now()}.
            Total Time to run: {end - start:.6f}s""")
        return value

    def info(self, *args, **kwargs):
        self.logger.info(f"Running {__name__} at {datetime.now()}.")
        return self.func(*args, **kwargs)

@logger(response="debug")
def stuff(x):
    return x*x

stuff(2)

The error I am receiving is:

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

clearly, it doesn't like the required callable and the response requirement. However, I see in all other class-based decorator setups that func needs to be called as part of the __init__ and I have also seen you can pass decorators addition information. What am I doing wrong here?

EDIT:

The purpose of getattr(self, self.response) is so that the function returned by __call__ is either the function along with the debug or info logging. This allows me to utilize the decorator @logging for both logging and debug, yet yields two different results depending on the response value specified in the decorator (i.e @logging(response="info")).

Solution:

class logger:

    def __init__(self, response:str = "debug"):
        self.logger = getLogger()
        self.response = response

    def __call__(self, func:callable):
        update_wrapper(self, func)
        self.func = func
        return getattr(self, self.response)

    def debug(self, *args, **kwargs):
        self.logger.debug(f"Running {self.func.__name__} (type:{type(self.func)}) with id: {id(self)} at {datetime.now()}")
        start = perf_counter()
        value = self.func(*args, **kwargs)
        end = perf_counter()
        self.logger.debug(f"""Completed {self.func.__name__} with id: {id(self)} at {datetime.now()}.
            Total Time to run: {end - start:.6f}s""")
        return value

    def info(self, *args, **kwargs):
        self.logger.info(f"Running {self.func.__name__} at {datetime.now()}.")
        return self.func(*args, **kwargs)

Solution

  • I don't know what your code should do, in particular it is not clear (to me) which kind of arguments should be passed to getattr(self, self.response)(*args, **kwargs). I am saying this to understand the proper workflow of the decorator.

    So your code will never work. Here some possible examples of decoration:

    the __call__way: @logger(response="debug")

    class logger_1:
    
        def __init__(self, response:str = "debug"):
           print(response)
    
        def __call__(self, func):
            self.func = func
            return self # ? depends on what are you doing
    
        def debug(self, *args, **kwargs):
            # ...
    
        def info(self, *args, **kwargs):
            #... 
    
    @logger_1(response="debug")
    def stuff(x):
        return x*x
    

    A level more of "abstraction": @logger(response="debug").('some_parameter').debug_method

    class logger_2:
    
        def __init__(self, response:str = "debug"):
           print(response)
    
        def __call__(self, *args, **kwargs):
           self.updated_response = getattr(self, self.response)(*args, **kwargs) # just an example
           return self
    
        def debug_method(self, func):
           self.func = func
           # ...
           return func
    
        def debug(self, *args, **kwargs):
            # ...
    
        def info(self, *args, **kwargs):
            #... 
    
    @logger_2(response="debug")('some_parameter').debug_method
    def stuff(x):
        return x*x
    

    NB: logger_2(response="debug").('some_parameter').debug_method is not taking argument because it waits to be "feed" with the target function stuff

    These are examples of syntax which constraint the workflow, so you need to be careful when design your decorator