summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py21
-rw-r--r--setup.cfg18
-rwxr-xr-xtests/test_cmd2.py3
-rw-r--r--tests_isolated/test_commandset/test_commandset.py100
4 files changed, 131 insertions, 11 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 902cb314..73e319b5 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -56,6 +56,7 @@ from typing import (
List,
Mapping,
Optional,
+ Set,
Tuple,
Type,
Union,
@@ -226,7 +227,7 @@ class Cmd(cmd.Cmd):
terminators: Optional[List[str]] = None,
shortcuts: Optional[Dict[str, str]] = None,
command_sets: Optional[Iterable[CommandSet]] = None,
- auto_load_commands: bool = True
+ auto_load_commands: bool = True,
) -> None:
"""An easy but powerful framework for writing line-oriented command
interpreters. Extends Python's cmd package.
@@ -310,12 +311,13 @@ class Cmd(cmd.Cmd):
# A dictionary mapping settable names to their Settable instance
self._settables: Dict[str, Settable] = dict()
self.always_prefix_settables: bool = False
- self.build_settables()
# CommandSet containers
- self._installed_command_sets: List[CommandSet] = []
+ self._installed_command_sets: Set[CommandSet] = set()
self._cmd_to_command_sets: Dict[str, CommandSet] = {}
+ self.build_settables()
+
# Use as prompt for multiline commands on the 2nd+ line of input
self.continuation_prompt = '> '
@@ -622,7 +624,7 @@ class Cmd(cmd.Cmd):
if default_category and not hasattr(method, constants.CMD_ATTR_HELP_CATEGORY):
utils.categorize(method, default_category)
- self._installed_command_sets.append(cmdset)
+ self._installed_command_sets.add(cmdset)
self._register_subcommands(cmdset)
cmdset.on_registered()
@@ -918,6 +920,9 @@ class Cmd(cmd.Cmd):
:param settable: Settable object being added
"""
+ if not self.always_prefix_settables:
+ if settable.name in self.settables.keys() and settable.name not in self._settables.keys():
+ raise KeyError(f'Duplicate settable: {settable.name}')
if settable.settable_obj is None:
settable.settable_obj = self
self._settables[settable.name] = settable
@@ -1310,7 +1315,7 @@ class Cmd(cmd.Cmd):
endidx: int,
flag_dict: Dict[str, Union[Iterable, Callable]],
*,
- all_else: Union[None, Iterable, Callable] = None
+ all_else: Union[None, Iterable, Callable] = None,
) -> List[str]:
"""Tab completes based on a particular flag preceding the token being completed.
@@ -1359,7 +1364,7 @@ class Cmd(cmd.Cmd):
endidx: int,
index_dict: Mapping[int, Union[Iterable, Callable]],
*,
- all_else: Union[None, Iterable, Callable] = None
+ all_else: Union[None, Iterable, Callable] = None,
) -> List[str]:
"""Tab completes based on a fixed position in the input string.
@@ -2561,7 +2566,7 @@ class Cmd(cmd.Cmd):
stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout,
stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
shell=True,
- **kwargs
+ **kwargs,
)
# Popen was called with shell=True so the user can chain pipe commands and redirect their output
@@ -2735,7 +2740,7 @@ class Cmd(cmd.Cmd):
choices: Iterable = None,
choices_provider: Optional[Callable] = None,
completer: Optional[Callable] = None,
- parser: Optional[argparse.ArgumentParser] = None
+ parser: Optional[argparse.ArgumentParser] = None,
) -> str:
"""
Read input from appropriate stdin value. Also supports tab completion and up-arrow history while
diff --git a/setup.cfg b/setup.cfg
index b405af4e..d2ca017b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,11 @@
[tool:pytest]
testpaths =
tests
+addopts =
+ --cov=cmd2
+ --cov-append
+ --cov-report=term
+ --cov-report=html
[flake8]
count = True
@@ -37,3 +42,16 @@ use_parentheses = true
ignore-path=docs/_build,.git,.idea,.pytest_cache,.tox,.nox,.venv,.vscode,build,cmd2,examples,tests,cmd2.egg-info,dist,htmlcov,__pycache__,*.egg,plugins
max-line-length=120
verbose=0
+
+[mypy]
+disallow_incomplete_defs = True
+disallow_untyped_defs = True
+disallow_untyped_calls = True
+warn_redundant_casts = True
+warn_unused_ignores = False
+warn_return_any = True
+warn_unreachable = True
+strict = True
+show_error_context = True
+show_column_numbers = True
+show_error_codes = True
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index cd03e66e..f3c44bd4 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -229,7 +229,8 @@ def onchange_app():
def test_set_onchange_hook(onchange_app):
out, err = run_cmd(onchange_app, 'set quiet True')
- expected = normalize("""
+ expected = normalize(
+ """
You changed quiet
quiet - was: False
now: True
diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py
index 3a25adde..85608248 100644
--- a/tests_isolated/test_commandset/test_commandset.py
+++ b/tests_isolated/test_commandset/test_commandset.py
@@ -13,15 +13,17 @@ import pytest
import cmd2
from cmd2 import (
- utils,
+ Settable,
)
from cmd2.exceptions import (
CommandSetRegistrationError,
)
from .conftest import (
- WithCommandSets,
complete_tester,
+ normalize,
+ run_cmd,
+ WithCommandSets,
)
@@ -910,3 +912,97 @@ def test_bad_subcommand():
with pytest.raises(CommandSetRegistrationError):
app = BadSubcommandApp()
+
+
+def test_commandset_settables():
+ # Define an arbitrary class with some attribute
+ class Arbitrary:
+ def __init__(self):
+ self.some_value = 5
+
+ # Declare a CommandSet with a settable of some arbitrary property
+ class WithSettablesA(CommandSetBase):
+ def __init__(self):
+ super(WithSettablesA, self).__init__()
+
+ self._arbitrary = Arbitrary()
+ self._settable_prefix = 'addon'
+
+ self.add_settable(
+ Settable(
+ 'arbitrary_value',
+ int,
+ 'Some settable value',
+ settable_object=self._arbitrary,
+ settable_attrib_name='some_value',
+ )
+ )
+
+ # create the command set and cmd2
+ cmdset = WithSettablesA()
+ arbitrary2 = Arbitrary()
+ app = cmd2.Cmd(command_sets=[cmdset])
+ app.add_settable(Settable('always_prefix_settables', bool, 'Prefix settables'))
+
+ assert 'arbitrary_value' in app.settables.keys()
+ assert 'always_prefix_settables' in app.settables.keys()
+
+ # verify the settable shows up
+ out, err = run_cmd(app, 'set')
+ assert 'arbitrary_value: 5' in out
+ out, err = run_cmd(app, 'set arbitrary_value')
+ assert out == ['arbitrary_value: 5']
+
+ # change the value and verify the value changed
+ out, err = run_cmd(app, 'set arbitrary_value 10')
+ expected = """
+arbitrary_value - was: 5
+now: 10
+"""
+ assert out == normalize(expected)
+ out, err = run_cmd(app, 'set arbitrary_value')
+ assert out == ['arbitrary_value: 10']
+
+ # can't add to cmd2 now because commandset already has this settable
+ with pytest.raises(KeyError):
+ app.add_settable(Settable('arbitrary_value', int, 'This should fail'))
+
+ cmdset.add_settable(
+ Settable('arbitrary_value', int, 'Replaced settable', settable_object=arbitrary2, settable_attrib_name='some_value')
+ )
+
+ # Can't add a settable to the commandset that already exists in cmd2
+ with pytest.raises(KeyError):
+ cmdset.add_settable(Settable('always_prefix_settables', int, 'This should also fail'))
+
+ # Can't remove a settable from the CommandSet if it is elsewhere and not in the CommandSet
+ with pytest.raises(KeyError):
+ cmdset.remove_settable('always_prefix_settables')
+
+ # unregister the CommandSet and verify the settable is now gone
+ app.unregister_command_set(cmdset)
+ out, err = run_cmd(app, 'set')
+ assert 'arbitrary_value' not in out
+ out, err = run_cmd(app, 'set arbitrary_value')
+ expected = """
+Parameter 'arbitrary_value' not supported (type 'set' for list of parameters).
+"""
+ assert err == normalize(expected)
+
+ # turn on prefixes and add the commandset back
+ app.always_prefix_settables = True
+ app.register_command_set(cmdset)
+
+ # Verify the settable is back with the defined prefix.
+
+ assert 'addon.arbitrary_value' in app.settables.keys()
+
+ # rename the prefix and verify that the prefix changes everywhere
+ cmdset._settable_prefix = 'some'
+ assert 'addon.arbitrary_value' not in app.settables.keys()
+ assert 'some.arbitrary_value' in app.settables.keys()
+
+ out, err = run_cmd(app, 'set')
+ assert 'some.arbitrary_value: 5' in out
+ out, err = run_cmd(app, 'set some.arbitrary_value')
+ assert out == ['some.arbitrary_value: 5']