summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2020-02-06 18:20:50 -0500
committerGitHub <noreply@github.com>2020-02-06 18:20:50 -0500
commitc7ac2e965d025806ce5baddfc718814e3e58d639 (patch)
tree6e046c2445edf62b024a9389dc719865584dc9cf
parent60a212c1c585f0c4c06ffcfeb9882520af8dbf35 (diff)
parentc4893ea8a132c06bc71b0ddd63801604e6f85177 (diff)
downloadcmd2-git-c7ac2e965d025806ce5baddfc718814e3e58d639.tar.gz
Merge pull request #873 from python-cmd2/set_update
Updated set command to support tab completion of values
-rw-r--r--CHANGELOG.md25
-rwxr-xr-xREADME.md2
-rw-r--r--cmd2/__init__.py1
-rw-r--r--cmd2/argparse_completer.py4
-rw-r--r--cmd2/cmd2.py257
-rw-r--r--cmd2/py_bridge.py2
-rw-r--r--cmd2/utils.py120
-rw-r--r--docs/api/utility_classes.rst4
-rw-r--r--docs/api/utility_functions.rst2
-rw-r--r--docs/conf.py6
-rw-r--r--docs/examples/first_app.rst4
-rw-r--r--docs/features/builtin_commands.rst6
-rw-r--r--docs/features/initialization.rst7
-rw-r--r--docs/features/plugins.rst4
-rw-r--r--docs/features/settings.rst29
-rwxr-xr-xexamples/cmd_as_argument.py2
-rwxr-xr-xexamples/colors.py2
-rwxr-xr-xexamples/decorator_example.py2
-rwxr-xr-xexamples/environment.py16
-rwxr-xr-xexamples/example.py2
-rwxr-xr-xexamples/first_app.py2
-rwxr-xr-xexamples/initialization.py4
-rwxr-xr-xexamples/pirate.py2
-rwxr-xr-xexamples/plumbum_colors.py2
-rwxr-xr-xexamples/remove_settable.py19
-rwxr-xr-xexamples/table_display.py2
-rw-r--r--examples/transcripts/exampleSession.txt2
-rw-r--r--examples/transcripts/transcript_regex.txt2
-rw-r--r--tests/conftest.py12
-rwxr-xr-xtests/test_cmd2.py97
-rwxr-xr-xtests/test_completion.py51
-rw-r--r--tests/test_transcript.py4
-rw-r--r--tests/test_utils.py21
-rw-r--r--tests/transcripts/regex_set.txt4
34 files changed, 426 insertions, 295 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 342aeaa0..77ba6b00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,20 @@
-## 0.9.26 (TBD, 2020)
+## 1.0.0-rc1 (TBD, 2020)
* Enhancements
* Changed the default help text to make `help -v` more discoverable
+ * Added `add_settable()` and `remove_settable()` convenience methods to update `self.settable` dictionary
* Breaking changes
* Renamed `locals_in_py` attribute of `cmd2.Cmd` to `self_in_py`
* The following public attributes of `cmd2.Cmd` are no longer settable at runtime by default:
* `continuation_prompt`
* `self_in_py`
* `prompt`
-
+ * `self.settable` changed to `self.settables`
+ * It is now a Dict[str, Settable] instead of Dict[str, str]
+ * setting onchange callbacks have a new method signature and must be added to the
+ Settable instance in order to be called
+ * **set** command now supports tab-completion of values
+ * Removed `cast()` utility function
+
## 0.9.25 (January 26, 2020)
* Enhancements
* Reduced what gets put in package downloadable from PyPI (removed irrelevant CI config files and such)
@@ -69,7 +76,7 @@
* Fix bug where cmd2 ran 'stty sane' command when stdin was not a terminal
* Enhancements
* Send all startup script paths to run_script. Previously we didn't do this if the file was empty, but that
- showed no record of the run_script command in history.
+ showed no record of the run_script command in history.
* Made it easier for developers to override `edit` command by having `do_history` no longer call `do_edit`. This
also removes the need to exclude `edit` command from history list.
* It is no longer necessary to set the `prog` attribute of an argparser with subcommands. cmd2 now automatically
@@ -138,7 +145,7 @@
* Enhancements
* Greatly simplified using argparse-based tab completion. The new interface is a complete overhaul that breaks
the previous way of specifying completion and choices functions. See header of [argparse_custom.py](https://github.com/python-cmd2/cmd2/blob/master/cmd2/argparse_custom.py)
- for more information.
+ for more information.
* Enabled tab completion on multiline commands
* **Renamed Commands Notice**
* The following commands were renamed in the last release and have been removed in this release
@@ -148,7 +155,7 @@
* We apologize for any inconvenience, but the new names are more self-descriptive
* Lots of end users were confused particularly about what exactly `load` should be loading
* Breaking Changes
- * Restored `cmd2.Cmd.statement_parser` to be a public attribute (no underscore)
+ * Restored `cmd2.Cmd.statement_parser` to be a public attribute (no underscore)
* Since it can be useful for creating [post-parsing hooks](https://cmd2.readthedocs.io/en/latest/features/hooks.html#postparsing-hooks)
* Completely overhauled the interface for adding tab completion to argparse arguments. See enhancements for more details.
* `ACArgumentParser` is now called `Cmd2ArgumentParser`
@@ -188,7 +195,7 @@
* `perror` - print a message to sys.stderr
* `pexcept` - print Exception message to sys.stderr. If debug is true, print exception traceback if one exists
* Signature of `poutput` and `perror` significantly changed
- * Removed color parameters `color`, `err_color`, and `war_color` from `poutput` and `perror`
+ * Removed color parameters `color`, `err_color`, and `war_color` from `poutput` and `perror`
* See the docstrings of these methods or the [cmd2 docs](https://cmd2.readthedocs.io/en/latest/features/generating_output.html) for more info on applying styles to output messages
* `end` argument is now keyword-only and cannot be specified positionally
* `traceback_war` no longer exists as an argument since it isn't needed now that `perror` and `pexcept` exist
@@ -198,7 +205,7 @@
* `COLORS_NEVER` --> `ANSI_NEVER`
* `COLORS_TERMINAL` --> `ANSI_TERMINAL`
* **Renamed Commands Notice**
- * The following commands have been renamed. The old names will be supported until the next release.
+ * The following commands have been renamed. The old names will be supported until the next release.
* `load` --> `run_script`
* `_relative_load` --> `_relative_run_script`
* `pyscript` --> `run_pyscript`
@@ -217,7 +224,7 @@
* Fixed issue where `_cmdloop()` suppressed exceptions by returning from within its `finally` code
* Fixed UnsupportedOperation on fileno error when a shell command was one of the commands run while generating
a transcript
- * Fixed bug where history was displaying expanded multiline commands when -x was not specified
+ * Fixed bug where history was displaying expanded multiline commands when -x was not specified
* Enhancements
* **Added capability to chain pipe commands and redirect their output (e.g. !ls -l | grep user | wc -l > out.txt)**
* `pyscript` limits a command's stdout capture to the same period that redirection does.
@@ -238,7 +245,7 @@
* Text scripts now run immediately instead of adding their commands to `cmdqueue`. This allows easy capture of
the entire script's output.
* Added member to `CommandResult` called `stop` which is the return value of `onecmd_plus_hooks` after it runs
- the given command line.
+ the given command line.
* Breaking changes
* Replaced `unquote_redirection_tokens()` with `unquote_specific_tokens()`. This was to support the fix
that allows terminators in alias and macro values.
diff --git a/README.md b/README.md
index cfb071cc..19bd259b 100755
--- a/README.md
+++ b/README.md
@@ -248,7 +248,7 @@ class CmdLineApp(cmd2.Cmd):
super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
# Make maxrepeats settable at runtime
- self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command'))
speak_parser = argparse.ArgumentParser()
speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
diff --git a/cmd2/__init__.py b/cmd2/__init__.py
index 8fc5e9f2..cc5a0963 100644
--- a/cmd2/__init__.py
+++ b/cmd2/__init__.py
@@ -27,3 +27,4 @@ from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
from .decorators import categorize, with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
from .parsing import Statement
from .py_bridge import CommandResult
+from .utils import Settable
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 23fd930e..6513fe13 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -64,10 +64,10 @@ def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool:
# noinspection PyProtectedMember
-class AutoCompleter(object):
+class AutoCompleter:
"""Automatic command line tab completion based on argparse parameters"""
- class _ArgumentState(object):
+ class _ArgumentState:
"""Keeps state of an argument being parsed"""
def __init__(self, arg_action: argparse.Action) -> None:
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 34435ed0..2c35a163 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -47,12 +47,13 @@ from . import ansi
from . import constants
from . import plugin
from . import utils
-from .argparse_custom import CompletionItem, DEFAULT_ARGUMENT_PARSER
+from .argparse_custom import CompletionError, CompletionItem, DEFAULT_ARGUMENT_PARSER
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .decorators import with_argparser
from .history import History, HistoryItem
from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt, rl_warning
+from .utils import Settable
# Set up readline
if rl_type == RlType.NONE: # pragma: no cover
@@ -198,22 +199,9 @@ class Cmd(cmd.Cmd):
# not include the description value of the CompletionItems.
self.max_completion_items = 50
- # To make an attribute settable with the "do_set" command, add it to this ...
- self.settable = \
- {
- # allow_style is a special case in which it's an application-wide setting defined in ansi.py
- 'allow_style': ('Allow ANSI text style sequences in output '
- '(valid values: {}, {}, {})'.format(ansi.STYLE_TERMINAL,
- ansi.STYLE_ALWAYS,
- ansi.STYLE_NEVER)),
- 'debug': 'Show full error stack on error',
- 'echo': 'Echo command issued into output',
- 'editor': 'Program used by ``edit``',
- 'feedback_to_output': 'Include nonessentials in `|`, `>` results',
- 'max_completion_items': 'Maximum number of CompletionItems to display during tab completion',
- 'quiet': "Don't print nonessential feedback",
- 'timing': 'Report execution times'
- }
+ # A dictionary mapping settable names to their Settable instance
+ self.settables = dict()
+ self.build_settables()
# Use as prompt for multiline commands on the 2nd+ line of input
self.continuation_prompt = '> '
@@ -393,6 +381,42 @@ class Cmd(cmd.Cmd):
# If False, then complete() will sort the matches using self.default_sort_key before they are displayed.
self.matches_sorted = False
+ 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
+
+ def remove_settable(self, name: str) -> None:
+ """
+ Convenience method for removing a settable parameter from self.settables
+ :param name: name of the settable being removed
+ :raises: KeyError if the no Settable matches this name
+ """
+ try:
+ del self.settables[name]
+ except KeyError:
+ raise KeyError(name + " is not a settable parameter")
+
+ def build_settables(self):
+ """Populates self.add_settable with parameters that can be edited via the set command"""
+ self.add_settable(Settable('allow_style', str,
+ 'Allow ANSI text style sequences in output (valid values: '
+ '{}, {}, {})'.format(ansi.STYLE_TERMINAL,
+ ansi.STYLE_ALWAYS,
+ ansi.STYLE_NEVER),
+ choices=[ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER]))
+
+ self.add_settable(Settable('debug', bool, "Show full traceback on exception"))
+ self.add_settable(Settable('echo', bool, "Echo command issued into output"))
+ self.add_settable(Settable('editor', str, "Program used by 'edit'"))
+ self.add_settable(Settable('feedback_to_output', bool, "Include nonessentials in '|', '>' results"))
+ self.add_settable(Settable('max_completion_items', int,
+ "Maximum number of CompletionItems to display during tab completion"))
+ self.add_settable(Settable('quiet', bool, "Don't print nonessential feedback"))
+ self.add_settable(Settable('timing', bool, "Report execution times"))
+
# ----- Methods related to presenting output to the user -----
@property
@@ -403,16 +427,13 @@ class Cmd(cmd.Cmd):
@allow_style.setter
def allow_style(self, new_val: str) -> None:
"""Setter property needed to support do_set when it updates allow_style"""
- new_val = new_val.lower()
- if new_val == ansi.STYLE_TERMINAL.lower():
- ansi.allow_style = ansi.STYLE_TERMINAL
- elif new_val == ansi.STYLE_ALWAYS.lower():
- ansi.allow_style = ansi.STYLE_ALWAYS
- elif new_val == ansi.STYLE_NEVER.lower():
- ansi.allow_style = ansi.STYLE_NEVER
+ new_val = new_val.capitalize()
+ if new_val in [ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER]:
+ ansi.allow_style = new_val
else:
- self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.STYLE_TERMINAL,
- ansi.STYLE_ALWAYS, ansi.STYLE_NEVER))
+ raise ValueError('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.STYLE_TERMINAL,
+ ansi.STYLE_ALWAYS,
+ ansi.STYLE_NEVER))
def _completion_supported(self) -> bool:
"""Return whether tab completion is supported"""
@@ -497,7 +518,7 @@ class Cmd(cmd.Cmd):
if apply_style:
final_msg = ansi.style_error(final_msg)
- if not self.debug and 'debug' in self.settable:
+ if not self.debug and 'debug' in self.settables:
warning = "\nTo enable full traceback, run the following command: 'set debug true'"
final_msg += ansi.style_warning(warning)
@@ -1451,7 +1472,7 @@ class Cmd(cmd.Cmd):
def _get_settable_completion_items(self) -> List[CompletionItem]:
"""Return list of current settable names and descriptions as CompletionItems"""
- return [CompletionItem(cur_key, self.settable[cur_key]) for cur_key in self.settable]
+ return [CompletionItem(cur_key, self.settables[cur_key].description) for cur_key in self.settables]
def _get_commands_aliases_and_macros_for_completion(self) -> List[str]:
"""Return a list of visible commands, aliases, and macros for tab completion"""
@@ -2258,11 +2279,10 @@ class Cmd(cmd.Cmd):
alias_parser = DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)
# Add subcommands to alias
- alias_subparsers = alias_parser.add_subparsers(dest='subcommand')
+ alias_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
alias_subparsers.required = True
# alias -> create
- alias_create_help = "create or overwrite an alias"
alias_create_description = "Create or overwrite an alias"
alias_create_epilog = ("Notes:\n"
@@ -2277,7 +2297,7 @@ class Cmd(cmd.Cmd):
" alias create show_log !cat \"log file.txt\"\n"
" alias create save_results print_results \">\" out.txt\n")
- alias_create_parser = alias_subparsers.add_parser('create', help=alias_create_help,
+ alias_create_parser = alias_subparsers.add_parser('create', help=alias_create_description.lower(),
description=alias_create_description,
epilog=alias_create_epilog)
alias_create_parser.add_argument('name', help='name of this alias')
@@ -2435,7 +2455,7 @@ class Cmd(cmd.Cmd):
macro_parser = DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)
# Add subcommands to macro
- macro_subparsers = macro_parser.add_subparsers(dest='subcommand')
+ macro_subparsers = macro_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
macro_subparsers.required = True
# macro -> create
@@ -2594,8 +2614,7 @@ class Cmd(cmd.Cmd):
super().do_help(args.command)
def _help_menu(self, verbose: bool = False) -> None:
- """Show a list of commands which help can be displayed for.
- """
+ """Show a list of commands which help can be displayed for"""
# Get a sorted list of help topics
help_topics = sorted(self.get_help_topics(), key=self.default_sort_key)
@@ -2798,93 +2817,111 @@ class Cmd(cmd.Cmd):
self.poutput("{!r} isn't a valid choice. Pick a number between 1 and {}:".format(
response, len(fulloptions)))
- def _get_read_only_settings(self) -> str:
- """Return a summary report of read-only settings which the user cannot modify at runtime.
-
- :return: The report string
- """
- read_only_settings = """
- Commands may be terminated with: {}
- Output redirection and pipes allowed: {}"""
- return read_only_settings.format(str(self.statement_parser.terminators), self.allow_redirection)
-
- def _show(self, args: argparse.Namespace, parameter: str = '') -> None:
- """Shows current settings of parameters.
+ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int,
+ arg_tokens: Dict[str, List[str]]) -> List[str]:
+ """Completes the value argument of set"""
+ param = arg_tokens['param'][0]
+ try:
+ settable = self.settables[param]
+ except KeyError:
+ raise CompletionError(param + " is not a settable parameter")
+
+ # Create a parser with a value field based on this settable
+ settable_parser = DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent])
+
+ # Settables with choices list the values of those choices instead of the arg name
+ # in help text and this shows in tab-completion hints. Set metavar to avoid this.
+ arg_name = 'value'
+ settable_parser.add_argument(arg_name, metavar=arg_name, help=settable.description,
+ choices=settable.choices,
+ choices_function=settable.choices_function,
+ choices_method=settable.choices_method,
+ completer_function=settable.completer_function,
+ completer_method=settable.completer_method)
- :param args: argparse parsed arguments from the set command
- :param parameter: optional search parameter
- """
- param = utils.norm_fold(parameter.strip())
- result = {}
- maxlen = 0
-
- for p in self.settable:
- if (not param) or p.startswith(param):
- result[p] = '{}: {}'.format(p, str(getattr(self, p)))
- maxlen = max(maxlen, len(result[p]))
-
- if result:
- for p in sorted(result, key=self.default_sort_key):
- if args.long:
- self.poutput('{} # {}'.format(result[p].ljust(maxlen), self.settable[p]))
- else:
- self.poutput(result[p])
+ from .argparse_completer import AutoCompleter
+ completer = AutoCompleter(settable_parser, self)
- # If user has requested to see all settings, also show read-only settings
- if args.all:
- self.poutput('\nRead only settings:{}'.format(self._get_read_only_settings()))
- else:
- self.perror("Parameter '{}' not supported (type 'set' for list of parameters).".format(param))
+ # Use raw_tokens since quotes have been preserved
+ _, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
+ return completer.complete_command(raw_tokens, text, line, begidx, endidx)
+ # When tab completing value, we recreate the set command parser with a value argument specific to
+ # the settable being edited. To make this easier, define a parent parser with all the common elements.
set_description = ("Set a settable parameter or show current settings of parameters\n"
- "\n"
- "Accepts abbreviated parameter names so long as there is no ambiguity.\n"
- "Call without arguments for a list of settable parameters with their values.")
-
- set_parser = DEFAULT_ARGUMENT_PARSER(description=set_description)
- set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well')
- set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter')
- set_parser.add_argument('param', nargs=argparse.OPTIONAL, help='parameter to set or view',
- choices_method=_get_settable_completion_items, descriptive_header='Description')
- set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='the new value for settable')
-
- @with_argparser(set_parser)
+ "Call without arguments for a list of all settable parameters with their values.\n"
+ "Call with just param to view that parameter's value.")
+ set_parser_parent = DEFAULT_ARGUMENT_PARSER(description=set_description, add_help=False)
+ set_parser_parent.add_argument('-l', '--long', action='store_true',
+ help='include description of parameters when viewing')
+ set_parser_parent.add_argument('param', nargs=argparse.OPTIONAL, help='parameter to set or view',
+ choices_method=_get_settable_completion_items, descriptive_header='Description')
+
+ # Create the parser for the set command
+ set_parser = DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent])
+
+ # Suppress tab-completion hints for this field. The completer method is going to create an
+ # AutoCompleter based on the actual parameter being completed and we only want that hint printing.
+ set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='new value for settable',
+ completer_method=complete_set_value, suppress_tab_hint=True)
+
+ # Preserve quotes so users can pass in quoted empty strings and flags (e.g. -h) as the value
+ @with_argparser(set_parser, preserve_quotes=True)
def do_set(self, args: argparse.Namespace) -> None:
"""Set a settable parameter or show current settings of parameters"""
+ if not self.settables:
+ self.pwarning("There are no settable parameters")
+ return
- # Check if param was passed in
- if not args.param:
- return self._show(args)
- param = utils.norm_fold(args.param.strip())
-
- # Check if value was passed in
- if not args.value:
- return self._show(args, param)
- value = args.value
-
- # Check if param points to just one settable
- if param not in self.settable:
- hits = [p for p in self.settable if p.startswith(param)]
- if len(hits) == 1:
- param = hits[0]
- else:
- return self._show(args, param)
+ if args.param:
+ try:
+ settable = self.settables[args.param]
+ except KeyError:
+ self.perror("Parameter '{}' not supported (type 'set' for list of parameters).".format(args.param))
+ return
- # Update the settable's value
- orig_value = getattr(self, param)
- setattr(self, param, utils.cast(orig_value, value))
+ if args.value:
+ args.value = utils.strip_quotes(args.value)
- # In cases where a Python property is used to validate and update a settable's value, its value will not
- # change if the passed in one is invalid. Therefore we should read its actual value back and not assume.
- new_value = getattr(self, param)
+ # Try to update the settable's value
+ try:
+ orig_value = getattr(self, args.param)
+ new_value = settable.val_type(args.value)
+ setattr(self, args.param, new_value)
+ # noinspection PyBroadException
+ except Exception as e:
+ err_msg = "Error setting {}: {}".format(args.param, e)
+ self.perror(err_msg)
+ return
- self.poutput('{} - was: {}\nnow: {}'.format(param, orig_value, new_value))
+ self.poutput('{} - was: {!r}\nnow: {!r}'.format(args.param, orig_value, new_value))
- # See if we need to call a change hook for this settable
- if orig_value != new_value:
- onchange_hook = getattr(self, '_onchange_{}'.format(param), None)
- if onchange_hook is not None:
- onchange_hook(old=orig_value, new=new_value) # pylint: disable=not-callable
+ # 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)
+ return
+
+ # Show one settable
+ to_show = [args.param]
+ else:
+ # Show all settables
+ to_show = list(self.settables.keys())
+
+ # Build the result strings
+ max_len = 0
+ results = dict()
+ for param in to_show:
+ results[param] = '{}: {!r}'.format(param, getattr(self, param))
+ max_len = max(max_len, ansi.style_aware_wcswidth(results[param]))
+
+ # Display the results
+ for param in sorted(results, key=self.default_sort_key):
+ result_str = results[param]
+ if args.long:
+ self.poutput('{} # {}'.format(utils.align_left(result_str, width=max_len),
+ self.settables[param].description))
+ else:
+ self.poutput(result_str)
shell_parser = DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt")
shell_parser.add_argument('command', help='the command to run', completer_method=shell_cmd_complete)
diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py
index 0a1b6ee7..b7346d22 100644
--- a/cmd2/py_bridge.py
+++ b/cmd2/py_bridge.py
@@ -53,7 +53,7 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
return not self.stderr
-class PyBridge(object):
+class PyBridge:
"""Provides a Python API wrapper for application commands."""
def __init__(self, cmd2_app):
self._cmd2_app = cmd2_app
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 42248884..cfe75f53 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -2,6 +2,7 @@
"""Shared utility functions"""
import collections
+import collections.abc as collections_abc
import glob
import os
import re
@@ -9,9 +10,8 @@ import subprocess
import sys
import threading
import unicodedata
-import collections.abc as collections_abc
from enum import Enum
-from typing import Any, Iterable, List, Optional, TextIO, Union
+from typing import Any, Callable, Iterable, List, Optional, TextIO, Union
from . import constants
@@ -57,6 +57,78 @@ def strip_quotes(arg: str) -> str:
return arg
+def str_to_bool(val: str) -> bool:
+ """Converts a string to a boolean based on its value.
+
+ :param val: string being converted
+ :return: boolean value expressed in the string
+ :raises: ValueError if the string does not contain a value corresponding to a boolean value
+ """
+ if isinstance(val, str):
+ if val.capitalize() == str(True):
+ return True
+ elif val.capitalize() == str(False):
+ return False
+ raise ValueError("must be True or False (case-insensitive)")
+
+
+class Settable:
+ """Used to configure a cmd2 instance member to be settable via the set command in the CLI"""
+ def __init__(self, name: str, val_type: Callable, description: str, *,
+ onchange_cb: Callable[[str, Any, Any], Any] = None,
+ choices: Iterable = None,
+ choices_function: Optional[Callable] = None,
+ choices_method: Optional[Callable] = None,
+ completer_function: Optional[Callable] = None,
+ completer_method: Optional[Callable] = None):
+ """
+ Settable Initializer
+
+ :param name: name of the instance attribute being made settable
+ :param val_type: callable used to cast the string value from the command line into its proper type and
+ 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 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)
+
+ Cmd.do_set() passes the following 3 arguments to onchange_cb:
+ param_name: str - name of the changed parameter
+ old_value: Any - the value before being changed
+ new_value: Any - the value after being changed
+
+ The following optional settings provide tab completion for a parameter's values. They correspond to the
+ same settings in argparse-based tab completion. A maximum of one of these should be provided.
+
+ :param choices: iterable of accepted values
+ :param choices_function: function that provides choices for this argument
+ :param choices_method: cmd2-app method that provides choices for this argument (See note below)
+ :param completer_function: tab-completion function that provides choices for this argument
+ :param completer_method: cmd2-app tab-completion method that provides choices
+ for this argument (See note below)
+
+ Note:
+ For choices_method and completer_method, do not set them to a bound method. This is because AutoCompleter
+ passes the self argument explicitly to these functions.
+
+ Therefore instead of passing something like self.path_complete, pass cmd2.Cmd.path_complete.
+ """
+ if val_type == bool:
+ val_type = str_to_bool
+ choices = ['true', 'false']
+
+ self.name = name
+ self.val_type = val_type
+ self.description = description
+ self.onchange_cb = onchange_cb
+ self.choices = choices
+ self.choices_function = choices_function
+ self.choices_method = choices_method
+ self.completer_function = completer_function
+ self.completer_method = completer_method
+
+
def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]],
default_values: collections_abc.Iterable = ()):
"""
@@ -88,38 +160,6 @@ def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]],
return T
-def cast(current: Any, new: str) -> Any:
- """Tries to force a new value into the same type as the current when trying to set the value for a parameter.
-
- :param current: current value for the parameter, type varies
- :param new: new value
- :return: new value with same type as current, or the current value if there was an error casting
- """
- typ = type(current)
- orig_new = new
-
- if typ == bool:
- try:
- return bool(int(new))
- except (ValueError, TypeError):
- pass
- try:
- new = new.lower()
- if (new == 'on') or (new[0] in ('y', 't')):
- return True
- if (new == 'off') or (new[0] in ('n', 'f')):
- return False
- except AttributeError:
- pass
- else:
- try:
- return typ(new)
- except (ValueError, TypeError):
- pass
- print("Problem setting parameter (now {}) to {}; incorrect type?".format(current, orig_new))
- return current
-
-
def which(exe_name: str) -> Optional[str]:
"""Find the full path of a given executable on a Linux or Mac machine
@@ -364,7 +404,7 @@ def get_exes_in_path(starts_with: str) -> List[str]:
return list(exes_set)
-class StdSim(object):
+class StdSim:
"""
Class to simulate behavior of sys.stdout or sys.stderr.
Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
@@ -372,7 +412,7 @@ class StdSim(object):
def __init__(self, inner_stream, echo: bool = False,
encoding: str = 'utf-8', errors: str = 'replace') -> None:
"""
- Initializer
+ StdSim Initializer
:param inner_stream: the wrapped stream. Should be a TextIO or StdSim instance.
:param echo: if True, then all input will be echoed to inner_stream
:param encoding: codec for encoding/decoding strings (defaults to utf-8)
@@ -444,7 +484,7 @@ class StdSim(object):
return getattr(self.inner_stream, item)
-class ByteBuf(object):
+class ByteBuf:
"""
Used by StdSim to write binary data and stores the actual bytes written
"""
@@ -473,7 +513,7 @@ class ByteBuf(object):
self.std_sim_instance.flush()
-class ProcReader(object):
+class ProcReader:
"""
Used to capture stdout and stderr from a Popen process if any of those were set to subprocess.PIPE.
If neither are pipes, then the process will run normally and no output will be captured.
@@ -575,7 +615,7 @@ class ProcReader(object):
pass
-class ContextFlag(object):
+class ContextFlag:
"""A context manager which is also used as a boolean flag value within the default sigint handler.
Its main use is as a flag to prevent the SIGINT handler in cmd2 from raising a KeyboardInterrupt
@@ -599,7 +639,7 @@ class ContextFlag(object):
raise ValueError("count has gone below 0")
-class RedirectionSavedState(object):
+class RedirectionSavedState:
"""Created by each command to store information about their redirection."""
def __init__(self, self_stdout: Union[StdSim, TextIO], sys_stdout: Union[StdSim, TextIO],
diff --git a/docs/api/utility_classes.rst b/docs/api/utility_classes.rst
index 7ed0c584..2ee92ced 100644
--- a/docs/api/utility_classes.rst
+++ b/docs/api/utility_classes.rst
@@ -1,6 +1,10 @@
Utility Classes
===============
+.. autoclass:: cmd2.utils.Settable
+
+ .. automethod:: __init__
+
.. autoclass:: cmd2.utils.StdSim
.. autoclass:: cmd2.utils.ByteBuf
diff --git a/docs/api/utility_functions.rst b/docs/api/utility_functions.rst
index e2d6f036..4f788e3d 100644
--- a/docs/api/utility_functions.rst
+++ b/docs/api/utility_functions.rst
@@ -23,8 +23,6 @@ Utility Functions
.. autofunction:: cmd2.utils.namedtuple_with_defaults
-.. autofunction:: cmd2.utils.cast
-
.. autofunction:: cmd2.utils.which
.. autofunction:: cmd2.utils.is_text_file
diff --git a/docs/conf.py b/docs/conf.py
index 7a8da9d1..02eb827f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -17,17 +17,11 @@ If extensions (or modules to document with autodoc) are in another directory,
add these directories to sys.path here. If the directory is relative to the
documentation root, use os.path.abspath to make it absolute, like shown here.
"""
-import os
-import sys
-
from pkg_resources import get_distribution
# Import for custom theme from Read the Docs
import sphinx_rtd_theme
-sys.path.insert(0, os.path.abspath('..'))
-
-
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
diff --git a/docs/examples/first_app.rst b/docs/examples/first_app.rst
index 19d573b4..310c8d0c 100644
--- a/docs/examples/first_app.rst
+++ b/docs/examples/first_app.rst
@@ -67,7 +67,7 @@ initializer to our class::
# Make maxrepeats settable at runtime
self.maxrepeats = 3
- self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command'))
In that initializer, the first thing to do is to make sure we initialize
``cmd2``. That's what the ``super().__init__()`` line does. Then we create an
@@ -203,7 +203,7 @@ method so it looks like this::
# Make maxrepeats settable at runtime
self.maxrepeats = 3
- self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command'))
Shortcuts are passed to the ``cmd2`` initializer, and if you want the built-in
shortcuts of ``cmd2`` you have to pass them. These shortcuts are defined as a
diff --git a/docs/features/builtin_commands.rst b/docs/features/builtin_commands.rst
index 025149b3..83d3176d 100644
--- a/docs/features/builtin_commands.rst
+++ b/docs/features/builtin_commands.rst
@@ -93,10 +93,10 @@ within a running application:
(Cmd) set --long
allow_style: Terminal # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never)
- debug: False # Show full error stack on error
+ debug: False # Show full traceback on exception
echo: False # Echo command issued into output
- editor: vim # Program used by ``edit``
- feedback_to_output: False # include nonessentials in `|`, `>` results
+ editor: vim # Program used by 'edit'
+ feedback_to_output: False # include nonessentials in '|', '>' results
max_completion_items: 50 # Maximum number of CompletionItems to display during tab completion
quiet: False # Don't print nonessential feedback
timing: False # Report execution times
diff --git a/docs/features/initialization.rst b/docs/features/initialization.rst
index 46b4ecd2..d48290fa 100644
--- a/docs/features/initialization.rst
+++ b/docs/features/initialization.rst
@@ -20,7 +20,7 @@ capabilities which you may wish to utilize while initializing the app::
"""
import cmd2
from cmd2 import style
-
+ from cmd2.ansi import FG_COLORS
class BasicApp(cmd2.Cmd):
CUSTOM_CATEGORY = 'My Custom Commands'
@@ -48,7 +48,10 @@ capabilities which you may wish to utilize while initializing the app::
self.foreground_color = 'cyan'
# Make echo_fg settable at runtime
- self.settable['foreground_color'] = 'Foreground color to use with echo command'
+ self.add_settable(cmd2.Settable('foreground_color',
+ str,
+ 'Foreground color to use with echo command',
+ choices=FG_COLORS))
@cmd2.with_category(CUSTOM_CATEGORY)
def do_intro(self, _):
diff --git a/docs/features/plugins.rst b/docs/features/plugins.rst
index caa46b8c..00c0a9f0 100644
--- a/docs/features/plugins.rst
+++ b/docs/features/plugins.rst
@@ -82,10 +82,10 @@ example::
super().__init__(*args, **kwargs)
# code placed here runs after cmd2.Cmd initializes
self.mysetting = 'somevalue'
- self.settable.update({'mysetting': 'short help message for mysetting'})
+ self.add_settable(cmd2.Settable('mysetting', str, 'short help message for mysetting'))
You can also hide settings from the user by removing them from
-``self.settable``.
+``self.settables``.
Decorators
~~~~~~~~~~
diff --git a/docs/features/settings.rst b/docs/features/settings.rst
index 55b6a10d..40b9bc35 100644
--- a/docs/features/settings.rst
+++ b/docs/features/settings.rst
@@ -3,8 +3,8 @@ Settings
Settings provide a mechanism for a user to control the behavior of a ``cmd2``
based application. A setting is stored in an instance attribute on your
-subclass of :class:`cmd2.cmd2.Cmd` and must also appear in the
-:attr:`cmd2.cmd2.Cmd.settable` dictionary. Developers may set default values
+subclass of :class:`.cmd2.Cmd` and must also appear in the
+:attr:`~.cmd2.Cmd.settable` dictionary. Developers may set default values
for these settings and users can modify them at runtime using the
:ref:`features/builtin_commands:set` command. Developers can
:ref:`features/settings:Create New Settings` and can also
@@ -116,15 +116,21 @@ Create New Settings
-------------------
Your application can define user-settable parameters which your code can
-reference. First create a class attribute with the default value. Then update
-the ``settable`` dictionary with your setting name and a short description
-before you initialize the superclass. Here's an example, from
+reference. In your initialization code:
+
+1. Create an instance attribute with a default value.
+2. Create a :class:`.Settable` object which describes your setting.
+3. Pass the :class:`.Settable` object to
+ :meth:`cmd2.cmd2.Cmd.add_settable`.
+
+Here's an example, from
``examples/environment.py``:
.. literalinclude:: ../../examples/environment.py
-If you want to be notified when a setting changes (as we do above), then define
-a method ``_onchange_{setting}()``. This method will be called after the user
+If you want to be notified when a setting changes (as we do above), then be
+sure to supply a method to the ``onchange_cb`` parameter of the
+`.cmd2.utils.Settable`. This method will be called after the user
changes a setting, and will receive both the old value and the new value.
.. code-block:: text
@@ -150,19 +156,20 @@ changes a setting, and will receive both the old value and the new value.
Hide Builtin Settings
-----------------------
+---------------------
You may want to prevent a user from modifying a builtin setting. A setting
-must appear in the :attr:`cmd2.cmd2.Cmd.settable` dictionary in order for it
+must appear in the :attr:`~.cmd2.Cmd.settable` dictionary in order for it
to be available to the :ref:`features/builtin_commands:set` command.
Let's say that you never want end users of your program to be able to enable
full debug tracebacks to print out if an error occurs. You might want to hide
the :ref:`features/settings:debug` setting. To do so, remove it from the
-:attr:`cmd2.cmd2.Cmd.settable` dictionary after you initialize your object::
+:attr:`~.cmd2.Cmd.settable` dictionary after you initialize your object.
+The :meth:`~.cmd2.Cmd.remove_settable` convenience method makes this easy::
class MyApp(cmd2.Cmd):
def __init__(self):
super().__init__()
- self.settable.pop('debug')
+ self.remove_settable('debug')
diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py
index 08643a50..b65bcbcb 100755
--- a/examples/cmd_as_argument.py
+++ b/examples/cmd_as_argument.py
@@ -36,7 +36,7 @@ class CmdLineApp(cmd2.Cmd):
self.self_in_py = True
self.maxrepeats = 3
# Make maxrepeats settable at runtime
- self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command'))
speak_parser = argparse.ArgumentParser()
speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
diff --git a/examples/colors.py b/examples/colors.py
index bbb3b2ad..33b17e53 100755
--- a/examples/colors.py
+++ b/examples/colors.py
@@ -39,7 +39,7 @@ class CmdLineApp(cmd2.Cmd):
self.maxrepeats = 3
# Make maxrepeats settable at runtime
- self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command'))
# Should ANSI color output be allowed
self.allow_style = ansi.STYLE_TERMINAL
diff --git a/examples/decorator_example.py b/examples/decorator_example.py
index 4f68653e..0f5374ce 100755
--- a/examples/decorator_example.py
+++ b/examples/decorator_example.py
@@ -27,7 +27,7 @@ class CmdLineApp(cmd2.Cmd):
self.maxrepeats = 3
# Make maxrepeats settable at runtime
- self.settable['maxrepeats'] = 'Max number of `--repeat`s allowed'
+ self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command'))
# Example of args set from the command-line (but they aren't being used here)
self._ip = ip_addr
diff --git a/examples/environment.py b/examples/environment.py
index 9e611f08..670b63ac 100755
--- a/examples/environment.py
+++ b/examples/environment.py
@@ -9,15 +9,19 @@ import cmd2
class EnvironmentApp(cmd2.Cmd):
""" Example cmd2 application. """
- degrees_c = 22
- sunny = False
-
def __init__(self):
super().__init__()
- self.settable.update({'degrees_c': 'Temperature in Celsius'})
- self.settable.update({'sunny': 'Is it sunny outside?'})
+ self.degrees_c = 22
+ self.sunny = False
+ self.add_settable(cmd2.Settable('degrees_c',
+ int,
+ 'Temperature in Celsius',
+ onchange_cb=self._onchange_degrees_c
+ ))
+ self.add_settable(cmd2.Settable('sunny', bool, 'Is it sunny outside?'))
def do_sunbathe(self, arg):
+ """Attempt to sunbathe."""
if self.degrees_c < 20:
result = "It's {} C - are you a penguin?".format(self.degrees_c)
elif not self.sunny:
@@ -26,7 +30,7 @@ class EnvironmentApp(cmd2.Cmd):
result = 'UV is bad for your skin.'
self.poutput(result)
- def _onchange_degrees_c(self, old, new):
+ def _onchange_degrees_c(self, param_name, old, new):
# if it's over 40C, it's gotta be sunny, right?
if new > 40:
self.sunny = True
diff --git a/examples/example.py b/examples/example.py
index b8f8202c..0272a6e5 100755
--- a/examples/example.py
+++ b/examples/example.py
@@ -32,7 +32,7 @@ class CmdLineApp(cmd2.Cmd):
# Make maxrepeats settable at runtime
self.maxrepeats = 3
- self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command'))
speak_parser = argparse.ArgumentParser()
speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
diff --git a/examples/first_app.py b/examples/first_app.py
index b5bd07e9..d8272e86 100755
--- a/examples/first_app.py
+++ b/examples/first_app.py
@@ -27,7 +27,7 @@ class FirstApp(cmd2.Cmd):
# Make maxrepeats settable at runtime
self.maxrepeats = 3
- self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command'))
speak_parser = argparse.ArgumentParser()
speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
diff --git a/examples/initialization.py b/examples/initialization.py
index 32aa852f..c13ed137 100755
--- a/examples/initialization.py
+++ b/examples/initialization.py
@@ -14,6 +14,7 @@
"""
import cmd2
from cmd2 import style
+from cmd2.ansi import FG_COLORS
class BasicApp(cmd2.Cmd):
@@ -42,7 +43,8 @@ class BasicApp(cmd2.Cmd):
self.foreground_color = 'cyan'
# Make echo_fg settable at runtime
- self.settable['foreground_color'] = 'Foreground color to use with echo command'
+ self.add_settable(cmd2.Settable('foreground_color', str, 'Foreground color to use with echo command',
+ choices=FG_COLORS))
@cmd2.with_category(CUSTOM_CATEGORY)
def do_intro(self, _):
diff --git a/examples/pirate.py b/examples/pirate.py
index eda3994e..acbab17c 100755
--- a/examples/pirate.py
+++ b/examples/pirate.py
@@ -25,7 +25,7 @@ class Pirate(cmd2.Cmd):
self.songcolor = 'blue'
# Make songcolor settable at runtime
- self.settable['songcolor'] = 'Color to ``sing`` in (black/red/green/yellow/blue/magenta/cyan/white)'
+ self.add_settable(cmd2.Settable('songcolor', str, 'Color to ``sing``', choices=cmd2.ansi.FG_COLORS))
# prompts and defaults
self.gold = 0
diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py
index fe692805..94815f50 100755
--- a/examples/plumbum_colors.py
+++ b/examples/plumbum_colors.py
@@ -75,7 +75,7 @@ class CmdLineApp(cmd2.Cmd):
self.maxrepeats = 3
# Make maxrepeats settable at runtime
- self.settable['maxrepeats'] = 'max repetitions for speak command'
+ self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command'))
# Should ANSI color output be allowed
self.allow_style = ansi.STYLE_TERMINAL
diff --git a/examples/remove_settable.py b/examples/remove_settable.py
new file mode 100755
index 00000000..6a2e4062
--- /dev/null
+++ b/examples/remove_settable.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+# coding=utf-8
+"""
+A sample application for cmd2 demonstrating how to remove one of the built-in runtime settable parameters.
+"""
+import cmd2
+
+
+class MyApp(cmd2.Cmd):
+
+ def __init__(self):
+ super().__init__()
+ self.remove_settable('debug')
+
+
+if __name__ == '__main__':
+ import sys
+ c = MyApp()
+ sys.exit(c.cmdloop())
diff --git a/examples/table_display.py b/examples/table_display.py
index a8fd2cb0..01143598 100755
--- a/examples/table_display.py
+++ b/examples/table_display.py
@@ -77,7 +77,7 @@ COLUMNS = [tf.Column('City', width=11, header_halign=tf.ColumnAlignment.AlignCen
# ######## Table data formatted as an iterable of python objects #########
-class CityInfo(object):
+class CityInfo:
"""City information container"""
def __init__(self, city: str, province: str, country: str, continent: str, population: int, area: float):
self.city = city
diff --git a/examples/transcripts/exampleSession.txt b/examples/transcripts/exampleSession.txt
index 54419f91..8a60f487 100644
--- a/examples/transcripts/exampleSession.txt
+++ b/examples/transcripts/exampleSession.txt
@@ -3,7 +3,7 @@
# The regex for editor will match whatever program you use.
# regexes on prompts just make the trailing space obvious
(Cmd) set
-allow_style: /(Terminal|Always|Never)/
+allow_style: '/(Terminal|Always|Never)/'
debug: False
echo: False
editor: /.*?/
diff --git a/examples/transcripts/transcript_regex.txt b/examples/transcripts/transcript_regex.txt
index 35fc5817..ce2a2beb 100644
--- a/examples/transcripts/transcript_regex.txt
+++ b/examples/transcripts/transcript_regex.txt
@@ -3,7 +3,7 @@
# The regex for editor will match whatever program you use.
# regexes on prompts just make the trailing space obvious
(Cmd) set
-allow_style: /(Terminal|Always|Never)/
+allow_style: '/(Terminal|Always|Never)/'
debug: False
echo: False
editor: /.*?/
diff --git a/tests/conftest.py b/tests/conftest.py
index b8abc4a5..7f77a207 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -88,10 +88,10 @@ SHORTCUTS_TXT = """Shortcuts for other commands:
"""
# Output from the show command with default settings
-SHOW_TXT = """allow_style: Terminal
+SHOW_TXT = """allow_style: 'Terminal'
debug: False
echo: False
-editor: vim
+editor: 'vim'
feedback_to_output: False
max_completion_items: 50
quiet: False
@@ -99,11 +99,11 @@ timing: False
"""
SHOW_LONG = """
-allow_style: Terminal # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never)
-debug: False # Show full error stack on error
+allow_style: 'Terminal' # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never)
+debug: False # Show full traceback on exception
echo: False # Echo command issued into output
-editor: vim # Program used by ``edit``
-feedback_to_output: False # Include nonessentials in `|`, `>` results
+editor: 'vim' # Program used by 'edit'
+feedback_to_output: False # Include nonessentials in '|', '>' results
max_completion_items: 50 # Maximum number of CompletionItems to display during tab completion
quiet: False # Don't print nonessential feedback
timing: False # Report execution times
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index a3dbe1be..0b4c60d6 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -79,7 +79,7 @@ def test_base_argparse_help(base_app):
def test_base_invalid_option(base_app):
out, err = run_cmd(base_app, 'set -z')
- assert err[0] == 'Usage: set [-h] [-a] [-l] [param] [value]'
+ assert err[0] == 'Usage: set [-h] [-l] [param] [value]'
assert 'Error: unrecognized arguments: -z' in err[1]
def test_base_shortcuts(base_app):
@@ -108,56 +108,7 @@ def test_base_show_long(base_app):
assert out == expected
-def test_base_show_readonly(base_app):
- base_app.editor = 'vim'
- out, err = run_cmd(base_app, 'set -a')
- expected = normalize(SHOW_TXT + '\nRead only settings:' + """
- Commands may be terminated with: {}
- Output redirection and pipes allowed: {}
-""".format(base_app.statement_parser.terminators, base_app.allow_redirection))
- assert out == expected
-
-
-def test_cast():
- # Boolean
- assert utils.cast(True, True) == True
- assert utils.cast(True, False) == False
- assert utils.cast(True, 0) == False
- assert utils.cast(True, 1) == True
- assert utils.cast(True, 'on') == True
- assert utils.cast(True, 'off') == False
- assert utils.cast(True, 'ON') == True
- assert utils.cast(True, 'OFF') == False
- assert utils.cast(True, 'y') == True
- assert utils.cast(True, 'n') == False
- assert utils.cast(True, 't') == True
- assert utils.cast(True, 'f') == False
-
- # Non-boolean same type
- assert utils.cast(1, 5) == 5
- assert utils.cast(3.4, 2.7) == 2.7
- assert utils.cast('foo', 'bar') == 'bar'
- assert utils.cast([1,2], [3,4]) == [3,4]
-
-def test_cast_problems(capsys):
- expected = 'Problem setting parameter (now {}) to {}; incorrect type?\n'
-
- # Boolean current, with new value not convertible to bool
- current = True
- new = [True, True]
- assert utils.cast(current, new) == current
- out, err = capsys.readouterr()
- assert out == expected.format(current, new)
-
- # Non-boolean current, with new value not convertible to current type
- current = 1
- new = 'octopus'
- assert utils.cast(current, new) == current
- out, err = capsys.readouterr()
- assert out == expected.format(current, new)
-
-
-def test_base_set(base_app):
+def test_set(base_app):
out, err = run_cmd(base_app, 'set quiet True')
expected = normalize("""
quiet - was: False
@@ -168,6 +119,16 @@ now: True
out, err = run_cmd(base_app, 'set quiet')
assert out == ['quiet: True']
+def test_set_val_empty(base_app):
+ base_app.editor = "fake"
+ out, err = run_cmd(base_app, 'set editor ""')
+ assert base_app.editor == ''
+
+def test_set_val_is_flag(base_app):
+ base_app.editor = "fake"
+ out, err = run_cmd(base_app, 'set editor "-h"')
+ assert base_app.editor == '-h'
+
def test_set_not_supported(base_app):
out, err = run_cmd(base_app, 'set qqq True')
expected = normalize("""
@@ -175,16 +136,13 @@ Parameter 'qqq' not supported (type 'set' for list of parameters).
""")
assert err == expected
-def test_set_quiet(base_app):
- out, err = run_cmd(base_app, 'set quie True')
- expected = normalize("""
-quiet - was: False
-now: True
-""")
- assert out == expected
- out, err = run_cmd(base_app, 'set quiet')
- assert out == ['quiet: True']
+def test_set_no_settables(base_app):
+ base_app.settables = {}
+ out, err = run_cmd(base_app, 'set quiet True')
+ expected = normalize("There are no settable parameters")
+ assert err == expected
+
@pytest.mark.parametrize('new_val, is_valid, expected', [
(ansi.STYLE_NEVER, False, ansi.STYLE_NEVER),
@@ -214,10 +172,11 @@ def test_set_allow_style(base_app, new_val, is_valid, expected):
class OnChangeHookApp(cmd2.Cmd):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ self.add_settable(utils.Settable('quiet', bool, "my description", onchange_cb=self._onchange_quiet))
- def _onchange_quiet(self, old, new) -> None:
+ def _onchange_quiet(self, name, old, new) -> None:
"""Runs when quiet is changed via set command"""
- self.poutput("You changed quiet")
+ self.poutput("You changed " + name)
@pytest.fixture
def onchange_app():
@@ -671,7 +630,7 @@ now: True
def test_debug_not_settable(base_app):
# Set debug to False and make it unsettable
base_app.debug = False
- del base_app.settable['debug']
+ base_app.remove_settable('debug')
# Cause an exception
out, err = run_cmd(base_app, 'bad "quote')
@@ -679,6 +638,10 @@ def test_debug_not_settable(base_app):
# Since debug is unsettable, the user will not be given the option to enable a full traceback
assert err == ['Invalid syntax: No closing quotation']
+def test_remove_settable_keyerror(base_app):
+ with pytest.raises(KeyError):
+ base_app.remove_settable('fake')
+
def test_edit_file(base_app, request, monkeypatch):
# Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock
base_app.editor = 'fooedit'
@@ -1583,13 +1546,13 @@ def test_get_macro_completion_items(base_app):
def test_get_settable_completion_items(base_app):
results = base_app._get_settable_completion_items()
for cur_res in results:
- assert cur_res in base_app.settable
- assert cur_res.description == base_app.settable[cur_res]
+ assert cur_res in base_app.settables
+ assert cur_res.description == base_app.settables[cur_res].description
def test_alias_no_subcommand(base_app):
out, err = run_cmd(base_app, 'alias')
assert "Usage: alias [-h]" in err[0]
- assert "Error: the following arguments are required: subcommand" in err[1]
+ assert "Error: the following arguments are required: SUBCOMMAND" in err[1]
def test_alias_create(base_app):
# Create the alias
@@ -1683,7 +1646,7 @@ def test_multiple_aliases(base_app):
def test_macro_no_subcommand(base_app):
out, err = run_cmd(base_app, 'macro')
assert "Usage: macro [-h]" in err[0]
- assert "Error: the following arguments are required: subcommand" in err[1]
+ assert "Error: the following arguments are required: SUBCOMMAND" in err[1]
def test_macro_create(base_app):
# Create the macro
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 475b44dd..99f832a4 100755
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -62,6 +62,9 @@ class CompletionsExample(cmd2.Cmd):
"""
def __init__(self):
cmd2.Cmd.__init__(self, multiline_commands=['test_multiline'])
+ self.foo = 'bar'
+ self.add_settable(utils.Settable('foo', str, description="a settable param",
+ completer_method=CompletionsExample.complete_foo_val))
def do_test_basic(self, args):
pass
@@ -98,6 +101,13 @@ class CompletionsExample(cmd2.Cmd):
"""Completing this should result in completedefault() being called"""
pass
+ def complete_foo_val(self, text, line, begidx, endidx, arg_tokens):
+ """Supports unit testing cmd2.Cmd2.complete_set_val to confirm it passes all tokens in the set command"""
+ if 'param' in arg_tokens:
+ return ["SUCCESS"]
+ else:
+ return ["FAIL"]
+
def completedefault(self, *ignored):
"""Method called to complete an input line when no command-specific
complete_*() method is available.
@@ -949,6 +959,27 @@ def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type):
path_complete_mock.assert_not_called()
default_complete_mock.assert_not_called()
+def test_complete_set_value(cmd2_app):
+ text = ''
+ line = 'set foo {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match == "SUCCESS "
+
+def test_complete_set_value_invalid_settable(cmd2_app, capsys):
+ text = ''
+ line = 'set fake {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
+ assert first_match is None
+
+ out, err = capsys.readouterr()
+ assert "fake is not a settable parameter" in out
+
@pytest.fixture
def sc_app():
c = SubcommandsExample()
@@ -985,7 +1016,7 @@ def test_cmd2_subcommand_completion_nomatch(sc_app):
assert first_match is None
-def test_cmd2_help_subcommand_completion_single(sc_app):
+def test_help_subcommand_completion_single(sc_app):
text = 'base'
line = 'help {}'.format(text)
endidx = len(line)
@@ -996,7 +1027,7 @@ def test_cmd2_help_subcommand_completion_single(sc_app):
# It is at end of line, so extra space is present
assert first_match is not None and sc_app.completion_matches == ['base ']
-def test_cmd2_help_subcommand_completion_multiple(sc_app):
+def test_help_subcommand_completion_multiple(sc_app):
text = ''
line = 'help base {}'.format(text)
endidx = len(line)
@@ -1006,7 +1037,7 @@ def test_cmd2_help_subcommand_completion_multiple(sc_app):
assert first_match is not None and sc_app.completion_matches == ['bar', 'foo', 'sport']
-def test_cmd2_help_subcommand_completion_nomatch(sc_app):
+def test_help_subcommand_completion_nomatch(sc_app):
text = 'z'
line = 'help base {}'.format(text)
endidx = len(line)
@@ -1115,7 +1146,7 @@ def scu_app():
return app
-def test_cmd2_subcmd_with_unknown_completion_single_end(scu_app):
+def test_subcmd_with_unknown_completion_single_end(scu_app):
text = 'f'
line = 'base {}'.format(text)
endidx = len(line)
@@ -1129,7 +1160,7 @@ def test_cmd2_subcmd_with_unknown_completion_single_end(scu_app):
assert first_match is not None and scu_app.completion_matches == ['foo ']
-def test_cmd2_subcmd_with_unknown_completion_multiple(scu_app):
+def test_subcmd_with_unknown_completion_multiple(scu_app):
text = ''
line = 'base {}'.format(text)
endidx = len(line)
@@ -1139,7 +1170,7 @@ def test_cmd2_subcmd_with_unknown_completion_multiple(scu_app):
assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport']
-def test_cmd2_subcmd_with_unknown_completion_nomatch(scu_app):
+def test_subcmd_with_unknown_completion_nomatch(scu_app):
text = 'z'
line = 'base {}'.format(text)
endidx = len(line)
@@ -1149,7 +1180,7 @@ def test_cmd2_subcmd_with_unknown_completion_nomatch(scu_app):
assert first_match is None
-def test_cmd2_help_subcommand_completion_single_scu(scu_app):
+def test_help_subcommand_completion_single_scu(scu_app):
text = 'base'
line = 'help {}'.format(text)
endidx = len(line)
@@ -1161,7 +1192,7 @@ def test_cmd2_help_subcommand_completion_single_scu(scu_app):
assert first_match is not None and scu_app.completion_matches == ['base ']
-def test_cmd2_help_subcommand_completion_multiple_scu(scu_app):
+def test_help_subcommand_completion_multiple_scu(scu_app):
text = ''
line = 'help base {}'.format(text)
endidx = len(line)
@@ -1170,7 +1201,7 @@ def test_cmd2_help_subcommand_completion_multiple_scu(scu_app):
first_match = complete_tester(text, line, begidx, endidx, scu_app)
assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport']
-def test_cmd2_help_subcommand_completion_with_flags_before_command(scu_app):
+def test_help_subcommand_completion_with_flags_before_command(scu_app):
text = ''
line = 'help -h -v base {}'.format(text)
endidx = len(line)
@@ -1189,7 +1220,7 @@ def test_complete_help_subcommands_with_blank_command(scu_app):
assert first_match is None and not scu_app.completion_matches
-def test_cmd2_help_subcommand_completion_nomatch_scu(scu_app):
+def test_help_subcommand_completion_nomatch_scu(scu_app):
text = 'z'
line = 'help base {}'.format(text)
endidx = len(line)
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 5739ad8e..64c95b30 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -16,7 +16,7 @@ import pytest
import cmd2
from .conftest import run_cmd, verify_help_text
from cmd2 import transcript
-from cmd2.utils import StdSim
+from cmd2.utils import StdSim, Settable
class CmdLineApp(cmd2.Cmd):
@@ -31,7 +31,7 @@ class CmdLineApp(cmd2.Cmd):
super().__init__(*args, multiline_commands=['orate'], **kwargs)
# Make maxrepeats settable at runtime
- self.settable['maxrepeats'] = 'Max number of `--repeat`s allowed'
+ self.add_settable(Settable('maxrepeats', int, 'Max number of `--repeat`s allowed'))
self.intro = 'This is an intro banner ...'
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 9dd54ee2..5030ce0e 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -531,3 +531,24 @@ def test_align_right_wide_fill_needs_padding():
width = 6
aligned = cu.align_right(text, fill_char=fill_char, width=width)
assert aligned == fill_char + ' ' + text
+
+
+def test_str_to_bool_true():
+ assert cu.str_to_bool('true')
+ assert cu.str_to_bool('True')
+ assert cu.str_to_bool('TRUE')
+ assert cu.str_to_bool('tRuE')
+
+def test_str_to_bool_false():
+ assert not cu.str_to_bool('false')
+ assert not cu.str_to_bool('False')
+ assert not cu.str_to_bool('FALSE')
+ assert not cu.str_to_bool('fAlSe')
+
+def test_str_to_bool_invalid():
+ with pytest.raises(ValueError):
+ cu.str_to_bool('other')
+
+def test_str_to_bool_bad_input():
+ with pytest.raises(ValueError):
+ cu.str_to_bool(1)
diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt
index 5bf9add3..5004adc5 100644
--- a/tests/transcripts/regex_set.txt
+++ b/tests/transcripts/regex_set.txt
@@ -4,10 +4,10 @@
# Regexes on prompts just make the trailing space obvious
(Cmd) set
-allow_style: /(Terminal|Always|Never)/
+allow_style: /'(Terminal|Always|Never)'/
debug: False
echo: False
-editor: /.*/
+editor: /'.*'/
feedback_to_output: False
max_completion_items: 50
maxrepeats: 3