Skip to main content

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

· 3 min read
Yaroslav Grebnov
Golang developer, SDET

Use Python metaclass to automatically apply a decorator to a class instance and static methods.

Implementation 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.

Implementation 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).

Implementation 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 examples

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: [email protected].