decorules is a tiny library that seeks to enforce class structure and instance behaviour on classes and their derived classes through decorators at class declaration time
Project description
decorules
decorules is a tiny library that seeks to enforce class structure and instance behaviour on classes and their derived classes through decorators at class declaration time.
The aim is to help library developers enforce behavior on derived classes and as a defensive measure, to quickly have an overview of any structural rules enforced on the class.
The rules are specified through decorators on the class declaration and using the metaclass HasEnforcedRules from the library. Enforcement of the rules is done by throwing exceptions (which can be developer specified) when a predicate function fails on the class or an instance (depending on the decorator used).
In the case of rules enforced on instances, these are enforced on creation of an instance only. The rules are available throughout and can thus be applied at any point in time.
Simple Examples
Further examples can be found under the test directory. Let us start with a requirement that a class or an instance must have an attribute of a certain type. Here are the basic steps:
- Create a function that takes a class or an instance and checks whether an attribute exists and is of the correct type. In the example, this function is
key_type_enforcer1
def key_type_enforcer1(instance_or_type,
enforced_type: type,
enforced_key: str,
attrs: dict = None):
member_object = getattr(instance_or_type, enforced_key, None)
if member_object is None:
if attrs is not None:
member_object = attrs.get(enforced_key, None)
if member_object is None:
return False
else:
return issubclass(type(member_object), enforced_type)
pass
- For restrictions on instances, the function must be predicate. This means the function takes one argument (the instance) and returns a boolean. Functions can be turned into predicates using different methods, in this example we use
partial
from thefunctools
package). For restrictions on classes, the function must take 2 arguments and return a boolean. The second argument should always default toNone
, it will be used to examine class attributes should this be required. Note that by defaulting the last argument toNone
inkey_type_enforcer1
, the function can be used both on instances and class declarations. - Use the decorator
raise_if_false_on_class
when enforcing a rule on a class level, orraise_if_false_on_instance
when enforcing upon instantiation.
To guarantee that a new class (and its derived classes) implements a function named library_functionality
:
from decorules import HasEnforcedRules
import types
from functools import partial
@raise_if_false_on_class(partial(key_type_enforcer1, enforced_type=types.FunctionType, enforced_key='library_functionality'), AttributeError)
class HasCorrectMethodClass(metaclass=HasEnforcedRules):
def library_functionality(self):
return 1
If in addition, we ensure that every instance had an int
member x
:
@raise_if_false_on_instance(partial(key_type_enforcer1, enforced_type=int, enforced_key='x'), AttributeError)
@raise_if_false_on_class(partial(key_type_enforcer1, enforced_type=types.FunctionType, enforced_key='library_functionality'), AttributeError)
class HasCorrectMethodAndInstanceVarClass(metaclass=HasEnforcedRules):
def __init__(self, value=20):
self.x = value
def library_functionality(self):
return 1
Should the __init__
implementation not set self.x
or remove it using del self.x
, all of the following instantitiation would throw an AttributeError
:
a = HasCorrectMethodAndInstanceVarClass()
b = HasCorrectMethodAndInstanceVarClass(25)
c = HasCorrectMethodAndInstanceVarClass(5)
For forcing x
to be larger than 10:
@raise_if_false_on_instance(lambda ins: ins.x > 10, ValueError, "Check x-member>10")
@raise_if_false_on_instance(partial(key_type_enforcer1, enforced_type=int, enforced_key='x'), AttributeError)
@raise_if_false_on_class(partial(key_type_enforcer1, enforced_type=types.FunctionType, enforced_key='library_functionality'), AttributeError)
class HasCorrectMethodAndInstanceVarCheckClass(metaclass=HasEnforcedRules):
def __init__(self, value=20):
self.x = value
def library_functionality(self):
return 1
Note the third argument in the decorator, this optional argument is prependend to the message of the exception. Here it is used to provide more explanation on the predicate. Only the third line would raise an exception:
a = HasCorrectMethodAndInstanceVarCheckClass()
b = HasCorrectMethodAndInstanceVarCheckClass(25)
c = HasCorrectMethodAndInstanceVarCheckClass(5) # a ValueError is raised
If we wanted to ensure that a static list had a number of instances of each type (e.g., 1 string
, 2 int
and 1 float
):
from collections import Counter
from collections.abc import Iterable
def min_list_type_counter(instance_or_type,
list_name: str,
min_counter: Counter,
attrs: dict = None):
member_object = getattr(instance_or_type, list_name, None)
if member_object is None:
if attrs is not None:
member_object = attrs.get(list_name, None)
if member_object is None:
return False
else:
if isinstance(member_object, Iterable):
return Counter(type(x) for x in member_object) >= min_counter
else:
return False
@raise_if_false_on_class(partial(min_list_type_counter, list_name='STATIC_LIST', min_counter = Counter({str: 1, int: 2, float:1})), AttributeError)
class HasClassLevelMemberTypeCheckClass(metaclass=HasEnforcedRules):
STATIC_LIST = ("Test", 10, 40, 50, 45.5, 60.0, '3', 'i', BaseException())
When using multiple decorators in general, one must be aware that the order of decorator matters with decorator closest to the function/class applied first. With multiple decorator we must also avoid clashes between decorators.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for decorules-0.0.3-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | dd06ca1e5e7f4e4f0e77b6a41cdf1ae9d0ad4b860831e868a112fdc521710cb6 |
|
MD5 | bd37dae68b905cd7d5e8c940f14f1dd6 |
|
BLAKE2b-256 | e11d416f9fc3260d0ebadaa6ebcc3aecf1bc438a6f41272e057955ba7c3476de |