summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py29
-rw-r--r--isolated_tests/test_commandset/test_commandset.py170
2 files changed, 187 insertions, 12 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 2a1bcbf2..ea590fac 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -427,7 +427,7 @@ class Cmd(cmd.Cmd):
methods = inspect.getmembers(
cmdset,
predicate=lambda meth: (inspect.ismethod(meth) or isinstance(meth, Callable))
- and meth.__name__.startswith(COMMAND_FUNC_PREFIX))
+ and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX))
installed_attributes = []
try:
@@ -435,21 +435,21 @@ class Cmd(cmd.Cmd):
command = method_name[len(COMMAND_FUNC_PREFIX):]
command_wrapper = _partial_passthru(method, self)
- self.__install_command_function(command, command_wrapper, type(cmdset).__name__)
+ self._install_command_function(command, command_wrapper, type(cmdset).__name__)
installed_attributes.append(method_name)
completer_func_name = COMPLETER_FUNC_PREFIX + command
cmd_completer = getattr(cmdset, completer_func_name, None)
if cmd_completer is not None:
completer_wrapper = _partial_passthru(cmd_completer, self)
- self.__install_completer_function(command, completer_wrapper)
+ self._install_completer_function(command, completer_wrapper)
installed_attributes.append(completer_func_name)
help_func_name = HELP_FUNC_PREFIX + command
cmd_help = getattr(cmdset, help_func_name, None)
if cmd_help is not None:
help_wrapper = _partial_passthru(cmd_help, self)
- self.__install_help_function(command, help_wrapper)
+ self._install_help_function(command, help_wrapper)
installed_attributes.append(help_func_name)
self._installed_command_sets.append(cmdset)
@@ -458,9 +458,10 @@ class Cmd(cmd.Cmd):
except Exception:
for attrib in installed_attributes:
delattr(self, attrib)
+ self._installed_command_sets.remove(cmdset)
raise
- def __install_command_function(self, command, command_wrapper, context=''):
+ 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
@@ -484,19 +485,19 @@ class Cmd(cmd.Cmd):
setattr(self, cmd_func_name, command_wrapper)
- def __install_completer_function(self, cmd_name, cmd_completer):
+ def _install_completer_function(self, cmd_name: str, cmd_completer: Callable):
completer_func_name = COMPLETER_FUNC_PREFIX + cmd_name
if hasattr(self, completer_func_name):
raise ValueError('Attribute already exists: {}'.format(completer_func_name))
setattr(self, completer_func_name, cmd_completer)
- def __install_help_function(self, cmd_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))
- setattr(self, help_func_name, cmd_completer)
+ setattr(self, help_func_name, cmd_help)
def uninstall_command_set(self, cmdset: CommandSet):
"""
@@ -551,10 +552,12 @@ 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: ' + command_name + ' needed by sub-command ' + str(method))
+ raise TypeError('Could not find command "{}" needed by sub-command: {}'
+ .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: ' + command_name + ' needed by sub-command ' + str(method))
+ raise TypeError('Could not find argparser for command "{}" needed by sub-command: {}'
+ .format(command_name, str(method)))
if hasattr(method, '__doc__') and method.__doc__ is not None:
help_text = method.__doc__.splitlines()[0]
@@ -597,10 +600,12 @@ 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: ' + command_name + ' needed by sub-command ' + str(method))
+ raise TypeError('Could not find command "{}" needed by sub-command: {}'
+ .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: ' + command_name + ' needed by sub-command ' + str(method))
+ raise TypeError('Could not find argparser for command "{}" needed by sub-command: {}'
+ .format(command_name, str(method)))
for action in command_parser._actions:
if isinstance(action, _UnloadableSubParsersAction):
diff --git a/isolated_tests/test_commandset/test_commandset.py b/isolated_tests/test_commandset/test_commandset.py
index c94c6690..c7b2ac70 100644
--- a/isolated_tests/test_commandset/test_commandset.py
+++ b/isolated_tests/test_commandset/test_commandset.py
@@ -121,6 +121,10 @@ def test_custom_construct_commandsets():
with pytest.raises(ValueError):
assert app.install_command_set(command_set_2)
+ # Verify that autoload doesn't conflict with a manually loaded CommandSet that could be autoloaded.
+ app2 = WithCommandSets(command_sets=[CommandSetA()])
+ assert hasattr(app2, 'do_apple')
+
def test_load_commands(command_sets_manual):
@@ -144,6 +148,9 @@ def test_load_commands(command_sets_manual):
assert 'Alone' not in cmds_cats
assert 'Fruits' not in cmds_cats
+ # uninstall a second time and verify no errors happen
+ command_sets_manual.uninstall_command_set(cmd_set)
+
# reinstall the command set and verify it is accessible
command_sets_manual.install_command_set(cmd_set)
@@ -211,3 +218,166 @@ def test_commandset_decorators(command_sets_app):
assert len(result.stderr) > 0
assert 'unrecognized arguments' in result.stderr
assert result.data is None
+
+
+def test_load_commandset_errors(command_sets_manual, capsys):
+ cmd_set = CommandSetA()
+
+ # create a conflicting command before installing CommandSet to verify rollback behavior
+ command_sets_manual._install_command_function('durian', cmd_set.do_durian)
+ with pytest.raises(ValueError):
+ command_sets_manual.install_command_set(cmd_set)
+
+ # verify that the commands weren't installed
+ cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
+
+ assert 'Alone' not in cmds_cats
+ assert 'Fruits' not in cmds_cats
+ assert not command_sets_manual._installed_command_sets
+
+ delattr(command_sets_manual, 'do_durian')
+
+ # pre-create intentionally conflicting macro and alias names
+ command_sets_manual.app_cmd('macro create apple run_pyscript')
+ command_sets_manual.app_cmd('alias create banana run_pyscript')
+
+ # now install a command set and verify the commands are now present
+ command_sets_manual.install_command_set(cmd_set)
+ out, err = capsys.readouterr()
+
+ # verify aliases and macros are deleted with warning if they conflict with a command
+ assert "Deleting alias 'banana'" in err
+ assert "Deleting macro 'apple'" in err
+
+ # verify duplicate commands are detected
+ with pytest.raises(ValueError):
+ command_sets_manual._install_command_function('banana', cmd_set.do_banana)
+
+ # verify bad command names are detected
+ with pytest.raises(ValueError):
+ command_sets_manual._install_command_function('bad command', cmd_set.do_banana)
+
+ # verify error conflict with existing completer function
+ with pytest.raises(ValueError):
+ command_sets_manual._install_completer_function('durian', cmd_set.complete_durian)
+
+ # verify error conflict with existing help function
+ with pytest.raises(ValueError):
+ command_sets_manual._install_help_function('cranberry', cmd_set.help_cranberry)
+
+
+class LoadableBase(cmd2.CommandSet):
+ def __init__(self, dummy):
+ super(LoadableBase, self).__init__()
+ self._dummy = dummy # prevents autoload
+
+ cut_parser = cmd2.Cmd2ArgumentParser('cut')
+ cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut', unloadable=True)
+
+ @cmd2.with_argparser(cut_parser)
+ def do_cut(self, ns: argparse.Namespace):
+ """Cut something"""
+ 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')
+
+
+class LoadableBadBase(cmd2.CommandSet):
+ def __init__(self, dummy):
+ super(LoadableBadBase, self).__init__()
+ self._dummy = dummy # prevents autoload
+
+ def do_cut(self, ns: argparse.Namespace):
+ """Cut something"""
+ 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')
+
+
+@cmd2.with_default_category('Fruits')
+class LoadableFruits(cmd2.CommandSet):
+ def __init__(self, dummy):
+ super(LoadableFruits, self).__init__()
+ self._dummy = dummy # prevents autoload
+
+ 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)
+
+
+@cmd2.with_default_category('Vegetables')
+class LoadableVegetables(cmd2.CommandSet):
+ def __init__(self, dummy):
+ super(LoadableVegetables, self).__init__()
+ self._dummy = dummy # prevents autoload
+
+ 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')
+
+
+def test_subcommands(command_sets_manual):
+
+ base_cmds = LoadableBase(1)
+ badbase_cmds = LoadableBadBase(1)
+ fruit_cmds = LoadableFruits(1)
+ veg_cmds = LoadableVegetables(1)
+
+ # installing sub-commands without base command present raises exception
+ with pytest.raises(TypeError):
+ command_sets_manual.install_command_set(fruit_cmds)
+
+ # if the base command is present but isn't an argparse command, expect exception
+ command_sets_manual.install_command_set(badbase_cmds)
+ with pytest.raises(TypeError):
+ command_sets_manual.install_command_set(fruit_cmds)
+
+ # verify that the commands weren't installed
+ cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
+ assert 'cut' in cmds_doc
+ assert 'Fruits' not in cmds_cats
+
+ # Now install the good base commands
+ command_sets_manual.uninstall_command_set(badbase_cmds)
+ command_sets_manual.install_command_set(base_cmds)
+
+ # verify that we catch an attempt to register subcommands when the commandset isn't installed
+ with pytest.raises(ValueError):
+ command_sets_manual._register_subcommands(fruit_cmds)
+
+ # verify that command set install and uninstalls without problems
+ command_sets_manual.install_command_set(fruit_cmds)
+ command_sets_manual.install_command_set(veg_cmds)
+ cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
+ assert 'Fruits' in cmds_cats
+
+ command_sets_manual.uninstall_command_set(fruit_cmds)
+ cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
+ assert 'Fruits' not in cmds_cats
+
+ # verify a double-unregister raises exception
+ with pytest.raises(ValueError):
+ command_sets_manual._unregister_subcommands(fruit_cmds)