summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Lin <anselor@gmail.com>2020-06-16 14:41:25 -0400
committeranselor <anselor@gmail.com>2020-08-04 13:38:08 -0400
commit80dbdcbf96a247a08e7284674333330b61bb7ad3 (patch)
tree207ddaac75099ae1914121ba298f7b0b0002e3bb
parentf995e7abdef961ec0813f749debb1cfef1f8989d (diff)
downloadcmd2-git-80dbdcbf96a247a08e7284674333330b61bb7ad3.tar.gz
Added more command validation. Moved some common behavior into private functions.
-rw-r--r--cmd2/cmd2.py75
-rw-r--r--cmd2/command_definition.py2
-rw-r--r--tests/test_commandset.py60
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():