Decorators

decorators provides base classes for creating reusable decorators. The core of this is the Decorator class, which detects whether a decorator is being applied to a function, method, or class, and calls the appropriate method, _wrap_fn_h, _wrap_method_h, or _wrap_fn_h. These preserve type variables by using the stdlib’s ParamSpec.

Use of functools.wrap is not needed, as the Decorator handles that.

In addition, if the same decorator is applied repeatedly with different data, that can be detected, and only a single decoration will be applied, the data being added to the target’s __dict__.

from jgdv.decorators import Decorator

class MyDecorator(Decorator):

    def _wrap_fn_h[**In](self, fn:Func[In, int]) -> Decorated[In, int|None]:

        def myfunc(*vals:In.args) -> int|None:
            if bool(vals[0]):
                return fn(*vals)
            return None

        return myfunc


@MyDecorator()
def a_func(val:int) -> int:
        return 2

assert(a_func(0) is None)
assert(a_func(5) is 2)

Specialised Decorators

The Point of this module is to create certain decorator tools to simplify certain styles of decorators, such as:

  • Monotonic Decorators, that intentionally stack.

  • Idempotent Decorators, that only apply to a function once.

  • Metadata Decorators, that only add annotations without changing runtime behaviour.

  • Data Decorators, that update function data for an idempotent decorator to work with.

One example is the @DKeyed decorator in jgdv.structs.dkey. This adds key data to a function, so when the function is called, values are extracted from data and provided as arguments. It is better to have one extraction function (an Idempotent decorator), but with Monotonic data decoration.

Mixin

@Mixin is a decorator to apply mixin classes into the MRO of a base class.

class BaseClass: ...
class Mixin1: ...
class Mixin2: ...


# Instead of this:
class MyClass(Mixin1, Mixin2, BaseClass): ...

# This:
from jgdv.decorators import Mixin


@Mixin(Mixin1, Mixin2)
class MyClass2(BaseClass): ...

Proto

@Proto is similar to Mixin, but for annotating Protocols explicitly, with optional definition-time checking that all required methods are implemented. This ensures that if a protocol changes, implementing classes will notify when they no longer fulfill the contract:

from typing import Protocol
from jgdv.decorators import Proto

class MyProto_p(Protocol): ...


# Instead of:
class MyInstance(MyProto_p): ...

# This:
@Proto(MyProto_p)
class MyInstance2: ...