diff options
-rw-r--r-- | cmd2/cmd2.py | 21 | ||||
-rw-r--r-- | setup.cfg | 18 | ||||
-rwxr-xr-x | tests/test_cmd2.py | 3 | ||||
-rw-r--r-- | tests_isolated/test_commandset/test_commandset.py | 100 |
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 @@ -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'] |