Protocols
coma
uses a hook pipeline, both to implement its
default behavior and to enable customization. To make this work, the various
types of hook must each follow a pre-defined protocol
(i.e., function signature) for both their parameters and their return value.
The hook protocols are fairly similar across all hook types, but there are a number of variations depending on the type. We begin by enumerating the shared aspects of the various protocols.
Function Signature
All protocols require the corresponding hook function signatures to define positional-or-keyword parameters. For example, we can define and invoke some (hypothetical) hook function as:
def some_hook(a, b, c):
...
some_hook(a=..., b=..., c=...)
This is fine because the hook function parameters are defined as positional-or-keyword. These requirement is needed because hook parameters are bound positionally by keyword.
Note
Technically, the requirement is that the hook function parameters are not positional-only, not variadic (positional or keyword), and not keyword-only. In practice, that means they have to be positional-or-keyword.
In addition to the above, all protocol parameters must be:
Defined in the hook function’s signature.
Ordered correctly in the hook function’s signature.
Named (i.e., spelled) correctly in the hook function’s signature.
Protocol Parameters
- generic_protocol(name, parser, known_args, unknown_args, command, configs, result)
Here, we list all possible protocol parameters, in the order in which they should be defined in the hook function’s signature.
Note
Not every type of hook uses every protocol parameter. The protocols specific to each hook type are listed below. None of these specific differences affect the parameter ordering or naming shown here.
- Parameters
name (str) – The name given to a command when it is
register()
ed.parser (argparse.ArgumentParser) – The
ArgumentParser
created to add command line arguments for a specific command when it isregister()
ed and subsequently parse actual command line arguments if the command is invoked on the command line.known_args (argparse.Namespace) – The
Namespace
object (i.e., first object) returned by parse_known_args() if a specific command is invoked on the command line.The
list
object (i.e., second object) returned by parse_known_args() if a specific command is invoked on the command line.command (Union[Callable, Any]) –
The command object itself that was
register()
ed if it is invoked on the command line.Note
If the
register()
ed object was a class, it is left unchanged. If theregister()
ed object was a function, it is implicitly wrapped in another object. The main init hook is meant to instantiatecommand
.Warning
Never make decisions based on the
type
ofcommand
, since it may be implicitly wrapped. Instead, usename
, which is guaranteed to be unique across allregister()
ed commands.A dictionary of identifier-configuration pairs representing all configs (both global and local) bound to a specific command if it is invoked on the command line.
Note
Before the main config hook, the values in the
configs
dictionary are assumed to be uninitialized config objects. Afterwards, they are assumed to be initialized config objects.result (Any) – The value returned from executing the command if it is invoked on the command line.
- Returns
Some protocols return values; others do not. See below for details on each protocol.
- Return type
@hook
Decorator
For many hooks, only a subset of the corresponding protocol parameters are needed
to implement their logic. It can therefore be cumbersome to define a function with
multiple unused parameters just to satisfy the hook protocol. The @hook
decorator (link: hook()
) solves this problem, as it allows
hook functions to be defined with a subset of the protocol parameters. For example:
@coma.hooks.hook
def name_hook(name):
...
defines a hook that only requires the command’s name
and ignores all
other protocol parameters.
Note
The @hook
decorator only alleviates the requirement that all protocol
parameters are defined in the hook function’s signature. Other requirements,
such as having the correct ordering and spelling of parameters, remain active.
sequence()
Function
Each type of hook must be implemented as a single function. However, it is often beneficial to decompose a large hook function into a series of smaller ones. These component functions must then be wrapped with a higher-order function that executes them in order, while binding all parameters using keywords.
While this wrapping can always be done manually, a convenience wrapper,
sequence()
, can be used when all hooks share the exact same
function signature (or are wrapped in the @hook
decorator) to abstract
away some of the minutiae. Compare:
wrapper = coma.hooks.sequence(
coma.hooks.parser_hook.factory("-a", type=int, default=123),
coma.hooks.parser_hook.factory("-b", type=int, default=456),
)
coma.register(..., parser_hook=wrapper)
with:
@coma.hooks.hook
def wrapper(parser):
coma.hooks.parser_hook.factory("-a", type=int, default=123)(parser=parser)
coma.hooks.parser_hook.factory("-b", type=int, default=456)(parser=parser)
coma.register(..., parser_hook=wrapper)
The former isn’t shorter, but it removes the minutiae of adding
(parser=parser)
to each wrapped hook function and removes the need to
decorate the wrapper function with the @hook
decorator.
Specific Protocols
Here, we list the specific protocol and intended semantics for each type of hook. See Protocol Parameters for details on each parameter.
Parser
- parser_hook_protocol(name, parser, command, configs)
- Semantics
This protocol adds command line arguments using
parser
.- Returns
The return value of a parser hook (if any) is always ignored.
- Return type
None
Pre Config
- pre_config_hook_protocol(name, known_args, unknown_args, command, configs)
- Semantics
This protocol is the first invocation hook to be executed.
- Returns
The return value of a pre config hook (if any) is always ignored.
- Return type
None
Config
- config_hook_protocol(name, known_args, unknown_args, command, configs)
Post Config
- post_config_hook_protocol(name, known_args, unknown_args, command, configs)
Pre Init
- pre_init_hook_protocol(name, known_args, unknown_args, command, configs)
- Semantics
This protocol’s hook is executed after all the config hooks and before the main init hook.
- Returns
The return value of a pre init hook (if any) is always ignored.
- Return type
None
Init
- init_hook_protocol(name, known_args, unknown_args, command, configs)
- Semantics
This protocol instantiates
command
using theconfigs
, returning the resulting instance object.Note
If the
register()
ed command object was a class, it was left unchanged. If theregister()
ed command object was a function, it was implicitly wrapped in another object. Either way,command
acts as though it is a class object that can be instantiated.- Returns
The return value of an init hook is an instantiated command object.
- Return type
Post Init
Pre Run
- pre_run_hook_protocol(name, known_args, unknown_args, command, configs)
- Semantics
This protocol’s hook is executed after all the config and init hooks and before the main run hook.
- Returns
The return value of a pre run hook (if any) is always ignored.
- Return type
None
Run
Post Run
- post_run_hook_protocol(name, known_args, unknown_args, command, configs, result)
- Semantics
This protocol is the last invocation hook to be executed.
- Returns
The return value of a post run hook (if any) is always ignored.
- Return type
None