Clickify parameters#
For any given command-line tool, most of the information in the cab schema (i.e. argument names and types, help strings) directly mirrors that already provided to the tool’s command-line parser. When wrapping a third-party package in a cab, this leads to an unavoidable duplication of effort (with all the attendant potential for inconsistencies) – after all, the package developer has already implemented their own command-line interface (CLI) parser, and this CLI needs to be described to Stimela. Note, however, that the schema itself provides all the information that would be needed to construct a CLI in the first place. For newly-developed packages, this provides a substantial labour-saving opportunity. Stimela includes a utility function that can convert a schema into a CLI using the click package. For a notional example, consider this
hello_schema.yml file defining a simple schema with two inputs:
inputs:
name:
dtype: str
info: Your name
required: true
policies:
positional: true
count:
dtype: int
default: 1
info: Number of greetings
This file can be instantly converted into a CLI as follows:
#!/usr/bin/env python
import click
from scabha.schema_utils import clickify_parameters
@click.command()
@clickify_parameters("hello_schema.yml")
def hello(count, name):
"""Simple program that greets NAME for a
total of COUNT times."""
for x in range(count):
print(f"Hello {name}!")
if __name__ == '__main__':
hello()
The resulting tool now has a fully-functional CLI:
$ ./hello.py --help
Usage: hello.py [OPTIONS] NAME
Simple program that greets NAME for a total
of COUNT times.
Options:
--count INTEGER Number of greetings
--help Show this message and exit.
To integrate the tool into Stimela, all we need is a cab definition, which can directly include the schema file:
cabs:
hello:
_include: hello_schema.yml
command: hello.py
This mechanism ensures that all inputs and outputs need only be defined by the developer once, in a single place – and provides both a CLI and Stimela integration with no additional effort, while ensuring that these are mutually consistent by construction. The QuartiCal, pfb-imaging and breizorro packages, for example, make extensive use of this.
In the above example, clickify_parameters() is passed a filename to read the schema from. An alternative to this is to pass it a Dict containing inputs, outputs and (optionally) policies sections (see Parameter policies reference). One can also pass a second argument containing a Dict of policies that will override the policies in the first Dict. This is useful when you ship a package containing full cab definitions, and want to read the schemas directly from the latter. Here we combine it with click’s subcommand feature:
import click
from scabha.schema_utils import clickify_parameters
from omegaconf import OmegaConf
schemas = OmegaConf.load(os.path.join(os.path.dirname(__file__), "cabs/mypackage.yml"))
@cli.command("hello",
help=_schemas.cabs.get("hello-world").info,
no_args_is_help=True)
@clickify_parameters(_schemas.cabs.get("hello-world"))
def hello_world(name, count):
for x in range(count):
print(f"Hello {name}!")
where mypackage.yaml contains:
cabs:
hello-world:
info: Greets NAME for a total of COUNT times
inputs:
name:
dtype: str
info: Your name
required: true
policies:
positional: true
count:
dtype: int
default: 1
info: Number of greetings
If your package defines multiple commands, it can be useful to create a new decorator that you can then reuse for multiple functions:
import click
from scabha.schema_utils import clickify_parameters
from omegaconf import OmegaConf
def clickify(command_name, schema_name=None):
schema_name = schema_name or command_name
return lambda func: \
cli.command(command_name, help=schemas.cabs.get(schema_name).info, no_args_is_help=True)(
clickify_parameters(schemas.cabs.get(schema_name))(func)
)
@clickify("hello", "hello-world"):
def hello_world(name, count):
for x in range(count):
print(f"Hello {name}!")