diff options
-rw-r--r-- | cmd2/argparse_custom.py | 2 | ||||
-rw-r--r-- | cmd2/cmd2.py | 34 | ||||
-rw-r--r-- | cmd2/constants.py | 3 | ||||
-rw-r--r-- | cmd2/decorators.py | 23 | ||||
-rw-r--r-- | docs/features/modular_commands.rst | 10 | ||||
-rw-r--r-- | examples/modular_subcommands.py | 6 | ||||
-rw-r--r-- | isolated_tests/test_commandset/test_commandset.py | 2 |
7 files changed, 44 insertions, 36 deletions
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 689c1db7..9dde5347 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -774,7 +774,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser): allow_abbrev=allow_abbrev) self.register('action', 'unloadable_parsers', _UnloadableSubParsersAction) - def add_subparsers(self, unloadable=False, **kwargs): + def add_subparsers(self, unloadable: bool = False, **kwargs): """ Custom override. Sets a default title if one was not given. diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index ea590fac..e15a856e 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -527,14 +527,14 @@ class Cmd(cmd.Cmd): def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: """ - Register sub-commands with their base command + Register subcommands with their base command - :param cmdset: CommandSet containing sub-commands + :param cmdset: CommandSet containing subcommands """ if not (cmdset is self or cmdset in self._installed_command_sets): - raise ValueError('Adding sub-commands from an unregistered CommandSet') + raise ValueError('Adding subcommands from an unregistered CommandSet') - # find all methods that start with the sub-command prefix + # find all methods that start with the subcommand prefix methods = inspect.getmembers( cmdset, predicate=lambda meth: (inspect.ismethod(meth) or isinstance(meth, Callable)) @@ -548,22 +548,18 @@ class Cmd(cmd.Cmd): subcommand_name = getattr(method, constants.SUBCMD_ATTR_NAME) command_name = getattr(method, constants.SUBCMD_ATTR_COMMAND) subcmd_parser = getattr(method, constants.CMD_ATTR_ARGPARSER) + parser_args = getattr(method, constants.SUBCMD_ATTR_PARSER_ARGS, {}) # Search for the base command function and verify it has an argparser defined command_func = self.cmd_func(command_name) if command_func is None or not hasattr(command_func, constants.CMD_ATTR_ARGPARSER): - raise TypeError('Could not find command "{}" needed by sub-command: {}' + raise TypeError('Could not find command "{}" needed by subcommand: {}' .format(command_name, str(method))) command_parser = getattr(command_func, constants.CMD_ATTR_ARGPARSER) if command_parser is None: - raise TypeError('Could not find argparser for command "{}" needed by sub-command: {}' + raise TypeError('Could not find argparser for command "{}" needed by subcommand: {}' .format(command_name, str(method))) - if hasattr(method, '__doc__') and method.__doc__ is not None: - help_text = method.__doc__.splitlines()[0] - else: - help_text = subcommand_name - if isinstance(cmdset, CommandSet): command_handler = _partial_passthru(method, self) else: @@ -572,18 +568,18 @@ class Cmd(cmd.Cmd): for action in command_parser._actions: if isinstance(action, _UnloadableSubParsersAction): - action.add_parser(subcommand_name, parents=[subcmd_parser], help=help_text) + action.add_parser(subcommand_name, parents=[subcmd_parser], **parser_args) def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: """ - Unregister sub-commands from their base command + Unregister subcommands from their base command - :param cmdset: CommandSet containing sub-commands + :param cmdset: CommandSet containing subcommands """ if not (cmdset is self or cmdset in self._installed_command_sets): - raise ValueError('Removing sub-commands from an unregistered CommandSet') + raise ValueError('Removing subcommands from an unregistered CommandSet') - # find all methods that start with the sub-command prefix + # find all methods that start with the subcommand prefix methods = inspect.getmembers( cmdset, predicate=lambda meth: (inspect.ismethod(meth) or isinstance(meth, Callable)) @@ -600,11 +596,11 @@ class Cmd(cmd.Cmd): # Search for the base command function and verify it has an argparser defined command_func = self.cmd_func(command_name) if command_func is None or not hasattr(command_func, constants.CMD_ATTR_ARGPARSER): - raise TypeError('Could not find command "{}" needed by sub-command: {}' + raise TypeError('Could not find command "{}" needed by subcommand: {}' .format(command_name, str(method))) command_parser = getattr(command_func, constants.CMD_ATTR_ARGPARSER) if command_parser is None: - raise TypeError('Could not find argparser for command "{}" needed by sub-command: {}' + raise TypeError('Could not find argparser for command "{}" needed by subcommand: {}' .format(command_name, str(method))) for action in command_parser._actions: @@ -3387,7 +3383,7 @@ class Cmd(cmd.Cmd): if 'gnureadline' in sys.modules: # Restore what the readline module pointed to if cmd2_env.readline_module is None: - del (sys.modules['readline']) + del sys.modules['readline'] else: sys.modules['readline'] = cmd2_env.readline_module diff --git a/cmd2/constants.py b/cmd2/constants.py index 0135e328..88a1bb82 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -50,6 +50,7 @@ CMD_ATTR_ARGPARSER = 'argparser' # Whether or not tokens are unquoted before sending to argparse CMD_ATTR_PRESERVE_QUOTES = 'preserve_quotes' -# sub-command attributes for the base command name and the sub-command name +# subcommand attributes for the base command name and the subcommand name SUBCMD_ATTR_COMMAND = 'parent_command' SUBCMD_ATTR_NAME = 'subcommand_name' +SUBCMD_ATTR_PARSER_ARGS = 'subcommand_parser_args' diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 6e3b7acf..82ad8cd7 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -1,7 +1,7 @@ # coding=utf-8 """Decorators for ``cmd2`` commands""" import argparse -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from . import constants from .exceptions import Cmd2ArgparseError @@ -339,13 +339,18 @@ def with_argparser(parser: argparse.ArgumentParser, *, def as_subcommand_to(command: str, subcommand: str, - parser: argparse.ArgumentParser) -> Callable[[argparse.Namespace], Optional[bool]]: + parser: argparse.ArgumentParser, + *, + help_text: Optional[str] = None, + aliases: Iterable[str] = None) -> Callable[[argparse.Namespace], Optional[bool]]: """ - Tag this method as a sub-command to an existing argparse decorated command. + Tag this method as a subcommand to an existing argparse decorated command. :param command: Command Name - :param subcommand: Sub-command name - :param parser: argparse Parser to for this sub-command + :param subcommand: Subcommand name + :param parser: argparse Parser for this subcommand + :param help_text: Help message for this subcommand + :param aliases: Alternative names for this subcommand :return: Wrapper function that can receive an argparse.Namespace """ def arg_decorator(func: Callable): @@ -357,10 +362,16 @@ def as_subcommand_to(command: str, parser.set_defaults(func=func) - # # Set some custom attributes for this command + # Set some custom attributes for this command setattr(func, constants.SUBCMD_ATTR_COMMAND, command) setattr(func, constants.CMD_ATTR_ARGPARSER, parser) setattr(func, constants.SUBCMD_ATTR_NAME, subcommand) + parser_args = {} + if help_text is not None: + parser_args['help'] = help_text + if aliases is not None: + parser_args['aliases'] = aliases[:] + setattr(func, constants.SUBCMD_ATTR_PARSER_ARGS, parser_args) return func diff --git a/docs/features/modular_commands.rst b/docs/features/modular_commands.rst index 82298c8f..3ead40ee 100644 --- a/docs/features/modular_commands.rst +++ b/docs/features/modular_commands.rst @@ -19,7 +19,7 @@ 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 +* Subcommand Injection - Subcommands 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. @@ -204,19 +204,19 @@ You may need to disable command auto-loading if you need dynamically load comman app.cmdloop() -Injecting Sub-Commands +Injecting Subcommands ---------------------- Description ~~~~~~~~~~~ -Using the `with_argparse` decorator, it is possible to define sub-commands for your command. This has a tendency to +Using the `with_argparse` decorator, it is possible to define subcommands 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 +such as `movies` and `shows` which each have subcommands `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 +Subcommand injection allows you to inject subcommands 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 diff --git a/examples/modular_subcommands.py b/examples/modular_subcommands.py index e4d2fe45..1ac951ae 100644 --- a/examples/modular_subcommands.py +++ b/examples/modular_subcommands.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # coding=utf-8 -"""A simple example demonstracting modular sub-command loading through CommandSets +"""A simple example demonstracting modular subcommand loading through CommandSets -In this example, there are loadable CommandSets defined. Each CommandSet has 1 sub-command defined that will be +In this example, there are loadable CommandSets defined. Each CommandSet has 1 subcommand defined that will be attached to the 'cut' command. The cut command is implemented with the `do_cut` function that has been tagged as an argparse command. The `load` and `unload` command will load and unload the CommandSets. The available top level commands as well as -sub-commands to the `cut` command will change depending on which CommandSets are loaded. +subcommands to the `cut` command will change depending on which CommandSets are loaded. """ import argparse import cmd2 diff --git a/isolated_tests/test_commandset/test_commandset.py b/isolated_tests/test_commandset/test_commandset.py index c7b2ac70..98385772 100644 --- a/isolated_tests/test_commandset/test_commandset.py +++ b/isolated_tests/test_commandset/test_commandset.py @@ -346,7 +346,7 @@ def test_subcommands(command_sets_manual): fruit_cmds = LoadableFruits(1) veg_cmds = LoadableVegetables(1) - # installing sub-commands without base command present raises exception + # installing subcommands without base command present raises exception with pytest.raises(TypeError): command_sets_manual.install_command_set(fruit_cmds) |