Enforcing Patterns and Interfaces - Contracts

Contracts exist to enforce how an interface is being used or extended. Having contracts allows for the integrity of the interface to be maintained despite being extended by third parties.

Having contracts allows for the explicit exposure of specific functions that can then be enforced. Contracts also allows for pre and post processing of function calls. This makes it easy to add extensive input and return validation for extensions of any given sub or plugin.

Contract Structure

When a sub is created, an additional directory called contracts can also be created inside of the same directory as the sub. The contracts directory loads plugins just like a regular sub, but they do not appear on the hub.

When functions are loaded from plugins onto the hub they are wrapped in an object called Contracted. This Contracted object, when called, executes the needed contracts, if any are present. The Contracted object also calls the wrapped function when called. Finally, the Contracted object presents a pass-through interface to the variables or properties that are present on the underlying function. This means that you can use the Contracted function entirely as if it were the real function. This also means that decorators will work for the underlying function as well.

The contract files are laid out in a simple way. If the contracts directory has an init.py file, then those contract functions will apply to all of the plugins in the contracted sub. Otherwise a file with the same virtual name as a plugin likely to be found in the sub will present contracts that will apply to just that plugin.

Contract Signature Enforcement

Contract signatures allow for enforcement of a function’s arguments. To create a contract signature, just make the function in the contract plugin. To contract a function called foo, name the signature function sig_foo, preceding the name of the contracted function, with sig_. Then give the function sig_foo a set of arguments that would be compatible with the intended function. This means that if the function needs a very specific set of options, then they can be defined, with type annotations. If the function can accept arbitrary options, then *args and **kwargs can also be used.

Therefore a contract found in contracts/test.py:

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

Means that a plugin found in test.py can have this function:

def foo(hub, a, b:str, bar="77", quo=55):
    return a

Notice how the signature does nothing. It just has a pass statement. The signature function is never called and needs no code. The foo function follows the sig_foo function but is able to use the leeway presented by the **kwargs option, allowing multiple keyword arguments to be used.

Contract Function Wrappers

Contract function wrappers allow for execution before, instead of, and after functions are called. This can be useful when performing input validation or modifying output of functions, or dynamically adding decorator-like functionality without making developers that extend your framework apply decorators themselves.

These functions follow the same rules as the signature functions, except they use the keywords, pre, call, and post. These functions also receive an added argument called ctx which contains the call context. This call context has different components based on whether the contract wrapper is pre, post, or call. The pre call has *args and **kwargs as passed to the function. The call function receives func which is the underlying function. The post function receives ret which is the return from calling the function and the return from post becomes the overall function call return.