python-3.xinheritanceloggingpython-decorators

applying decorators to class function when using a base class


I am trying to build a decorator that allows me to log functions in my classes in Python3. The decorator is as follows:

import functools
def log(_func=None, *, logger):
    def decorator_log(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                logger.info(f"Entering function {func.__qualname__}")
                args_repr = [repr(a) for a in args]
                kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
                signature = ", ".join(args_repr + kwargs_repr)
                logger.debug(f"function {func.__qualname__} called with args {signature}")
                try:
                    result = func(*args, **kwargs)
                    return result
                except Exception as e:
                    logger.exception(f"Exception raised in {func.__qualname__}. exception: {str(e)}")
                    raise e
                logger.info(f"Leaving function {func.__qualname__}.{func.__name__}")
            except Exception:
                pass
        return wrapper

    if _func is None:
        return decorator_log
    else:
        return decorator_log(_func)

This is then used to decorate class functions like so:

class MainController(BaseController):
    logger = logging.getLogger(__name__)
    def __init__(self, main_model, main_view):
        ...

    @log(logger=logger)
    def initialize_components(self):
        ...

etc.

This works fine, but runs into issues when I use base classes and subclasses. In particular, I want to decorate almost all functions in both the base class and the subclass, but when I do this, func.__qualname__ will be subclass.methodname only when the subclass overrides a base class method, and it will be the baseclass.methodname otherwise.

How can I modify this approach so that even in cases where the subclass does not override the base class method, a decorator applied to the base class method will give me subclass.methodname wherever the decorator call func.__qualname__, instead of the base class?

I do not want to decorate it at the class level since in some cases there are multiple levels of inheritance from standard library classes and I only want to decorate functions that I define myself.


Solution

  • Instead of func.__qualname__ just use the class name along with the __name__ attribute:

    def wrapper(*args, **kwargs):
        self = args[0]  #  this will only work for methods
        ...
        name = "self.__class__.__name__}.{func.__name__}"
        logger.debug(f"function {name} called with args {signature}")
        ...
    

    Of course, if you want the decorator to be applied to "stand-alone" functions, then the first argument (args[0], if it exists at all) won't be a reference to an instance: you have to adapt the code above to check for that, compute name accordingly.