diff options
author | Eric Lin <anselor@gmail.com> | 2020-07-27 11:43:16 -0400 |
---|---|---|
committer | anselor <anselor@gmail.com> | 2020-08-04 13:38:08 -0400 |
commit | 3a6db395cb28b5b13dde38284dc22791583012fa (patch) | |
tree | a901fe8323d47cb061d9c2d4961565603f27b773 /docs | |
parent | 06cee9126839c465a356f8b44a5f008853eb8cad (diff) | |
download | cmd2-git-3a6db395cb28b5b13dde38284dc22791583012fa.tar.gz |
Adds support for injectable subcommands as part of CommandSet
load/unload.
Updated examples and documentation to include discussion of injectable
sub-commands.
Diffstat (limited to 'docs')
-rw-r--r-- | docs/features/modular_commands.rst | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/docs/features/modular_commands.rst b/docs/features/modular_commands.rst index d94e225a..82298c8f 100644 --- a/docs/features/modular_commands.rst +++ b/docs/features/modular_commands.rst @@ -19,6 +19,9 @@ Features * Dynamically Loadable/Unloadable Commands - Command functions and CommandSets can both be loaded and unloaded dynamically during application execution. This can enable features such as dynamically loaded modules that add additional commands. +* Sub-command Injection - Sub-commands can be defined separately from the base command. This allows for a more + action-centric instead of object-centric command system while still organizing your code and handlers around the + objects being managed. See the examples for more details: https://github.com/python-cmd2/cmd2/tree/master/plugins/command_sets/examples @@ -199,3 +202,126 @@ You may need to disable command auto-loading if you need dynamically load comman if __name__ == '__main__': app = ExampleApp() app.cmdloop() + + +Injecting Sub-Commands +---------------------- + +Description +~~~~~~~~~~~ +Using the `with_argparse` decorator, it is possible to define sub-commands for your command. This has a tendency to +either drive your interface into an object-centric interface. For example, imagine you have a tool that manages your +media collection and you want to manage movies or shows. An object-centric approach would push you to have base commands +such as `movies` and `shows` which each have sub-commands `add`, `edit`, `list`, `delete`. If you wanted to present an +action-centric command set, so that `add`, `edit`, `list`, and `delete` are the base commands, you'd have to organize +your code around these similar actions rather than organizing your code around similar objects being managed. + +Sub-command injection allows you to inject sub-commands into a base command to present an interface that is sensible to +a user while still organizing your code in whatever structure make more logical sense to the developer. + +Example +~~~~~~~ + +This example is a variation on the Dynamic Commands example above. A `cut` command is introduced as a base +command and each CommandSet + +.. code-block:: python + + import argparse + import cmd2 + from cmd2 import CommandSet, with_argparser, with_category, with_default_category + + + @with_default_category('Fruits') + class LoadableFruits(CommandSet): + def __init__(self): + super().__init__() + + def do_apple(self, cmd: cmd2.Cmd, _: cmd2.Statement): + cmd.poutput('Apple') + + banana_parser = cmd2.Cmd2ArgumentParser(add_help=False) + banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) + + @cmd2.as_subcommand_to('cut', 'banana', banana_parser) + def cut_banana(self, cmd: cmd2.Cmd, ns: argparse.Namespace): + """Cut banana""" + cmd.poutput('cutting banana: ' + ns.direction) + + + @with_default_category('Vegetables') + class LoadableVegetables(CommandSet): + def __init__(self): + super().__init__() + + def do_arugula(self, cmd: cmd2.Cmd, _: cmd2.Statement): + cmd.poutput('Arugula') + + bokchoy_parser = cmd2.Cmd2ArgumentParser(add_help=False) + bokchoy_parser.add_argument('style', choices=['quartered', 'diced']) + + @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser) + def cut_bokchoy(self, cmd: cmd2.Cmd, _: cmd2.Statement): + cmd.poutput('Bok Choy') + + + class ExampleApp(cmd2.Cmd): + """ + CommandSets are automatically loaded. Nothing needs to be done. + """ + + def __init__(self, *args, **kwargs): + # gotta have this or neither the plugin or cmd2 will initialize + super().__init__(*args, auto_load_commands=False, **kwargs) + + self._fruits = LoadableFruits() + self._vegetables = LoadableVegetables() + + load_parser = cmd2.Cmd2ArgumentParser('load') + load_parser.add_argument('cmds', choices=['fruits', 'vegetables']) + + @with_argparser(load_parser) + @with_category('Command Loading') + def do_load(self, ns: argparse.Namespace): + if ns.cmds == 'fruits': + try: + self.install_command_set(self._fruits) + self.poutput('Fruits loaded') + except ValueError: + self.poutput('Fruits already loaded') + + if ns.cmds == 'vegetables': + try: + self.install_command_set(self._vegetables) + self.poutput('Vegetables loaded') + except ValueError: + self.poutput('Vegetables already loaded') + + @with_argparser(load_parser) + def do_unload(self, ns: argparse.Namespace): + if ns.cmds == 'fruits': + self.uninstall_command_set(self._fruits) + self.poutput('Fruits unloaded') + + if ns.cmds == 'vegetables': + self.uninstall_command_set(self._vegetables) + self.poutput('Vegetables unloaded') + + cut_parser = cmd2.Cmd2ArgumentParser('cut') + cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut', unloadable=True) + + @with_argparser(cut_parser) + def do_cut(self, ns: argparse.Namespace): + func = getattr(ns, 'handler', None) + if func is not None: + # Call whatever subcommand function was selected + func(ns) + else: + # No subcommand was provided, so call help + self.poutput('This command does nothing without sub-parsers registered') + self.do_help('cut') + + + if __name__ == '__main__': + app = ExampleApp() + app.cmdloop() |