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.