summaryrefslogtreecommitdiff
path: root/cmd2/cmd2.py
diff options
context:
space:
mode:
Diffstat (limited to 'cmd2/cmd2.py')
-rw-r--r--cmd2/cmd2.py140
1 files changed, 104 insertions, 36 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 999c97cb..65aa88e0 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -48,7 +48,14 @@ from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .command_definition import CommandSet, _partial_passthru
from .constants import COMMAND_FUNC_PREFIX, COMPLETER_FUNC_PREFIX, HELP_FUNC_PREFIX
from .decorators import with_argparser
-from .exceptions import Cmd2ShlexError, EmbeddedConsoleExit, EmptyStatement, RedirectionError, SkipPostcommandHooks
+from .exceptions import (
+ CommandSetRegistrationError,
+ Cmd2ShlexError,
+ EmbeddedConsoleExit,
+ EmptyStatement,
+ RedirectionError,
+ SkipPostcommandHooks
+)
from .history import History, HistoryItem
from .parsing import Macro, MacroArg, Statement, StatementParser, shlex_split
from .rl_utils import RlType, rl_get_point, rl_make_safe_prompt, rl_set_prompt, rl_type, rl_warning, vt100_support
@@ -245,8 +252,8 @@ class Cmd(cmd.Cmd):
shortcuts=shortcuts)
# Load modular commands
- self._installed_functions = [] # type: List[str]
self._installed_command_sets = [] # type: List[CommandSet]
+ self._cmd_to_command_sets = {} # type: Dict[str, CommandSet]
if command_sets:
for command_set in command_sets:
self.install_command_set(command_set)
@@ -260,8 +267,6 @@ class Cmd(cmd.Cmd):
if not valid:
raise ValueError("Invalid command name {!r}: {}".format(cur_cmd, errmsg))
- self._register_subcommands(self)
-
# Stores results from the last command run to enable usage of results in a Python script or interactive console
# Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
self.last_result = None
@@ -399,6 +404,8 @@ class Cmd(cmd.Cmd):
# If False, then complete() will sort the matches using self.default_sort_key before they are displayed.
self.matches_sorted = False
+ self._register_subcommands(self)
+
def _autoload_commands(self) -> None:
"""Load modular command definitions."""
# Search for all subclasses of CommandSet, instantiate them if they weren't provided in the constructor
@@ -406,12 +413,11 @@ class Cmd(cmd.Cmd):
existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets]
for cmdset_type in all_commandset_defs:
init_sig = inspect.signature(cmdset_type.__init__)
- if cmdset_type in existing_commandset_types or \
- len(init_sig.parameters) != 1 or \
- 'self' not in init_sig.parameters:
- continue
- cmdset = cmdset_type()
- self.install_command_set(cmdset)
+ if not (cmdset_type in existing_commandset_types or
+ len(init_sig.parameters) != 1 or
+ 'self' not in init_sig.parameters):
+ cmdset = cmdset_type()
+ self.install_command_set(cmdset)
def install_command_set(self, cmdset: CommandSet) -> None:
"""
@@ -421,7 +427,7 @@ class Cmd(cmd.Cmd):
"""
existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets]
if type(cmdset) in existing_commandset_types:
- raise ValueError('CommandSet ' + type(cmdset).__name__ + ' is already installed')
+ raise CommandSetRegistrationError('CommandSet ' + type(cmdset).__name__ + ' is already installed')
cmdset.on_register(self)
methods = inspect.getmembers(
@@ -452,6 +458,8 @@ class Cmd(cmd.Cmd):
self._install_help_function(command, help_wrapper)
installed_attributes.append(help_func_name)
+ self._cmd_to_command_sets[command] = cmdset
+
self._installed_command_sets.append(cmdset)
self._register_subcommands(cmdset)
@@ -460,19 +468,22 @@ class Cmd(cmd.Cmd):
delattr(self, attrib)
if cmdset in self._installed_command_sets:
self._installed_command_sets.remove(cmdset)
+ if cmdset in self._cmd_to_command_sets.values():
+ self._cmd_to_command_sets = \
+ {key: val for key, val in self._cmd_to_command_sets.items() if val is not cmdset}
raise
def _install_command_function(self, command: str, command_wrapper: Callable, context=''):
cmd_func_name = COMMAND_FUNC_PREFIX + command
- # Make sure command function doesn't share naem with existing attribute
+ # Make sure command function doesn't share name with existing attribute
if hasattr(self, cmd_func_name):
- raise ValueError('Attribute already exists: {} ({})'.format(cmd_func_name, context))
+ raise CommandSetRegistrationError('Attribute already exists: {} ({})'.format(cmd_func_name, context))
# Check if command has an invalid name
valid, errmsg = self.statement_parser.is_valid_command(command)
if not valid:
- raise ValueError("Invalid command name {!r}: {}".format(command, errmsg))
+ raise CommandSetRegistrationError("Invalid command name {!r}: {}".format(command, errmsg))
# Check if command shares a name with an alias
if command in self.aliases:
@@ -490,14 +501,14 @@ class Cmd(cmd.Cmd):
completer_func_name = COMPLETER_FUNC_PREFIX + cmd_name
if hasattr(self, completer_func_name):
- raise ValueError('Attribute already exists: {}'.format(completer_func_name))
+ raise CommandSetRegistrationError('Attribute already exists: {}'.format(completer_func_name))
setattr(self, completer_func_name, cmd_completer)
def _install_help_function(self, cmd_name: str, cmd_help: Callable):
help_func_name = HELP_FUNC_PREFIX + cmd_name
if hasattr(self, help_func_name):
- raise ValueError('Attribute already exists: {}'.format(help_func_name))
+ raise CommandSetRegistrationError('Attribute already exists: {}'.format(help_func_name))
setattr(self, help_func_name, cmd_help)
def uninstall_command_set(self, cmdset: CommandSet):
@@ -506,7 +517,7 @@ class Cmd(cmd.Cmd):
:param cmdset: CommandSet to uninstall
"""
if cmdset in self._installed_command_sets:
-
+ self._check_uninstallable(cmdset)
self._unregister_subcommands(cmdset)
methods = inspect.getmembers(
@@ -522,6 +533,9 @@ class Cmd(cmd.Cmd):
if cmd_name in self.disabled_commands:
self.enable_command(cmd_name)
+ if cmd_name in self._cmd_to_command_sets:
+ del self._cmd_to_command_sets[cmd_name]
+
delattr(self, COMMAND_FUNC_PREFIX + cmd_name)
if hasattr(self, COMPLETER_FUNC_PREFIX + cmd_name):
@@ -532,14 +546,42 @@ class Cmd(cmd.Cmd):
cmdset.on_unregister(self)
self._installed_command_sets.remove(cmdset)
+ def _check_uninstallable(self, cmdset: CommandSet):
+ methods = inspect.getmembers(
+ cmdset,
+ predicate=lambda meth: isinstance(meth, Callable)
+ and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX))
+
+ for method in methods:
+ command_name = method[0][len(COMMAND_FUNC_PREFIX):]
+
+ # Search for the base command function and verify it has an argparser defined
+ if command_name in self.disabled_commands:
+ command_func = self.disabled_commands[command_name].command_function
+ else:
+ command_func = self.cmd_func(command_name)
+
+ command_parser = getattr(command_func, constants.CMD_ATTR_ARGPARSER, None)
+ def check_parser_uninstallable(parser):
+ for action in parser._actions:
+ if isinstance(action, argparse._SubParsersAction):
+ for subparser in action.choices.values():
+ attached_cmdset = getattr(subparser, constants.PARSER_ATTR_COMMANDSET, None)
+ if attached_cmdset is not None and attached_cmdset is not cmdset:
+ raise CommandSetRegistrationError(
+ 'Cannot uninstall CommandSet when another CommandSet depends on it')
+ check_parser_uninstallable(subparser)
+ if command_parser is not None:
+ check_parser_uninstallable(command_parser)
+
def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
"""
Register subcommands with their base command
- :param cmdset: CommandSet containing subcommands
+ :param cmdset: CommandSet or cmd2.Cmd subclass containing subcommands
"""
if not (cmdset is self or cmdset in self._installed_command_sets):
- raise ValueError('Adding subcommands from an unregistered CommandSet')
+ raise CommandSetRegistrationError('Cannot register subcommands with an unregistered CommandSet')
# find all methods that start with the subcommand prefix
methods = inspect.getmembers(
@@ -553,10 +595,14 @@ class Cmd(cmd.Cmd):
# iterate through all matching methods
for method_name, method in methods:
subcommand_name = getattr(method, constants.SUBCMD_ATTR_NAME)
- command_name = getattr(method, constants.SUBCMD_ATTR_COMMAND)
+ full_command_name = getattr(method, constants.SUBCMD_ATTR_COMMAND) # type: str
subcmd_parser = getattr(method, constants.CMD_ATTR_ARGPARSER)
parser_args = getattr(method, constants.SUBCMD_ATTR_PARSER_ARGS, {})
+ command_tokens = full_command_name.split()
+ command_name = command_tokens[0]
+ subcommand_names = command_tokens[1:]
+
# Search for the base command function and verify it has an argparser defined
if command_name in self.disabled_commands:
command_func = self.disabled_commands[command_name].command_function
@@ -564,12 +610,12 @@ class Cmd(cmd.Cmd):
command_func = self.cmd_func(command_name)
if command_func is None:
- raise TypeError('Could not find command "{}" needed by subcommand: {}'
- .format(command_name, str(method)))
+ raise CommandSetRegistrationError('Could not find command "{}" needed by subcommand: {}'
+ .format(command_name, str(method)))
command_parser = getattr(command_func, constants.CMD_ATTR_ARGPARSER, None)
if command_parser is None:
- raise TypeError('Could not find argparser for command "{}" needed by subcommand: {}'
- .format(command_name, str(method)))
+ raise CommandSetRegistrationError('Could not find argparser for command "{}" needed by subcommand: {}'
+ .format(command_name, str(method)))
if isinstance(cmdset, CommandSet):
command_handler = _partial_passthru(method, self)
@@ -577,9 +623,23 @@ class Cmd(cmd.Cmd):
command_handler = method
subcmd_parser.set_defaults(handler=command_handler)
- for action in command_parser._actions:
+ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser:
+ if not subcmd_names:
+ return action
+ cur_subcmd = subcmd_names.pop(0)
+ for sub_action in action._actions:
+ if isinstance(sub_action, argparse._SubParsersAction):
+ for choice_name, choice in sub_action.choices.items():
+ if choice_name == cur_subcmd:
+ return find_subcommand(choice, subcmd_names)
+ raise CommandSetRegistrationError('Could not find sub-command "{}"'.format(full_command_name))
+
+ target_parser = find_subcommand(command_parser, subcommand_names)
+
+ for action in target_parser._actions:
if isinstance(action, argparse._SubParsersAction):
- action.add_parser(subcommand_name, parents=[subcmd_parser], **parser_args)
+ attached_parser = action.add_parser(subcommand_name, parents=[subcmd_parser], **parser_args)
+ setattr(attached_parser, constants.PARSER_ATTR_COMMANDSET, cmdset)
def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
"""
@@ -588,7 +648,7 @@ class Cmd(cmd.Cmd):
:param cmdset: CommandSet containing subcommands
"""
if not (cmdset is self or cmdset in self._installed_command_sets):
- raise ValueError('Removing subcommands from an unregistered CommandSet')
+ raise CommandSetRegistrationError('Cannot unregister subcommands with an unregistered CommandSet')
# find all methods that start with the subcommand prefix
methods = inspect.getmembers(
@@ -610,13 +670,17 @@ class Cmd(cmd.Cmd):
else:
command_func = self.cmd_func(command_name)
- if command_func is None:
- raise TypeError('Could not find command "{}" needed by subcommand: {}'
- .format(command_name, str(method)))
+ if command_func is None: # pragma: no cover
+ # This really shouldn't be possible since _register_subcommands would prevent this from happening
+ # but keeping in case it does for some strange reason
+ raise CommandSetRegistrationError('Could not find command "{}" needed by subcommand: {}'
+ .format(command_name, str(method)))
command_parser = getattr(command_func, constants.CMD_ATTR_ARGPARSER, None)
- if command_parser is None:
- raise TypeError('Could not find argparser for command "{}" needed by subcommand: {}'
- .format(command_name, str(method)))
+ if command_parser is None: # pragma: no cover
+ # This really shouldn't be possible since _register_subcommands would prevent this from happening
+ # but keeping in case it does for some strange reason
+ raise CommandSetRegistrationError('Could not find argparser for command "{}" needed by subcommand: {}'
+ .format(command_name, str(method)))
for action in command_parser._actions:
if isinstance(action, argparse._SubParsersAction):
@@ -1439,6 +1503,7 @@ class Cmd(cmd.Cmd):
# Parse the command line
statement = self.statement_parser.parse_command_only(line)
command = statement.command
+ cmd_set = self._cmd_to_command_sets[command] if command in self._cmd_to_command_sets else None
expanded_line = statement.command_and_args
# We overwrote line with a properly formatted but fully stripped version
@@ -1509,7 +1574,8 @@ class Cmd(cmd.Cmd):
import functools
compfunc = functools.partial(self._complete_argparse_command,
argparser=argparser,
- preserve_quotes=getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES))
+ preserve_quotes=getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES),
+ cmd_set=cmd_set)
else:
compfunc = self.completedefault
@@ -1677,7 +1743,9 @@ class Cmd(cmd.Cmd):
return None
def _complete_argparse_command(self, text: str, line: str, begidx: int, endidx: int, *,
- argparser: argparse.ArgumentParser, preserve_quotes: bool) -> List[str]:
+ argparser: argparse.ArgumentParser,
+ preserve_quotes: bool,
+ cmd_set: Optional[CommandSet] = None) -> List[str]:
"""Completion function for argparse commands"""
from .argparse_completer import ArgparseCompleter
completer = ArgparseCompleter(argparser, self)
@@ -1686,7 +1754,7 @@ class Cmd(cmd.Cmd):
# To have tab completion parsing match command line parsing behavior,
# use preserve_quotes to determine if we parse the quoted or unquoted tokens.
tokens_to_parse = raw_tokens if preserve_quotes else tokens
- return completer.complete_command(tokens_to_parse, text, line, begidx, endidx)
+ return completer.complete_command(tokens_to_parse, text, line, begidx, endidx, cmd_set=cmd_set)
def in_script(self) -> bool:
"""Return whether a text script is running"""