diff options
-rw-r--r-- | cmd2/cmd2.py | 75 | ||||
-rw-r--r-- | cmd2/command_definition.py | 2 | ||||
-rw-r--r-- | tests/test_commandset.py | 60 |
3 files changed, 101 insertions, 36 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 40748d51..25251bef 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -41,7 +41,7 @@ import types from code import InteractiveConsole from collections import namedtuple from contextlib import redirect_stdout -from typing import Any, AnyStr, Callable, Dict, Iterable, List, Mapping, Optional, Tuple, Type, Union +from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple, Type, Union from . import ansi, constants, plugin, utils from .argparse_custom import DEFAULT_ARGUMENT_PARSER, CompletionItem @@ -438,37 +438,69 @@ class Cmd(cmd.Cmd): try: for method in methods: command = method[0][len(COMMAND_FUNC_PREFIX):] - - valid, errmsg = self.statement_parser.is_valid_command(command) - if not valid: - raise ValueError("Invalid command name {!r}: {}".format(command, errmsg)) - - assert getattr(self, COMMAND_FUNC_PREFIX + command, None) is None, \ - 'In {}: Duplicate command function: {}'.format(type(cmdset).__name__, method[0]) - command_wrapper = _partial_passthru(method[1], self) - setattr(self, method[0], command_wrapper) + + self.__install_command_function(command, command_wrapper, type(cmdset).__name__) installed_attributes.append(method[0]) completer_func_name = COMPLETER_FUNC_PREFIX + command cmd_completer = getattr(cmdset, completer_func_name, None) - if cmd_completer and not getattr(self, completer_func_name, None): + if cmd_completer is not None: completer_wrapper = _partial_passthru(cmd_completer, self) - setattr(self, completer_func_name, 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 and not getattr(self, help_func_name, None): + if cmd_help is not None: help_wrapper = _partial_passthru(cmd_help, self) - setattr(self, help_func_name, help_wrapper) + self.__install_help_function(command, help_wrapper) installed_attributes.append(help_func_name) + self._installed_command_sets.append(cmdset) except Exception: for attrib in installed_attributes: delattr(self, attrib) raise + def __install_command_function(self, command, command_wrapper, context=''): + cmd_func_name = COMMAND_FUNC_PREFIX + command + + # Make sure command function doesn't share naem with existing attribute + if hasattr(self, cmd_func_name): + raise ValueError('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)) + + # Check if command shares a name with an alias + if command in self.aliases: + self.pwarning("Deleting alias '{}' because it shares its name with a new command".format(command)) + del self.aliases[command] + + # Check if command shares a name with a macro + if command in self.macros: + self.pwarning("Deleting macro '{}' because it shares its name with a new command".format(command)) + del self.macros[command] + + setattr(self, cmd_func_name, command_wrapper) + + def __install_completer_function(self, cmd_name, cmd_completer): + 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): + 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) + def uninstall_command_set(self, cmdset: CommandSet): """ Uninstalls a CommandSet and unloads all associated commands @@ -528,22 +560,13 @@ class Cmd(cmd.Cmd): :param cmd_help: help generator for the command :return: None """ - valid, errmsg = self.statement_parser.is_valid_command(cmd_name) - if not valid: - raise ValueError("Invalid command name {!r}: {}".format(cmd_name, errmsg)) + self.__install_command_function(cmd_name, types.MethodType(cmd_func, self)) - if getattr(self, COMMAND_FUNC_PREFIX + cmd_name, None) is not None: - raise KeyError('Duplicate command function registered: ' + cmd_name) - setattr(self, COMMAND_FUNC_PREFIX + cmd_name, types.MethodType(cmd_func, self)) self._installed_functions.append(cmd_name) if cmd_completer is not None: - assert getattr(self, COMPLETER_FUNC_PREFIX + cmd_name, None) is None, \ - 'Duplicate command completer registered: ' + COMPLETER_FUNC_PREFIX + cmd_name - setattr(self, COMPLETER_FUNC_PREFIX + cmd_name, types.MethodType(cmd_completer, self)) + self.__install_completer_function(cmd_name, types.MethodType(cmd_completer, self)) if cmd_help is not None: - assert getattr(self, HELP_FUNC_PREFIX + cmd_name, None) is None, \ - 'Duplicate command help registered: ' + HELP_FUNC_PREFIX + cmd_name - setattr(self, HELP_FUNC_PREFIX + cmd_name, types.MethodType(cmd_help, self)) + self.__install_help_function(cmd_name, types.MethodType(cmd_help, self)) def uninstall_command(self, cmd_name: str): """ diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index 1768d86e..b5c9fbca 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -19,7 +19,7 @@ try: # pragma: no cover if TYPE_CHECKING: from .cmd2 import Cmd, Statement import argparse -except ImportError: +except ImportError: # pragma: no cover pass _REGISTERED_COMMANDS = {} # type: Dict[str, Callable] diff --git a/tests/test_commandset.py b/tests/test_commandset.py index a4207f99..bed570b9 100644 --- a/tests/test_commandset.py +++ b/tests/test_commandset.py @@ -12,17 +12,11 @@ from cmd2 import utils from .conftest import ( complete_tester, + normalize, run_cmd, ) -# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available -try: - import mock -except ImportError: - from unittest import mock - - @cmd2.register_command @cmd2.with_category("AAA") def do_unbound(cmd: cmd2.Cmd, statement: cmd2.Statement): @@ -131,6 +125,8 @@ def command_sets_manual(): def test_autoload_commands(command_sets_app): + # verifies that, when autoload is enabled, CommandSets and registered functions all show up + cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_app._build_command_info() assert 'AAA' in cmds_cats @@ -144,6 +140,7 @@ def test_autoload_commands(command_sets_app): def test_custom_construct_commandsets(): + # Verifies that a custom initialized CommandSet loads correctly when passed into the constructor command_set = CommandSetB('foo') app = WithCommandSets(command_sets=[command_set]) @@ -159,7 +156,6 @@ def test_load_commands(command_sets_manual): cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info() # start by verifying none of the installable commands are present - assert 'AAA' not in cmds_cats assert 'Alone' not in cmds_cats assert 'Command Set' not in cmds_cats @@ -167,12 +163,15 @@ def test_load_commands(command_sets_manual): # install the `unbound` command command_sets_manual.install_registered_command('unbound') - with pytest.raises(KeyError): + # verify that the same registered command can't be installed twice + with pytest.raises(ValueError): assert command_sets_manual.install_registered_command('unbound') + # verifies detection of unregistered commands with pytest.raises(KeyError): assert command_sets_manual.install_registered_command('nonexistent_command') + # verifies that a duplicate function name is detected def do_unbound(cmd: cmd2.Cmd, statement: cmd2.Statement): """ This function duplicates an existing command @@ -259,6 +258,17 @@ def test_command_functions(command_sets_manual): first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) assert first_match is None + # A bad command name gets rejected with an exception + with pytest.raises(ValueError): + assert command_sets_manual.install_command_function('>"', + do_command_with_support, + complete_command_with_support, + help_command_with_support) + + # create an alias to verify that it gets removed when the command is created + out, err = run_cmd(command_sets_manual, 'alias create command_with_support run_pyscript') + assert out == normalize("Alias 'command_with_support' created") + command_sets_manual.install_registered_command('command_with_support') cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info() @@ -302,6 +312,38 @@ def test_command_functions(command_sets_manual): first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) assert first_match is None + # create an alias to verify that it gets removed when the command is created + out, err = run_cmd(command_sets_manual, 'macro create command_with_support run_pyscript') + assert out == normalize("Macro 'command_with_support' created") + + command_sets_manual.install_command_function('command_with_support', + do_command_with_support, + complete_command_with_support, + help_command_with_support) + + cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info() + assert 'AAA' in cmds_cats + assert 'command_with_support' in cmds_cats['AAA'] + + out, err = run_cmd(command_sets_manual, 'command_with_support') + assert 'Command with support functions' in out[0] + + out, err = run_cmd(command_sets_manual, 'help command_with_support') + assert 'Help for command_with_support' in out[0] + + first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) + assert first_match == 'Ham' + + text = '' + line = 'command_with_support Ham' + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) + + assert first_match == 'Basket' + + def test_partial_with_passthru(): |