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