summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py59
-rw-r--r--cmd2/command_definition.py40
-rw-r--r--cmd2/utils.py35
-rwxr-xr-xtests/test_cmd2.py7
4 files changed, 116 insertions, 25 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index cccfa882..902cb314 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -308,9 +308,14 @@ class Cmd(cmd.Cmd):
self.max_completion_items = 50
# A dictionary mapping settable names to their Settable instance
- self.settables: Dict[str, Settable] = dict()
+ self._settables: Dict[str, Settable] = dict()
+ self.always_prefix_settables: bool = False
self.build_settables()
+ # CommandSet containers
+ self._installed_command_sets: List[CommandSet] = []
+ self._cmd_to_command_sets: Dict[str, CommandSet] = {}
+
# Use as prompt for multiline commands on the 2nd+ line of input
self.continuation_prompt = '> '
@@ -500,8 +505,6 @@ class Cmd(cmd.Cmd):
# depends on them and it's possible a module's on_register() method may need to access some.
############################################################################################################
# Load modular commands
- self._installed_command_sets: List[CommandSet] = []
- self._cmd_to_command_sets: Dict[str, CommandSet] = {}
if command_sets:
for command_set in command_sets:
self.register_command_set(command_set)
@@ -575,6 +578,15 @@ class Cmd(cmd.Cmd):
if type(cmdset) in existing_commandset_types:
raise CommandSetRegistrationError('CommandSet ' + type(cmdset).__name__ + ' is already installed')
+ if self.always_prefix_settables:
+ if len(cmdset.settable_prefix.strip()) == 0:
+ raise CommandSetRegistrationError('CommandSet settable prefix must not be empty')
+ else:
+ all_settables = self.settables
+ for key in cmdset.settables.keys():
+ if key in all_settables:
+ raise KeyError(f'Duplicate settable {key} is already registered')
+
cmdset.on_register(self)
methods = inspect.getmembers(
cmdset,
@@ -888,13 +900,27 @@ class Cmd(cmd.Cmd):
action.remove_parser(subcommand_name)
break
+ @property
+ def settables(self) -> Mapping[str, Settable]:
+ all_settables = dict(self._settables)
+ for cmd_set in self._installed_command_sets:
+ cmdset_settables = cmd_set.settables
+ for settable_name, settable in cmdset_settables.items():
+ if self.always_prefix_settables:
+ all_settables[f'{cmd_set.settable_prefix}.{settable_name}'] = settable
+ else:
+ all_settables[settable_name] = settable
+ return all_settables
+
def add_settable(self, settable: Settable) -> None:
"""
Convenience method to add a settable parameter to ``self.settables``
:param settable: Settable object being added
"""
- self.settables[settable.name] = settable
+ if settable.settable_obj is None:
+ settable.settable_obj = self
+ self._settables[settable.name] = settable
def remove_settable(self, name: str) -> None:
"""
@@ -904,7 +930,7 @@ class Cmd(cmd.Cmd):
:raises: KeyError if the Settable matches this name
"""
try:
- del self.settables[name]
+ del self._settables[name]
except KeyError:
raise KeyError(name + " is not a settable parameter")
@@ -3712,29 +3738,23 @@ class Cmd(cmd.Cmd):
if args.param:
try:
settable = self.settables[args.param]
+ if settable.settable_obj is None:
+ settable.settable_obj = self
except KeyError:
self.perror("Parameter '{}' not supported (type 'set' for list of parameters).".format(args.param))
return
if args.value:
- args.value = utils.strip_quotes(args.value)
-
# Try to update the settable's value
try:
- orig_value = getattr(self, args.param)
- setattr(self, args.param, settable.val_type(args.value))
- new_value = getattr(self, args.param)
+ orig_value = settable.get_value()
+ new_value = settable.set_value(utils.strip_quotes(args.value))
# noinspection PyBroadException
except Exception as e:
err_msg = "Error setting {}: {}".format(args.param, e)
self.perror(err_msg)
- return
-
- self.poutput('{} - was: {!r}\nnow: {!r}'.format(args.param, orig_value, new_value))
-
- # Check if we need to call an onchange callback
- if orig_value != new_value and settable.onchange_cb:
- settable.onchange_cb(args.param, orig_value, new_value)
+ else:
+ self.poutput('{} - was: {!r}\nnow: {!r}'.format(args.param, orig_value, new_value))
return
# Show one settable
@@ -3747,7 +3767,8 @@ class Cmd(cmd.Cmd):
max_len = 0
results = dict()
for param in to_show:
- results[param] = '{}: {!r}'.format(param, getattr(self, param))
+ settable = self.settables[param]
+ results[param] = '{}: {!r}'.format(param, settable.get_value())
max_len = max(max_len, ansi.style_aware_wcswidth(results[param]))
# Display the results
@@ -5131,7 +5152,7 @@ class Cmd(cmd.Cmd):
:return:
"""
# figure out what class the command support function was defined in
- func_class = get_defining_class(cmd_support_func)
+ func_class = get_defining_class(cmd_support_func) # type: Optional[Type]
# Was there a defining class identified? If so, is it a sub-class of CommandSet?
if func_class is not None and issubclass(func_class, CommandSet):
diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py
index 654043c8..a63e0efc 100644
--- a/cmd2/command_definition.py
+++ b/cmd2/command_definition.py
@@ -3,6 +3,8 @@
Supports the definition of commands in separate classes to be composed into cmd2.Cmd
"""
from typing import (
+ Dict,
+ Mapping,
Optional,
Type,
)
@@ -14,6 +16,9 @@ from .constants import (
from .exceptions import (
CommandSetRegistrationError,
)
+from .utils import (
+ Settable,
+)
# Allows IDEs to resolve types without impacting imports at runtime, breaking circular dependency issues
try: # pragma: no cover
@@ -90,6 +95,8 @@ class CommandSet(object):
def __init__(self):
self._cmd: Optional[cmd2.Cmd] = None
+ self._settables: Dict[str, Settable] = {}
+ self._settable_prefix = self.__class__.__name__
def on_register(self, cmd) -> None:
"""
@@ -126,3 +133,36 @@ class CommandSet(object):
Subclasses can override this to perform remaining cleanup steps.
"""
self._cmd = None
+
+ @property
+ def settable_prefix(self) -> str:
+ return self._settable_prefix
+
+ @property
+ def settables(self) -> Mapping[str, Settable]:
+ return self._settables
+
+ def add_settable(self, settable: Settable) -> None:
+ """
+ Convenience method to add a settable parameter to the CommandSet
+
+ :param settable: Settable object being added
+ """
+ if self._cmd and not self._cmd.always_prefix_settables:
+ if settable.name in self._cmd.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
+
+ def remove_settable(self, name: str) -> None:
+ """
+ Convenience method for removing a settable parameter from the CommandSet
+
+ :param name: name of the settable being removed
+ :raises: KeyError if the Settable matches this name
+ """
+ try:
+ del self._settables[name]
+ except KeyError:
+ raise KeyError(name + " is not a settable parameter")
diff --git a/cmd2/utils.py b/cmd2/utils.py
index c9577e82..1008cb86 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -28,13 +28,15 @@ from typing import (
Optional,
TextIO,
Type,
+ TYPE_CHECKING,
Union,
)
from . import (
constants,
)
-
+if TYPE_CHECKING: # pragma: no cover
+ import cmd2
def is_quoted(arg: str) -> bool:
"""
@@ -93,7 +95,7 @@ def str_to_bool(val: str) -> bool:
class Settable:
- """Used to configure a cmd2 instance member to be settable via the set command in the CLI"""
+ """Used to configure an attribute to be settable via the set command in the CLI"""
def __init__(
self,
@@ -101,6 +103,8 @@ class Settable:
val_type: Callable,
description: str,
*,
+ settable_object: Optional[object] = None,
+ settable_attrib_name: Optional[str] = None,
onchange_cb: Callable[[str, Any, Any], Any] = None,
choices: Iterable = None,
choices_provider: Optional[Callable] = None,
@@ -114,6 +118,8 @@ class Settable:
even validate its value. Setting this to bool provides tab completion for true/false and
validation using str_to_bool(). The val_type function should raise an exception if it fails.
This exception will be caught and printed by Cmd.do_set().
+ :param settable_object: Object to configure with the set command
+ :param settable_attrib_name: Attribute name to be modified. Defaults to `name` if not specified.
:param description: string describing this setting
:param onchange_cb: optional function or method to call when the value of this settable is altered
by the set command. (e.g. onchange_cb=self.debug_changed)
@@ -137,11 +143,36 @@ class Settable:
self.name = name
self.val_type = val_type
self.description = description
+ self.settable_obj = settable_object
+ self.settable_attrib_name = settable_attrib_name if settable_attrib_name is not None else name
self.onchange_cb = onchange_cb
self.choices = choices
self.choices_provider = choices_provider
self.completer = completer
+ def get_value(self) -> Any:
+ """
+ Get the value of the settable attribute
+ :return:
+ """
+ return getattr(self.settable_obj, self.settable_attrib_name)
+
+ def set_value(self, value: Any) -> Any:
+ """
+ Set the settable attribute on the specified destination object
+ :param value: New value to set
+ :return: New value that the attribute was set to
+ """
+ # Try to update the settable's value
+ orig_value = self.get_value()
+ setattr(self.settable_obj, self.settable_attrib_name, self.val_type(value))
+ new_value = getattr(self.settable_obj, self.settable_attrib_name)
+
+ # Check if we need to call an onchange callback
+ if orig_value != new_value and self.onchange_cb:
+ self.onchange_cb(self.name, orig_value, new_value)
+ return new_value
+
def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]], default_values: collections_abc.Iterable = ()):
"""
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index b4b13945..cd03e66e 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -176,7 +176,7 @@ Parameter 'qqq' not supported (type 'set' for list of parameters).
def test_set_no_settables(base_app):
- base_app.settables = {}
+ base_app._settables.clear()
out, err = run_cmd(base_app, 'set quiet True')
expected = normalize("There are no settable parameters")
assert err == expected
@@ -229,11 +229,10 @@ 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
-You changed quiet
"""
)
assert out == expected