Files
openfe/openfecli
Alyssa Travitz a3e1108ad8 add docs for gathering septop and abfe (#1737)
* add autodocs for gathering septop and abfe

* update docstrings

* add blurb to openfe gather docs

* add missing docstrings

* add warning

* add zstandard to docs mocks

* Apply suggestions from code review

Co-authored-by: Irfan Alibay <IAlibay@users.noreply.github.com>

* add cinnabar to doc mocks

---------

Co-authored-by: Irfan Alibay <IAlibay@users.noreply.github.com>
2025-12-08 08:41:26 -08:00
..
2025-11-10 21:58:30 +00:00
2024-11-08 10:43:56 -08:00

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)

@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.