WolVes
WolVes

Reputation: 1336

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

Upvotes: 3

Views: 1193

Answers (3)

wim
wim

Reputation: 362657

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

Upvotes: 3

Sabil
Sabil

Reputation: 4510

You can use the following function to get function name.

get_name Code Snippet to Produce Your Desire Output:

@staticmethod
    def get_name(func):
        func_name = func.__qualname__.split('.') if '__init__' in func.__qualname__ else func.__name__
        func_name = ' '.join(func_name[::-1]) if isinstance(func_name, list) else func_name
        return func_name

Output:

Starting: __init__ Tester.
<class '__main__.Tester'>
Starting: func.

If you want more generic then you can use the following function.

get_name Generic Code Snippet:

@staticmethod
    def get_name(func):
        func_name = func.__qualname__.split('.') if '.' in func.__qualname__ else func.__name__
        func_name = ' '.join(func_name[::-1]) if isinstance(func_name, list) else func_name
        return func_name

Output:

Starting: __init__ Tester.
<class '__main__.Tester'>
Starting: func Tester.

Complete Working Code As You Desired:

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):
        func_name = func.__qualname__.split('.') if '__init__' in func.__qualname__ else func.__name__
        func_name = ' '.join(func_name[::-1]) if isinstance(func_name, list) else func_name
        return func_name


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

test = Tester()
test.func(3)

Output:

Starting: __init__ Tester.
<class '__main__.Tester'>
Starting: func.

Upvotes: 1

0x5453
0x5453

Reputation: 13589

Starting with Python 3.3, you should be able to use func.__qualname__ to get there.

I personally would keep it simple for consistency:

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

But to exactly match your expected output, I believe this will work:

@staticmethod
def get_name(func):
    if '.__init__' in func.__qualname__:
        class_name = func.__qualname__.split('.')[-2]  # this prints the innermost type in case of nested classes
        return f'__init__ {class_name}'
    return func.__name__

Upvotes: 2

Related Questions