Build CLIs which
WARNING: Experimental. This module is an initial proof of concept and may change significantly.
Define a TogetherCLI by registering plugins which register either click.Group
or click.Command objects. Then, build and invoke the CLI. See
complete examples of standalone and multi-package usage.
Possibly the smallest usage example possible defines a single command with a single subcommand in one file. Each plugin is implemented as a class (though implementing plugins as modules is also supported).
# in mycli.py import together class BasePlugin: @together.hook def together_root_command(self, config): @click.group("mycli") def mycli(): pass return mycli class FooPlugin: @together.hook def together_subcommand(self, config): @click.command("foo") def foo(): click.echo("running foo") return foo class MyCLI(together.TogetherCLI): # customize plugin loading to specify these plugins def register_plugins(self): self.plugin_manager.register(BasePlugin()) self.plugin_manager.register(FooPlugin()) # build the CLI runme = MyCLI() # run it runme()
This would allow usage like
python mycli.py foo.
Changing Plugin Loading
By default, plugins are loaded by looking up the
entrypoint. To define custom plugin loading, override the
TogetherCLI.register_plugins method in a subclass.
For example, you could load a mix of explicit plugins and setuptools entrypoints with a custom name like so:
import mypackage.plugins class MyCLI(together.TogetherCLI): def register_plugins(self): # self.plugin_manager is a pluggy.PluginManager instance self.plugin_manager.register(mypackage.plugins.Foo) self.plugin_manager.register(mypackage.plugins.Bar) self.plugin_manager.load_setuptools_entrypoints("mypackage")
together automatically defines a class,
implements stateful tracking of option data as it is parsed. This allows the
value which was passed to a parent command or the root command to be recorded
on the click context and accessed by child command callbacks.
For example, if you wish to support arguments like
your commands, and allow usage like
mycli --format=json foo to be valid, you
need to save and retrieve that option.
CommandState implements this logic and
is automatically registered as the context object for commands.
To implement such an option, define a click decorator which uses a callback to get the state object and use it for storage. You can also write an easy getter function to get back the stored value. Like so:
import click from together import get_state def format_option(f): def callback(ctx, param, value): state = get_state() state.set("format", value) return click.option( "--format", type=click.Choice(["json", "text"]), callback=callback )(f) def get_format(): return get_state().get("format")
get_state()requires that there is an active Click Context it therefore can only normally execute inside of running Click commands
Using CommandState to access Config
The configuration object produced by your
together_configure hooks is
attached to the CommandState for easy access. All you need to do is get the
state object and look at its
def was_configured_to_foobar(): state = get_state() return state.config.get("foobar") is True
Plugin Order and Execution
Several rules govern how plugins execute and their ordering.
- Plugins are
pluggyplugins and hooks execute in LIFO order
togetherreverses the order of the resulting registrations to produce FIFO ordering
- Only the last plugin to register a root command will execute. You should only register one root command.
For the most part, this will make the plugin subcommand registration operate in FIFO order.
- Fix packaging bug
- Convert from
together_exception_handlerhook for registering exception handlers which can be matched against errors when invoking the CLI app
Exception handlers combine exception matching functions (predicates), exception handling callbacks, and optional priority levels (to ensure early or late matching)
Predicates may be boolean functions or exception classes, which will be checked with
You can (and should) now invoke a TogetherCLI by calling it, not relying upon the
Add the CommandState object supporting arbitrary storage (in an inner dict) and the
get_verbositybuilt on the CommandState object
This can be used to implement the common pattern of defining a single central object which is pulled off of the click context to record options which might be valid at multiple levels of a command heirarchy.
- Bugfix: incorrectly unpacking subcommand lists
together_subcommand_collectionwith support in the
together_subcommandhook for lists of subcommands
- Allow hooks to return command registration info without explicitly wrapping it in a registration object
- Change hook names and support config
- Initial release