mirror of
https://github.com/OpenFreeEnergy/openfe.git
synced 2026-06-04 22:34:24 +08:00
129 lines
5.8 KiB
Markdown
129 lines
5.8 KiB
Markdown
# Contributing CLI Subcommands
|
|
|
|
Adding a new subcommand to the `openfe` CLI is pretty straightforward, but
|
|
there are some best practices that will make your contribution easier to
|
|
maintain.
|
|
|
|
## How the CLI finds subcommands
|
|
|
|
Subcommands are registered with the CLI based on the existence of an instance
|
|
of `CommandPlugin` in modules located in particular directories or namespaces.
|
|
This means that after you create the command function, you need to wrap it in a
|
|
`CommandPlugin`, which must be assigned to a variable name. The variable name
|
|
itself is unimportant (I usually use `PLUGIN`). It's perfectly fine to include
|
|
more than one plugin in the same file, but the each must have a different
|
|
variable name.
|
|
|
|
The allowed locations for command plugins may change, but currently includes:
|
|
|
|
* modules located in the namespace `openfecli.commands`
|
|
|
|
When contributing to the core CLI, all you need to do is add your subcommand
|
|
module to the `openfecli/commands/` directory, and the CLI should register it
|
|
automatically.
|
|
|
|
## Best practices
|
|
|
|
### The CLI should be a thin wrapper around the library
|
|
|
|
The intent of the CLI is to provide convenient ways of accomplishing things
|
|
that can also be accomplished with the core library. This means that CLI
|
|
commands should be thin wrappers that either just call a method from the core
|
|
library, or run a very simple workflow based on methods from the core library.
|
|
|
|
If you find that your CLI command starts to have some more complex logic, this
|
|
probably means that some of that logic would be beneficial to users of the
|
|
library as well. Consider moving that code into the core library.
|
|
|
|
This also implies that we can split any CLI subcommand into two stages:
|
|
|
|
1. Convert from user input to objects that have meaning to the library.
|
|
2. Run some code as if we were users of the library, with no reference to the
|
|
fact that the inputs came from the command line.
|
|
|
|
### Divide the subcommand module into three components
|
|
|
|
The recommended way of structuring a subcommand module is to split it into
|
|
three parts (where `command` is replaced by the name of your subcommand):
|
|
|
|
1. `command`: The command method, which is decorated by `@click.command`. The
|
|
purpose of this method is to convert user input to objects that can be used
|
|
by the core library. Then it calls the `command_main` method.
|
|
2. `command_main`: The workflow method, which is written using code from the
|
|
underlying library, with no reference to the fact that this is part of the
|
|
CLI. This typically contains a very simple workflow script. Although the
|
|
output from this process is usually saved to some output file as part of the
|
|
script in `command_main`, the best practice is to also return the result of
|
|
this method. The `command` method will ignore this return value, but
|
|
returning it makes it so that the `command_main` method can be reused in
|
|
other CLI commands to create more complex workflows.
|
|
3. `PLUGIN`: a `CommandPlugin` instance, which wraps the `command` object with
|
|
metadata about the subcommand, such as which help section to display it in,
|
|
and which versions of the library and CLI the plugin is compatible with.
|
|
|
|
As an example, here's a rough skeleton for a subcommand called `my_command`
|
|
(imports excluded)
|
|
|
|
```python
|
|
@click.command("my_command", short_help="This is my command")
|
|
... # add decorators for arguments/options
|
|
def my_command(...): # input params are based on arguments options
|
|
"""Docstring here is the help given by ``openfe my-command --help``"""
|
|
... # do whatever you need to convert the user input to library objects
|
|
my_command_main(...) # takes library objects
|
|
|
|
def my_command_main(...): # takes library objects
|
|
... # run some simple library code
|
|
return result
|
|
|
|
PLUGIN = CommandPlugin(
|
|
command=my_command,
|
|
section="My Section",
|
|
requires_lib=(1, 0),
|
|
requires_cli=(1, 0)
|
|
)
|
|
```
|
|
|
|
### Use reusable subcommand arguments/options
|
|
|
|
In `click`, command-line arguments and options are declared by attaching
|
|
decorators for each option to a method. The method must then take parameters
|
|
based on the option name as specified by the decorator.
|
|
|
|
Because of this, it is straightforward to create an object associated with a
|
|
given input option/argument, which contains details such as the help string and
|
|
even a method to get a library object from the user input string.
|
|
|
|
The best practice is to create this object outside a given subcommand, and then
|
|
reuse it between different subcommands. This ensures that the user sees
|
|
consistency in the interface and behavior between different CLI subcommands.
|
|
|
|
Details on how we'll do this in OpenFE are still being developed.
|
|
|
|
### Delay slow imports
|
|
|
|
Usually in Python, we put all imports at the top of a file. That is the best
|
|
practice for libraries and scripts, because it makes it easy for a developer to
|
|
find dependencies, and helps prevent developers from repeating import
|
|
statements.
|
|
|
|
However, when dealing with a CLI script like this, it's important to remember
|
|
that some user interactions, such as subcommand autocomplete or enquiring about the
|
|
CLI with `--version` or `--help`, will also run any top-level imports. If
|
|
imports are slow, then these user-facing interactions will be slow.
|
|
|
|
Because of this, the best practice when writing CLI subcommands is to move slow
|
|
imports inside the method that needs them.
|
|
|
|
### Testing your subcommand
|
|
|
|
Dividing the subcommand as recommended above facilitates testing. When testing
|
|
the `command` method itself, mock out the `command_main`, and use tools within
|
|
`click` to mock the user command inputs. The purpose of testing the `command`
|
|
method is to ensure that you correctly convert from user input to library object.
|
|
|
|
The purpose of testing the `command_main` method is to ensure that integration
|
|
with the library works. If this is truly a thin wrapper (and with the
|
|
assumption that the core library is thoroughly tested), then a smoke test may
|
|
be sufficient for `command_main`.
|