Skip to content

Hassle-free creation of decorators for functions and methods, OO-style.

License

Notifications You must be signed in to change notification settings

jorenham/classy-decorators

Repository files navigation

Classy Decorators

PyPI version

Hassle-free creation of decorators for functions and methods, OO-style.

Features

  • One decorator to rule them all; it works on functions, methods, classmethods and staticmethods.
  • Easily define parameters with a dataclass-like annotated attribute syntax.
  • Runtime type-checking of decorator paremeters.
  • For decorators with all-default or no parameters, parentheses are optional: @spam() == @spam
  • Inheritance is supported.
  • Any decorator parameters and instance attributes are accessible (as copies) in bound methods as well; no need to worry about those pesky descriptors.
  • MyPy compatible and 100% test coverage.

Dependencies

Python 3.8 or 3.9.

Install

pip install classy-decorators

for better runtime type checking:

pip install classy-decorators[typeguard]

Usage

The following code can also be found in the examples.

Create a decorator by subclassing classy_decorators.Decorator. You can override the the decorated function or method using the __call_inner__ method (__call__ is meant for internal use only and should not be used for this).

Simple decorator

from classy_decorators import Decorator

class Double(Decorator):
    def __call_inner__(self, *args, **kwargs) -> float:
        return super().__call_inner__(*args, **kwargs) * 2

To see it in action, let's decorate a function:

@Double
def add(a, b):
    return a + b

assert add(7, 14) == 42

You can also decorate methods and classmethods:

class AddConstant:
    default_constant = 319

    def __init__(self, constant=default_constant):
        self.constant = constant

    @Double
    def add_to(self, value):
        return value + self.constant

    @Double
    @classmethod
    def add_default(cls, value):
        return value + cls.default_constant

assert AddConstant(7).add_to(14) == 42
assert AddConstant.add_default(14) == 666

Decorator parameters

Our Double decorator is pretty nice, but we can do better! So let's create a decorator that is able to multiply results by any number instead of only by 2:

class Multiply(Decorator):
    factor: int

    def __call_inner__(self, *args, **kwargs) -> float:
        return super().__call_inner__(*args, **kwargs) * self.factor

By simply setting the type-annotated factor attribute, we can use it as decorator parameter. If you are familiar with dataclasses, you can see that this is very similar to defining dataclass fields.

@Multiply(2)
def add_and_double(a, b):
    return a + b

@Multiply(factor=3)
def add_and_triple(a, b):
    return a + b

assert add_and_double(8, 15) == 46
assert add_and_triple(8, 15) == 69

Default parameters and inheritance

It's classy to be DRY, so let's combine our Double and Multiply decorators into one that multiplies by 2, unless specified otherwise:

class DoubleOrMultiply(Multiply):
    factor = 2

@DoubleOrMultiply
def add_and_double(a, b):
    return a + b

@DoubleOrMultiply(factor=3)
def add_and_triple(a, b):
    return a + b

assert add_and_double(7, 14) == 42
assert add_and_triple(8, 15) == 69

Advanced dataclass methods

The Decorator base class provided, aside from __call_inner__, two other interface methods you can override:

  • Decorator.__decorate__(self, **params), which is called just after a function method is decorated, with all decorator parameter values or defaults as keyword arguments, i.e. DoubleOrMultiply.__decorate__(self, factor: int = 2).
  • Decorator.__bind__(self, instance_or_class), which is called when a method (not for functions), is bound to an instance, or when a class/static method is bound to a class.

Additionally, these properties can be used for figuring out what's been decorated:

  • is_function
  • is_method; either an instance, class- or static method
  • is_instancemethod
  • is_classmethod
  • is_staticmethod

And for methods, is_bound and is_unbound are provided.

If you're looking for the original wrapped function, you can find it at __func__.


Classy, eh?