Python: Apply a decorator to a class instance and static methods

Posted November 9, 2021 by Yaroslav Grebnov ‐ 3 min read

Requirements

Automatically apply a decorator to a class instance and static methods.

Solution idea

Use a meataclass providing the required functionality implementation.

Solution description

The implementation consists of two essential parts:

  1. implementing the decorator as a classmethod of the metaclass,
  2. applying the decorator to all the given class methods, including static ones in the metaclass __new__ method.

In order to make the example more illustrative, we will use the decorator which logs the boundaries of a method execution. Two logging levels will be used, INFO and DEBUG. The actual logging level will be determined based on the log_debug class attribute value.

Solution part 1. Decorator

As it was mentioned above, the decorator is the metaclass classmethod:

@classmethod
def decorator(mcs, func, log_level):
    def wrapper(*args, **kwargs):
        log_level(f"Start {func.__func__.__qualname__ if isinstance(func, staticmethod) else func.__qualname__}: "
                  f"{f', args: {args[1:]}' if len(args) > 1 or kwargs else ''}{f', {kwargs}' if kwargs else ''}")
        result = func.__func__(*args, **kwargs) if isinstance(func, staticmethod) else func(*args, **kwargs)
        log_level(f"End {func.__func__.__qualname__ if isinstance(func, staticmethod) else func.__qualname__}")
        return result
    return wrapper

We are logging the method’s __qualname__ and mark the method execution start and end. Static methods we call using func.__func__(*args, **kwargs), instance methods - just func(*args, **kwargs).

Solution part 2. Metaclass new method

def __new__(mcs, name, bases, attrs):
    for attr_name, attr_value in attrs.items():
        if isinstance(attr_value, types.FunctionType) or isinstance(attr_value, staticmethod):
            attrs[attr_name] = mcs.decorator(
                attr_value,
                logging.debug if 'log_debug' in attrs and attrs['log_debug'] else logging.info
            )
    return super().__new__(mcs, name, bases, attrs)

From a given class attributes, we take the instance and static methods:

if isinstance(attr_value, types.FunctionType) or isinstance(attr_value, staticmethod):

The decorator is applied to the selected class attributes. As it was mentioned before, logging level is determined based on the log_debug class attribute value:

attrs[attr_name] = mcs.decorator(
    attr_value,
    logging.debug if 'log_debug' in attrs and attrs['log_debug'] else logging.info
)

Usage example

The metaclass can be used in the following way:

  1. logging at INFO level:
class SomeClass(metaclass=LogMethods)
  1. logging at DEBUG level:
class SomeOtherClass(metaclass=LogMethods):
    log_debug = True

Complete listing

import types
import logging


class LogMethods(type):
    """A metaclass to be used to log class methods' execution boundaries.
    Default logging level is INFO. Logging level can be switched to DEBUG by setting
    the class log_debug attribute to 'True'."""

    def __new__(mcs, name, bases, attrs):
        for attr_name, attr_value in attrs.items():
            if isinstance(attr_value, types.FunctionType) or isinstance(attr_value, staticmethod):
                attrs[attr_name] = mcs.decorator(
                    attr_value,
                    logging.debug if 'log_debug' in attrs and attrs['log_debug'] else logging.info
                )
        return super().__new__(mcs, name, bases, attrs)

    @classmethod
    def decorator(mcs, func, log_level):
        def wrapper(*args, **kwargs):
            log_level(f"Start {func.__func__.__qualname__ if isinstance(func, staticmethod) else func.__qualname__}: "
                      f"{f', args: {args[1:]}' if len(args) > 1 or kwargs else ''}{f', {kwargs}' if kwargs else ''}")
            result = func.__func__(*args, **kwargs) if isinstance(func, staticmethod) else func(*args, **kwargs)
            log_level(f"End {func.__func__.__qualname__ if isinstance(func, staticmethod) else func.__qualname__}")
            return result
        return wrapper

In case you have a question or a comment concerning this post, please send them to: