Search code examples
pythonpython-3.xclassdecoratorpython-decorators

Get the parent class name of a function with a decorator


Let there be a class with functions:

class Tester:
    
    @Logger()
    def __init__(self):
        print(__class__)
    
    @Logger()
    def func(self, num):
        return num**2

where Logger is a decorator roughly defined as:

from typing import Optional, Any
from logging import getLogger

class Logger:
    def __init__(self):
        self.logger = getLogger()
        self.logging_function = getattr(self, 'function')
        
    def __call__(self, decorator: callable):
        
        def f(*args, **kwargs):
            return self.logging_function(decorator, *args, **kwargs)
        
        return f
    
    def function(self, func: callable, *args: Optional[Any], **kwargs: Optional[Any]):
        func_name = Logger.get_name(func)
        self.logger.info(f"Starting: {func_name}.")
        return func(*args, **kwargs)
    
    @staticmethod
    def get_name(func):
        return f'__init__ {func.__class__.__name__}' if func.__name__ == '__init__' else func.__name__

How can we edit the Logger get_name function, such that if the function being run is a class __init__ that the name returned is __init__ Tester, but if the function is named something else it merely returns the function __name__?

(AKA) Expected output:

>>> test = Tester()
INFO: Starting __init__ Tester.
<class '__main__.Tester'>

>>> test.func(3)
INFO: Starting func. 
9

Current Output:

>>> test = Tester()
INFO: Starting __init__ function.
<class '__main__.Tester'>

>>> test.func(3)
INFO: Starting func. 
9

Solution

  • You could use the qualified name instead like this:

    @staticmethod
    def get_name(func):
        return func.__qualname__
    

    Which will give you something like:

    >>> test = Tester()
    INFO:Starting: Tester.__init__.
    <class '__main__.Tester'>
    >>> test.func(3)
    INFO:Starting: Tester.func.
    9
    

    You might also be interested in the standard LogRecord attribute funcName, which does a similar thing from within the function. A basic demo of that:

    import logging
    logging.basicConfig(
        level=logging.INFO,
        format="%(levelname)s (%(funcName)s) %(message)s",
    )
    
    log = logging.getLogger()
    
    class A:
        def my_method(self):
            log.info("hello")
    
    def bar():
        log.info("world")
    
    A().my_method()
    bar()
    

    Outputs this:

    INFO (my_method) hello
    INFO (bar) world