Contracts

One of the core components of POP is the contract system. When everything is a plugin then these plugins need to be able to enforce the interface they belong to. POP provides contracts to enable these interfaces. Contracts can be used to define plugin interfaces, ensure that functions that are needed are available, and that needed functions follow argument signatures. Contracts can also define transparent wrappers, running pre or post functions around function calls, or replacing the actual function call.

Signature System

The signature system allows for function signatures to be enforced in plugins via contracts. This means that a plugin the implements a contract will be forced to implement the named functions and follow the restrictions inside defined by the function signature.

In a nutshell the signature system allows for contracts to define the implementation interface for plugins.

If this file is defined as the contract contracts/red.py

def sig_foo(hub, a, b: str):
    pass

Then the file red.py is forced to create a function with a compatible signature

def foo(hub, a, b: str):
    return a + b

The signature system also verifies *args and **kwargs conditions, parameter names, types and annotations.

So this sig contract:

def sig_foo(hub, a, b, **args):
    pass

Will work with this function:

def foo(hub, a, b, c, d):
    return a + b + c + d

Because the contract allows arbitrary *args, but in this example the contract will mandate that a and b are defined.

Similarly **kwargs will pass through:

def sig_foo(hub, a, b, c=4, **kwargs):
    pass

Which allows a function like this:

def foo(hub, a, b, c, d=5, e="foo"):
    return a

Since the sig function in the contract allows **kwargs, the function can have **kwargs.

Similarly, if the sig function does not have **kwargs then additional parameters are NOT allowed beyond what is defined in the sig.

Wrappers

Contracts allow for functions to be wrapped. This allows for external validators to be enforced, for parameters to be validated, and data to me manipulated.

The available wrappers are pre, post, and call. When these are included in a contract they will be called when the function is called.

Module and Function Wrappers

When creating wrappers, they can be applied to all functions in a module or they can be applied to specific functions. To make a module level wrapper, just make a single function with the wrapper type name:

def pre(hub, ctx):
    pass

This function will now be executed for every function called in the corresponding plugin.

A wrapper can also be made to be specific to a function by using the same function name, just prepend the function name with the name of the wrapper to use, as in pre_:

def pre_foo(hub, ctx):
    pass

Pre

When using pre the contract function will be executed before the module function. The pre function receives the hub and a ctx object. The ctx object is used to contain the context of the call. This ctx object has access to args and kwargs for the function call:

def pre(hub, ctx):
    if len(ctx.args) > 1:
        raise ValueError("No can haz args!")
    if ctx.kwargs:
        raise ValueError("No can haz kwargs!")

Call

The call wrapper can be used to replace the actual execution of the function. When call is used the underlying function is not called, it needs to be called inside of the call function. This function can be useful when you want to have conditions around weather to call a function, or to have a full context around the wrapping of the function. The function object is included in the ctx:

def call(hub, ctx):
    return ctx.func(*ctx.args, **ctx.kwargs)

Post

The post wrapper allows for the return data from the function to be handled. This can be useful if your function(s) need to modify or validate return data. The return data from the post function is the return data send back when the function is called.

def post(hub, ctx):
    ret = ctx.ret
    if isinstance(ret, list):
        ret.append("post called")
    elif isinstance(ret, dict):
        ret["post"] = "called"
    return ret

Using the contracts Directory

Contracts can be added to a sub by just adding a subdirectory called contracts into the directory containing the sub plugins. So if you have a sub called rpc then the contracts directory would be rpc/contracts.

Inside the contracts directory the name of the modules will map to the name of the plugin in the corresponding sub. The virtualname of the contract module is also honored and will override the file name in the same way that virtualname will override the file name in standard plugin modules.

This means that if you want a contract for a module called red then the file: contracts/red.py will apply for the module red.py. Similarly if you want a single contract to be applied to multiple plugins the implement the red interface just call the contract module red and then have the modules that implement the interface take the red virtualname.

Using __contracts__

A plugin can also volunteer itself to take on a specific contract or a list of contracts. This can be done with the __contracts__ value at the top of a plugin module.

__contracts__ = ["red", "blue", "green"]

All of the contract wrappers and sigs will be enforced and called. If multiple wrappers are defined for a given function then they will be called in the order in which they are defined in the __contracts__ variable.

Subsystem Wide Contracts

Sometimes it makes sense to enforce the same contract over an entire subsystem. This can be useful when the pattern you are using exposes many ways to accomplish the same task, like many back ends to a database, or many ways to read in different types of files.

To make a subsystem wide contract just make an init.py file in your contratcs directory. That init.py contract will now be applied to all modules in the subsystem.