summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Lin <anselor@gmail.com>2020-07-29 17:59:47 -0400
committeranselor <anselor@gmail.com>2020-08-04 13:38:08 -0400
commit105369bdd15c2067b3ee6bdd6a737733a34c38ef (patch)
tree6358e7923d35d491998d263a93fdbdd8f1e8c256
parent8d0b162cf5712c26947f1e0bbc2b1021cb71e366 (diff)
downloadcmd2-git-105369bdd15c2067b3ee6bdd6a737733a34c38ef.tar.gz
Suggested PR Fixes.
sub-commande => subcommand Added help/aliases to `as_subcommand_to` decorator.
-rw-r--r--cmd2/argparse_custom.py2
-rw-r--r--cmd2/cmd2.py34
-rw-r--r--cmd2/constants.py3
-rw-r--r--cmd2/decorators.py23
-rw-r--r--docs/features/modular_commands.rst10
-rw-r--r--examples/modular_subcommands.py6
-rw-r--r--isolated_tests/test_commandset/test_commandset.py2
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)