Skip to main content

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:

  1. 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
  1. 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 the functools package). For restrictions on classes, the function must take 2 arguments and return a boolean. The second argument should always default to None, it will be used to examine class attributes should this be required. Note that by defaulting the last argument to None in key_type_enforcer1, the function can be used both on instances and class declarations.
  2. Use the decorator raise_if_false_on_class when enforcing a rule on a class level, or raise_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

decorules-0.0.3.tar.gz (9.7 kB view hashes)

Uploaded Source

Built Distribution

decorules-0.0.3-py3-none-any.whl (9.2 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page