summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py32
-rw-r--r--cmd2/command_definition.py29
-rw-r--r--examples/modular_commands/commandset_basic.py17
-rw-r--r--tests/test_commandset.py146
4 files changed, 188 insertions, 36 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index ef273d15..310ad32f 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -46,7 +46,7 @@ from typing import Any, AnyStr, Callable, Dict, Iterable, List, Mapping, Optiona
from . import ansi, constants, plugin, utils
from .argparse_custom import DEFAULT_ARGUMENT_PARSER, CompletionItem
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
-from .command_definition import _UNBOUND_COMMANDS, CommandSet, _partial_passthru
+from .command_definition import _REGISTERED_COMMANDS, 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
@@ -403,8 +403,8 @@ class Cmd(cmd.Cmd):
"""
# start by loading registered functions as commands
- for cmd_name, cmd_func, cmd_completer, cmd_help in _UNBOUND_COMMANDS:
- self.install_command_function(cmd_name, cmd_func, cmd_completer, cmd_help)
+ for cmd_name in _REGISTERED_COMMANDS.keys():
+ self.install_registered_command(cmd_name)
# Search for all subclasses of CommandSet, instantiate them if they weren't provided in the constructor
all_commandset_defs = CommandSet.__subclasses__()
@@ -492,6 +492,28 @@ class Cmd(cmd.Cmd):
cmdset.on_unregister(self)
self._installed_command_sets.remove(cmdset)
+ def install_registered_command(self, cmd_name: str):
+ cmd_completer = None
+ cmd_help = None
+
+ if cmd_name not in _REGISTERED_COMMANDS:
+ raise KeyError('Command ' + cmd_name + ' has not been registered')
+
+ cmd_func = _REGISTERED_COMMANDS[cmd_name]
+
+ module = inspect.getmodule(cmd_func)
+
+ module_funcs = [mf for mf in inspect.getmembers(module) if inspect.isfunction(mf[1])]
+ for mf in module_funcs:
+ if mf[0] == COMPLETER_FUNC_PREFIX + cmd_name:
+ cmd_completer = mf[1]
+ elif mf[0] == HELP_FUNC_PREFIX + cmd_name:
+ cmd_help = mf[1]
+ if cmd_completer is not None and cmd_help is not None:
+ break
+
+ self.install_command_function(cmd_name, cmd_func, cmd_completer, cmd_help)
+
def install_command_function(self,
cmd_name: str,
cmd_func: Callable,
@@ -510,8 +532,8 @@ class Cmd(cmd.Cmd):
if not valid:
raise ValueError("Invalid command name {!r}: {}".format(cmd_name, errmsg))
- assert getattr(self, COMMAND_FUNC_PREFIX + cmd_name, None) is None,\
- 'Duplicate command function registered: ' + cmd_name
+ 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:
diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py
index 115cef64..6996bd9d 100644
--- a/cmd2/command_definition.py
+++ b/cmd2/command_definition.py
@@ -5,17 +5,16 @@ Supports the definition of commands in separate classes to be composed into cmd2
import functools
from typing import (
Callable,
+ Dict,
Iterable,
- List,
Optional,
- Tuple,
Type,
Union,
)
-from .constants import COMMAND_FUNC_PREFIX, HELP_FUNC_PREFIX, COMPLETER_FUNC_PREFIX
+from .constants import COMMAND_FUNC_PREFIX
# Allows IDEs to resolve types without impacting imports at runtime, breaking circular dependency issues
-try:
+try: # pragma: no cover
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .cmd2 import Cmd, Statement
@@ -23,7 +22,7 @@ try:
except ImportError:
pass
-_UNBOUND_COMMANDS = [] # type: List[Tuple[str, Callable, Optional[Callable], Optional[Callable]]]
+_REGISTERED_COMMANDS = {} # type: Dict[str, Callable]
"""
Registered command tuples. (command, do_ function, complete_ function, help_ function
"""
@@ -69,24 +68,12 @@ def register_command(cmd_func: Callable[['Cmd', Union['Statement', 'argparse.Nam
"""
assert cmd_func.__name__.startswith(COMMAND_FUNC_PREFIX), 'Command functions must start with `do_`'
- import inspect
-
cmd_name = cmd_func.__name__[len(COMMAND_FUNC_PREFIX):]
- cmd_completer = None
- cmd_help = None
-
- module = inspect.getmodule(cmd_func)
-
- module_funcs = [mf for mf in inspect.getmembers(module) if inspect.isfunction(mf[1])]
- for mf in module_funcs:
- if mf[0] == COMPLETER_FUNC_PREFIX + cmd_name:
- cmd_completer = mf[1]
- elif mf[0] == HELP_FUNC_PREFIX + cmd_name:
- cmd_help = mf[1]
- if cmd_completer is not None and cmd_help is not None:
- break
- _UNBOUND_COMMANDS.append((cmd_name, cmd_func, cmd_completer, cmd_help))
+ if cmd_name not in _REGISTERED_COMMANDS:
+ _REGISTERED_COMMANDS[cmd_name] = cmd_func
+ else:
+ raise KeyError('Command ' + cmd_name + ' is already registered')
return cmd_func
diff --git a/examples/modular_commands/commandset_basic.py b/examples/modular_commands/commandset_basic.py
index 8b51b7e4..01ce1b39 100644
--- a/examples/modular_commands/commandset_basic.py
+++ b/examples/modular_commands/commandset_basic.py
@@ -11,8 +11,21 @@ from cmd2.utils import CompletionError
@register_command
@with_category("AAA")
def do_unbound(cmd: Cmd, statement: Statement):
+ """This is an example of registering an unbound function
+
+ :param cmd:
+ :param statement:
+ :return:
+ """
+ cmd.poutput('Unbound Command: {}'.format(statement.args))
+
+
+@register_command
+@with_category("AAA")
+def do_func_with_help(cmd: Cmd, statement: Statement):
"""
This is an example of registering an unbound function
+
:param cmd:
:param statement:
:return:
@@ -20,6 +33,10 @@ def do_unbound(cmd: Cmd, statement: Statement):
cmd.poutput('Unbound Command: {}'.format(statement.args))
+def help_func_with_help(cmd: Cmd):
+ cmd.poutput('Help for func_with_help')
+
+
@with_default_category('Basic Completion')
class BasicCompletionCommandSet(CommandSet):
# List of strings used with completion functions
diff --git a/tests/test_commandset.py b/tests/test_commandset.py
index 02fff7b2..269c5de9 100644
--- a/tests/test_commandset.py
+++ b/tests/test_commandset.py
@@ -10,6 +10,11 @@ import pytest
import cmd2
from cmd2 import utils
+from .conftest import (
+ complete_tester,
+ run_cmd,
+)
+
# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available
try:
@@ -41,14 +46,14 @@ def do_command_with_support(cmd: cmd2.Cmd, statement: cmd2.Statement):
:param statement:
:return:
"""
- cmd.poutput('Unbound Command: {}'.format(statement.args))
+ cmd.poutput('Command with support functions: {}'.format(statement.args))
def help_command_with_support(cmd: cmd2.Cmd):
cmd.poutput('Help for command_with_support')
-def complete_command_with_support(self, cmd: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+def complete_command_with_support(cmd: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""Completion function for do_index_based"""
food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato']
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
@@ -96,11 +101,29 @@ class WithCommandSets(cmd2.Cmd):
super().__init__(*args, **kwargs)
+@cmd2.with_default_category('Command Set B')
+class CommandSetB(cmd2.CommandSet):
+ def __init__(self, arg1):
+ super().__init__()
+ self._arg1 = arg1
+
+ def do_aardvark(self, cmd: cmd2.Cmd, statement: cmd2.Statement):
+ cmd.poutput('Aardvark!')
+
+ def do_bat(self, cmd: cmd2.Cmd, statement: cmd2.Statement):
+ """Banana Command"""
+ cmd.poutput('Bat!!')
+
+ def do_crocodile(self, cmd: cmd2.Cmd, statement: cmd2.Statement):
+ cmd.poutput('Crocodile!!')
+
+
@pytest.fixture
def command_sets_app():
app = WithCommandSets()
return app
+
@pytest.fixture()
def command_sets_manual():
app = WithCommandSets(auto_load_commands=False)
@@ -120,28 +143,55 @@ def test_autoload_commands(command_sets_app):
assert 'cranberry' in cmds_cats['Command Set']
+def test_custom_construct_commandsets():
+ command_set = CommandSetB('foo')
+ app = WithCommandSets(command_sets=[command_set])
+
+ cmds_cats, cmds_doc, cmds_undoc, help_topics = app._build_command_info()
+ assert 'Command Set B' in cmds_cats
+
+ command_set_2 = CommandSetB('bar')
+ with pytest.raises(ValueError):
+ assert app.install_command_set(command_set_2)
+
+
def test_load_commands(command_sets_manual):
cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
- assert 'AAA' not in cmds_cats
+ # 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
- command_sets_manual.install_command_function('unbound', do_unbound, None, None)
+ # install the `unbound` command
+ command_sets_manual.install_registered_command('unbound')
+ with pytest.raises(KeyError):
+ assert command_sets_manual.install_registered_command('unbound')
+
+ with pytest.raises(KeyError):
+ assert command_sets_manual.install_registered_command('nonexistent_command')
+
+ def do_unbound(cmd: cmd2.Cmd, statement: cmd2.Statement):
+ """
+ This function duplicates an existing command
+ """
+ cmd.poutput('Unbound Command: {}'.format(statement.args))
+
+ with pytest.raises(KeyError):
+ assert cmd2.register_command(do_unbound)
+
+ # verify only the `unbound` command was installed
cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
assert 'AAA' in cmds_cats
assert 'unbound' in cmds_cats['AAA']
-
assert 'Alone' not in cmds_cats
-
assert 'Command Set' not in cmds_cats
+ # now install a command set and verify the commands are now present
cmd_set = CommandSetA()
-
command_sets_manual.install_command_set(cmd_set)
cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
@@ -155,6 +205,7 @@ def test_load_commands(command_sets_manual):
assert 'Command Set' in cmds_cats
assert 'cranberry' in cmds_cats['Command Set']
+ # uninstall the `unbound` command and verify only it was uninstalled
command_sets_manual.uninstall_command('unbound')
cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
@@ -167,12 +218,87 @@ def test_load_commands(command_sets_manual):
assert 'Command Set' in cmds_cats
assert 'cranberry' in cmds_cats['Command Set']
+ # uninstall the command set and verify it is now also no longer accessible
command_sets_manual.uninstall_command_set(cmd_set)
cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
assert 'AAA' not in cmds_cats
-
assert 'Alone' not in cmds_cats
-
assert 'Command Set' not in cmds_cats
+
+ # reinstall the command set and verifyt is accessible but the `unbound` command isn't
+ command_sets_manual.install_command_set(cmd_set)
+
+ cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
+
+ assert 'AAA' not in cmds_cats
+
+ assert 'Alone' in cmds_cats
+ assert 'elderberry' in cmds_cats['Alone']
+
+ assert 'Command Set' in cmds_cats
+ assert 'cranberry' in cmds_cats['Command Set']
+
+
+def test_command_functions(command_sets_manual):
+ cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
+ assert 'AAA' not in cmds_cats
+
+ out, err = run_cmd(command_sets_manual, 'command_with_support')
+ assert 'is not a recognized command, alias, or macro' in err[0]
+
+ out, err = run_cmd(command_sets_manual, 'help command_with_support')
+ assert 'No help on command_with_support' in err[0]
+
+ text = ''
+ line = 'command_with_support'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
+ assert first_match is None
+
+ command_sets_manual.install_registered_command('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'
+
+ command_sets_manual.uninstall_command('command_with_support')
+
+ cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
+ assert 'AAA' not in cmds_cats
+
+ out, err = run_cmd(command_sets_manual, 'command_with_support')
+ assert 'is not a recognized command, alias, or macro' in err[0]
+
+ out, err = run_cmd(command_sets_manual, 'help command_with_support')
+ assert 'No help on command_with_support' in err[0]
+
+ text = ''
+ line = 'command_with_support'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
+ assert first_match is None
+