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

Introduction

decorules is a tiny python library that seeks to enforce rules on class structure and instance behavior for classes and class hierarchies through decorators at the point of class declaration.

The primary objective is to help library developers enforce behavior on derived classes. Secondary benefits are defensive measures, for instance 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).

By default, rules enforced on instances are enforced on creation of an instance only. It is possible to enforce these rules on any member function call by using a decorator on the method.

Installation

decorules was built using python 3.10. It is available as a package on pypi and can be installed through pip:

pip install decorules

Should you require an installation of pip, follow the instructions on the pip website.

Examples

A worked out example of a class hierachry can be found under src/example, with library_class.py and client_class.py representing the library and client respectively.

Further examples can be found under the test directory.

The aim here is to simply walk through some simple examples to demonstrate usage.

Firstly, suppose we wish to enforce that a (base) class or an instance of the class 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_enforcer
def key_type_enforcer(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 will use partial from the functools package. For restrictions on classes that do not check the values of attributes predicate functions can be provided. If the rule on the class does make use of such a value (e.g., check if a static float is positive), the function must take 2 arguments and return a boolean. The second argument should always default to None[^1].
  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. Both decorators take 1 compulsory argument (the function from step 2. which returns a True/False value) and 2 optional arguments, the first is the type of the exception to be raised should the rule not hold[^2] and the second optional argument is a string providing extra information when the exception is raised.
  3. The rules on instances are only applied after the call to __init__. We have the option to add the enforces_instance_rules decorator to any method of the class, thereby enforcing the instance rules after each method call.

In order to guarantee that the class (and its derived classes) implements a function named library_functionality we would implement:

from decorules import HasEnforcedRules
import types
from functools import partial

@raise_if_false_on_class(partial(key_type_enforcer, 
                                 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 an int member named x existed after every instantiation:

@raise_if_false_on_instance(partial(key_type_enforcer, enforced_type=int, enforced_key='x'), AttributeError)  
@raise_if_false_on_class(partial(key_type_enforcer, 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 calls would throw an AttributeError:

a = HasCorrectMethodAndInstanceVarClass()
b = HasCorrectMethodAndInstanceVarClass(25)
c = HasCorrectMethodAndInstanceVarClass(5)

For forcing the member 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_enforcer, enforced_type=int, enforced_key='x'), AttributeError)  
@raise_if_false_on_class(partial(key_type_enforcer, 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 will be prependend to the message of the exception. For the implementation above, 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 set had a minimum 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_SET', 
                                 min_counter = Counter({str: 1, int: 2, float:1})), 
                         AttributeError)
class HasClassLevelMemberTypeCheckClass(metaclass=HasEnforcedRules):
    STATIC_SET = ("Test", 10, 40, 50, 45.5, 60.0, '3', 'i', BaseException())

If we wanted to raise an exception as soon as a member value reaches the value 10 during the course of the process:

@raise_if_false_on_instance(lambda x: x.y<10, ValueError)
class HasMethodCheckedAndFailsAfterCall(metaclass=HasEnforcedRules):
    def __init__(self, value=20):
        self.y = value
    @enforces_instance_rules
    def add(self, value=0):
        self.y += value

a = HasMethodCheckedAndFailsAfterCall(0)
a.add(1)
a.add(1)
a.add(1)
a.add(10)  # will raise a ValueError

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.

Though not intended for this use, the enforced rules (through predicate functions) are available through the EnforcedFunctions static class and can thus be retrieved, applied and transferred at any point in the code.

[^1]: The second argument will be used to examine class attributes when required. Note that by always providing a second argument and defaulting it to None (as was done in key_type_enforcer), the function can be used both on instances and class declarations. [^2]: Note that this is an exception type and not an instance. For rules on classes this defaults to AttributeError, for rules of instantiation this defaults to ValueError. Other exceptions or classes (including user defined ones) can be supplied, provided instances can be constructed from a string

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.7.tar.gz (12.4 kB view details)

Uploaded Source

Built Distribution

decorules-0.0.7-py3-none-any.whl (10.9 kB view details)

Uploaded Python 3

File details

Details for the file decorules-0.0.7.tar.gz.

File metadata

  • Download URL: decorules-0.0.7.tar.gz
  • Upload date:
  • Size: 12.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.11.1

File hashes

Hashes for decorules-0.0.7.tar.gz
Algorithm Hash digest
SHA256 8844153bf244f9043fe39ee96e4fb5ed337b5a74f662b84e26aead30b48a95cd
MD5 b2c065f373e1b59b5f00eb56e7a7c2cb
BLAKE2b-256 82ba1e2e1fd3f7e80f9642fb793568ed637baf4df2dd845641eb8d97a5a59268

See more details on using hashes here.

File details

Details for the file decorules-0.0.7-py3-none-any.whl.

File metadata

  • Download URL: decorules-0.0.7-py3-none-any.whl
  • Upload date:
  • Size: 10.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.11.1

File hashes

Hashes for decorules-0.0.7-py3-none-any.whl
Algorithm Hash digest
SHA256 b6200b2898790ade7ecbe9f4b6af1c3efb01fc090605a70cf1c11e35b82333ff
MD5 6663d5d3cf80007550e9244d65af5482
BLAKE2b-256 eb5e6f2dac56b8b03a5e64ca8c07a49c5fb2679c78ca807c4be4fcd14ac10f11

See more details on using hashes here.

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