summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorEric Lin <anselor@gmail.com>2020-07-27 11:43:16 -0400
committeranselor <anselor@gmail.com>2020-08-04 13:38:08 -0400
commit3a6db395cb28b5b13dde38284dc22791583012fa (patch)
treea901fe8323d47cb061d9c2d4961565603f27b773 /docs
parent06cee9126839c465a356f8b44a5f008853eb8cad (diff)
downloadcmd2-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.rst126
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()