From a5d3f7959c252ee23cf6360b81292d376b8c6fcc Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 4 Feb 2020 17:44:35 -0500 Subject: [PATCH 01/24] Updated set command to support tab completion of values --- cmd2/cmd2.py | 201 +++++++++++++++++--------------- cmd2/utils.py | 61 +++++++++- tests/conftest.py | 10 +- tests/test_cmd2.py | 33 ++---- tests/test_transcript.py | 4 +- tests/transcripts/regex_set.txt | 4 +- 6 files changed, 181 insertions(+), 132 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 34435ed0f..52971dddf 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -47,7 +47,7 @@ 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 @@ -198,22 +198,9 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *, # 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 +380,31 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *, # 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: utils.Settable) -> None: + """ + Convenience method to add a settable parameter to self.settables + :param settable: Settable object being added + """ + self.settables[settable.name] = settable + + def build_settables(self): + """Populates self.add_settable with parameters that can be edited via the set command""" + self.add_settable(utils.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(utils.Settable('debug', bool, "Show full error stack on error")) + self.add_settable(utils.Settable('echo', bool, "Echo command issued into output")) + self.add_settable(utils.Settable('editor', str, "Program used by `edit`")) + self.add_settable(utils.Settable('feedback_to_output', bool, "Include nonessentials in '|', '>' results")) + self.add_settable(utils.Settable('max_completion_items', int, + "Maximum number of CompletionItems to display during tab completion")) + self.add_settable(utils.Settable('quiet', bool, "Don't print nonessential feedback")) + self.add_settable(utils.Settable('timing', bool, "Report execution times")) + # ----- Methods related to presenting output to the user ----- @property @@ -411,8 +423,9 @@ def allow_style(self, new_val: str) -> None: elif new_val == ansi.STYLE_NEVER.lower(): ansi.allow_style = ansi.STYLE_NEVER 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 +510,7 @@ def pexcept(self, msg: Any, *, end: str = '\n', apply_style: bool = True) -> Non 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 +1464,7 @@ def _get_macro_completion_items(self) -> List[CompletionItem]: 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""" @@ -2262,7 +2275,6 @@ def _alias_list(self, args: argparse.Namespace) -> None: 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 +2289,7 @@ def _alias_list(self, args: argparse.Namespace) -> None: " 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') @@ -2594,8 +2606,7 @@ def do_help(self, args: argparse.Namespace) -> None: 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 +2809,95 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], 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 based on this settable + settable_parser = DEFAULT_ARGUMENT_PARSER() + settable_parser.add_argument(settable.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)) + tokens = [param] + arg_tokens['value'] + return completer.complete_command(tokens, text, line, begidx, endidx) 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') + + # 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='the new value for settable', + completer_method=complete_set_value, suppress_tab_hint=True) @with_argparser(set_parser) def do_set(self, args: argparse.Namespace) -> None: """Set a settable parameter or show current settings of parameters""" + if not self.settables: + self.poutput("There are no settable parameters") - # 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: + # 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 - # 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) + self.poutput('{} - was: {!r}\nnow: {!r}'.format(args.param, orig_value, new_value)) - self.poutput('{} - was: {}\nnow: {}'.format(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) + return - # 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 + # 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/utils.py b/cmd2/utils.py index 422488841..a23826c37 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 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,61 @@ 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 val.lower() == "true": + return True + elif val.lower() == "false": + return False + raise ValueError("must be true or false") + + +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, *, + choices: Iterable = None, + choices_function: Optional[Callable] = None, + choices_method: Optional[Callable] = None, + completer_function: Optional[Callable] = None, + completer_method: Optional[Callable] = None, + onchange_cb: Callable[[str, Any, Any], Any] = None): + """ + Settable Initializer + + :param name: name of the instance attribute being made settable + :param val_type: type or callable used to cast the string value from the command line + setting this to bool provides tab completion for true/false and validation using str_to_bool + :param description: string describing this setting + + 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 + :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 + """ + if val_type == bool: + val_type = str_to_bool + choices = ['true', 'false'] + + self.name = name + self.val_type = val_type + self.description = description + self.choices = choices + self.choices_function = choices_function + self.choices_method = choices_method + self.completer_function = completer_function + self.completer_method = completer_method + self.onchange_cb = onchange_cb + + def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]], default_values: collections_abc.Iterable = ()): """ @@ -372,7 +427,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) diff --git a/tests/conftest.py b/tests/conftest.py index b8abc4a53..51345881d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -88,10 +88,10 @@ def verify_help_text(cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]]) -> """ # 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 @@ def verify_help_text(cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]]) -> """ SHOW_LONG = """ -allow_style: Terminal # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never) +allow_style: 'Terminal' # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never) debug: False # Show full error stack on error 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 a3dbe1bef..8b14949c6 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,16 +108,6 @@ 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 @@ -175,16 +165,6 @@ def test_set_not_supported(base_app): """) 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'] @pytest.mark.parametrize('new_val, is_valid, expected', [ (ansi.STYLE_NEVER, False, ansi.STYLE_NEVER), @@ -214,10 +194,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 +652,7 @@ def test_base_debug(base_app): def test_debug_not_settable(base_app): # Set debug to False and make it unsettable base_app.debug = False - del base_app.settable['debug'] + del base_app.settables['debug'] # Cause an exception out, err = run_cmd(base_app, 'bad "quote') @@ -1583,8 +1564,8 @@ 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') diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 5739ad8e2..64c95b30c 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -16,7 +16,7 @@ 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 @@ def __init__(self, *args, **kwargs): 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/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt index 5bf9add33..5004adc58 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 From 457123d3a1376a2ab713f0ff638313b0eacfcf3e Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 4 Feb 2020 21:57:05 -0500 Subject: [PATCH 02/24] Updated CHANGELOG and made a few minor tweaks --- CHANGELOG.md | 4 ++++ cmd2/cmd2.py | 10 +++------- cmd2/utils.py | 42 +++++------------------------------------- tests/test_cmd2.py | 39 --------------------------------------- 4 files changed, 12 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 342aeaa0a..d9c1e774a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ * `continuation_prompt` * `self_in_py` * `prompt` + * `self.settable` changed to `self.settables` + * It is now a Dict[str, Settable] instead of Dict[str, str] + * **set** command now supports tab-completion of values + * Removed `cast()` utility function ## 0.9.25 (January 26, 2020) * Enhancements diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 52971dddf..ccd969912 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -415,13 +415,9 @@ def allow_style(self) -> str: @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: raise ValueError('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, diff --git a/cmd2/utils.py b/cmd2/utils.py index a23826c37..2f2efefa0 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -58,17 +58,17 @@ def strip_quotes(arg: str) -> str: def str_to_bool(val: str) -> bool: - """ - Converts a string to a boolean based on its value + """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 val.lower() == "true": + if val.capitalize() == str(True): return True - elif val.lower() == "false": + elif val.capitalize() == str(False): return False - raise ValueError("must be true or false") + raise ValueError("must be True or False") class Settable: @@ -143,38 +143,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 diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 8b14949c6..671a6685a 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -108,45 +108,6 @@ def test_base_show_long(base_app): 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): out, err = run_cmd(base_app, 'set quiet True') expected = normalize(""" From 14f456b0c3b70021fa650a596f81e0cfa7bd8949 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 4 Feb 2020 22:18:56 -0500 Subject: [PATCH 03/24] Fixed a bug in a very unusual case and added some unit tests --- cmd2/cmd2.py | 3 ++- cmd2/utils.py | 9 +++++---- tests/test_cmd2.py | 7 +++++++ tests/test_utils.py | 19 +++++++++++++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index ccd969912..563e328d9 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2845,7 +2845,8 @@ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int, def do_set(self, args: argparse.Namespace) -> None: """Set a settable parameter or show current settings of parameters""" if not self.settables: - self.poutput("There are no settable parameters") + self.pwarning("There are no settable parameters") + return if args.param: try: diff --git a/cmd2/utils.py b/cmd2/utils.py index 2f2efefa0..c200085a0 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -64,10 +64,11 @@ def str_to_bool(val: str) -> bool: :return: boolean value expressed in the string :raises: ValueError if the string does not contain a value corresponding to a boolean value """ - if val.capitalize() == str(True): - return True - elif val.capitalize() == str(False): - return False + 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") diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 671a6685a..9aaebc997 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -127,6 +127,13 @@ def test_set_not_supported(base_app): assert err == expected +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), ('neVeR', False, ansi.STYLE_NEVER), diff --git a/tests/test_utils.py b/tests/test_utils.py index 9dd54ee2b..804e58be9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -531,3 +531,22 @@ 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') + +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') + +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) From 61dfcf613966355d68c01f2aa06f0158b54c1f20 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 4 Feb 2020 22:34:29 -0500 Subject: [PATCH 04/24] Fixed doc build and updated README --- README.md | 3 ++- docs/api/utility_functions.rst | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cfb071cc2..7bec97349 100755 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ import argparse import random import sys import cmd2 +from cmd2.utils import Settable class CmdLineApp(cmd2.Cmd): """ Example cmd2 application. """ @@ -248,7 +249,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(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/docs/api/utility_functions.rst b/docs/api/utility_functions.rst index e2d6f0362..4f788e3d6 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 From 40722f10ace3107dcb4709008239ac8233ada30f Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 4 Feb 2020 23:01:30 -0500 Subject: [PATCH 05/24] Added cmd2.utils.Settable to the cmd2 namespace and updated examples and docs --- README.md | 3 +-- cmd2/__init__.py | 1 + docs/examples/first_app.rst | 4 ++-- docs/features/initialization.rst | 7 +++++-- docs/features/plugins.rst | 4 ++-- docs/features/settings.rst | 4 ++-- examples/cmd_as_argument.py | 2 +- examples/colors.py | 2 +- examples/decorator_example.py | 2 +- examples/environment.py | 6 +++--- examples/example.py | 2 +- examples/first_app.py | 2 +- examples/initialization.py | 4 +++- examples/pirate.py | 2 +- examples/plumbum_colors.py | 2 +- examples/remove_settable.py | 19 +++++++++++++++++++ 16 files changed, 45 insertions(+), 21 deletions(-) create mode 100755 examples/remove_settable.py diff --git a/README.md b/README.md index 7bec97349..19bd259b7 100755 --- a/README.md +++ b/README.md @@ -229,7 +229,6 @@ import argparse import random import sys import cmd2 -from cmd2.utils import Settable class CmdLineApp(cmd2.Cmd): """ Example cmd2 application. """ @@ -249,7 +248,7 @@ class CmdLineApp(cmd2.Cmd): super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts) # Make maxrepeats settable at runtime - self.add_settable(Settable('maxrepeats', int, '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 8fc5e9f25..cc5a0963d 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -27,3 +27,4 @@ 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/docs/examples/first_app.rst b/docs/examples/first_app.rst index 19d573b45..310c8d0cd 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/initialization.rst b/docs/features/initialization.rst index 46b4ecd24..d48290faf 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 caa46b8c5..00c0a9f04 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 55b6a10d8..6a8996e10 100644 --- a/docs/features/settings.rst +++ b/docs/features/settings.rst @@ -150,7 +150,7 @@ 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 @@ -165,4 +165,4 @@ the :ref:`features/settings:debug` setting. To do so, remove it from the def __init__(self): super().__init__() - self.settable.pop('debug') + self.settables.pop('debug') diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index 08643a509..b65bcbcb1 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -36,7 +36,7 @@ def __init__(self): 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 bbb3b2ad0..33b17e53d 100755 --- a/examples/colors.py +++ b/examples/colors.py @@ -39,7 +39,7 @@ def __init__(self): 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 4f68653ee..0f5374ceb 100755 --- a/examples/decorator_example.py +++ b/examples/decorator_example.py @@ -27,7 +27,7 @@ def __init__(self, ip_addr=None, port=None, transcript_files=None): 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 9e611f08c..fb90838c4 100755 --- a/examples/environment.py +++ b/examples/environment.py @@ -8,16 +8,16 @@ 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.add_settable(cmd2.Settable('degrees_c', int, 'Temperature in Celsius')) + 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: diff --git a/examples/example.py b/examples/example.py index b8f8202c9..0272a6e5d 100755 --- a/examples/example.py +++ b/examples/example.py @@ -32,7 +32,7 @@ def __init__(self): # 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 b5bd07e99..d8272e866 100755 --- a/examples/first_app.py +++ b/examples/first_app.py @@ -27,7 +27,7 @@ def __init__(self): # 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 32aa852f0..c13ed1371 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 @@ def __init__(self): 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 eda3994e1..acbab17ca 100755 --- a/examples/pirate.py +++ b/examples/pirate.py @@ -25,7 +25,7 @@ def __init__(self): 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 fe692805c..94815f50c 100755 --- a/examples/plumbum_colors.py +++ b/examples/plumbum_colors.py @@ -75,7 +75,7 @@ def __init__(self): 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 000000000..13a75e118 --- /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.settables.pop('debug') + + +if __name__ == '__main__': + import sys + c = MyApp() + sys.exit(c.cmdloop()) From ed83f7cfedfdcf8620e0feb9a8c6b98cd69dda16 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 4 Feb 2020 23:05:31 -0500 Subject: [PATCH 06/24] Updated example transcripts --- examples/transcripts/exampleSession.txt | 2 +- examples/transcripts/transcript_regex.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/transcripts/exampleSession.txt b/examples/transcripts/exampleSession.txt index 54419f919..8a60f4876 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 35fc5817c..ce2a2beb6 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: /.*?/ From d812fbc4d6dbd31c643caf4f8561f47ee8cb58aa Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 4 Feb 2020 23:36:49 -0500 Subject: [PATCH 07/24] Removed unnecessary inheritance from object --- cmd2/argparse_completer.py | 4 ++-- cmd2/py_bridge.py | 2 +- cmd2/utils.py | 10 +++++----- examples/table_display.py | 2 +- tests/conftest.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 23fd930e9..6513fe139 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/py_bridge.py b/cmd2/py_bridge.py index 0a1b6ee75..b7346d220 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -53,7 +53,7 @@ def __bool__(self) -> bool: 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 c200085a0..ee53e9242 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -388,7 +388,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. @@ -468,7 +468,7 @@ def __getattr__(self, item: str): return getattr(self.inner_stream, item) -class ByteBuf(object): +class ByteBuf: """ Used by StdSim to write binary data and stores the actual bytes written """ @@ -497,7 +497,7 @@ def write(self, b: bytes) -> None: 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. @@ -599,7 +599,7 @@ def _write_bytes(stream: Union[StdSim, TextIO], to_write: bytes) -> None: 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 @@ -623,7 +623,7 @@ def __exit__(self, *args) -> None: 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/examples/table_display.py b/examples/table_display.py index a8fd2cb0f..011435983 100755 --- a/examples/table_display.py +++ b/examples/table_display.py @@ -77,7 +77,7 @@ def two_dec(num: float) -> str: # ######## 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/tests/conftest.py b/tests/conftest.py index 51345881d..9e3e2798c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,7 +102,7 @@ def verify_help_text(cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]]) -> allow_style: 'Terminal' # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never) debug: False # Show full error stack on error echo: False # Echo command issued into output -editor: 'vim' # Program used by `edit` +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 From 4657baa622a88291292f532ce36d3508b3ea0048 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 4 Feb 2020 23:42:15 -0500 Subject: [PATCH 08/24] To support completer/choices functions that receive arg_tokens, changed complete_set_value() to pass all tokens into its AutoCompleter --- cmd2/cmd2.py | 60 +++++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 563e328d9..1ce8f3a41 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -53,6 +53,7 @@ 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 @@ -380,7 +381,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *, # 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: utils.Settable) -> None: + def add_settable(self, settable: Settable) -> None: """ Convenience method to add a settable parameter to self.settables :param settable: Settable object being added @@ -389,21 +390,21 @@ def add_settable(self, settable: utils.Settable) -> None: def build_settables(self): """Populates self.add_settable with parameters that can be edited via the set command""" - self.add_settable(utils.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(utils.Settable('debug', bool, "Show full error stack on error")) - self.add_settable(utils.Settable('echo', bool, "Echo command issued into output")) - self.add_settable(utils.Settable('editor', str, "Program used by `edit`")) - self.add_settable(utils.Settable('feedback_to_output', bool, "Include nonessentials in '|', '>' results")) - self.add_settable(utils.Settable('max_completion_items', int, - "Maximum number of CompletionItems to display during tab completion")) - self.add_settable(utils.Settable('quiet', bool, "Don't print nonessential feedback")) - self.add_settable(utils.Settable('timing', bool, "Report execution times")) + 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 error stack on error")) + 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 ----- @@ -2814,9 +2815,9 @@ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int, except KeyError: raise CompletionError(param + " is not a settable parameter") - # Create a parser based on this settable - settable_parser = DEFAULT_ARGUMENT_PARSER() - settable_parser.add_argument(settable.name, help=settable.description, + # Create a parser with a value field based on this settable + settable_parser = DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent]) + settable_parser.add_argument('value', help=settable.description, choices=settable.choices, choices_function=settable.choices_function, choices_method=settable.choices_method, @@ -2826,19 +2827,26 @@ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int, from .argparse_completer import AutoCompleter completer = AutoCompleter(settable_parser, self) - tokens = [param] + arg_tokens['value'] + tokens, _ = self.tokens_for_completion(line, begidx, endidx) return completer.complete_command(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" - "Call without arguments for a list of settable parameters with their values.") - set_parser = DEFAULT_ARGUMENT_PARSER(description=set_description) - 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') + "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='the new value for settable', + set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='new value for settable', completer_method=complete_set_value, suppress_tab_hint=True) @with_argparser(set_parser) From fee77c23579133807d7b275a57a3816259a93f0d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 4 Feb 2020 23:47:58 -0500 Subject: [PATCH 09/24] Fixed missing parenthesis --- cmd2/cmd2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 1ce8f3a41..d6870d06b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -399,7 +399,7 @@ def build_settables(self): self.add_settable(Settable('debug', bool, "Show full error stack on error")) 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('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")) From 80adeb66420d45b7f22320a5de3e32d1b57b4e1b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 00:12:45 -0500 Subject: [PATCH 10/24] Added unit tests --- tests/test_completion.py | 51 ++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/tests/test_completion.py b/tests/test_completion.py index 475b44dd5..99f832a47 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 @@ def do_test_no_completer(self, args): """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) From ce7cbe22916eef1693c0a9d0b0c43297d53f9d3b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 09:35:44 -0500 Subject: [PATCH 11/24] Added support to do_set() for setting a parameter to an empty string as well as something resembling an argparse flag (e.g. -h) --- cmd2/cmd2.py | 10 +++++++--- tests/test_cmd2.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d6870d06b..37779f569 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2827,8 +2827,9 @@ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int, from .argparse_completer import AutoCompleter completer = AutoCompleter(settable_parser, self) - tokens, _ = self.tokens_for_completion(line, begidx, endidx) - return completer.complete_command(tokens, text, line, begidx, endidx) + # 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. @@ -2849,7 +2850,8 @@ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int, set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='new value for settable', completer_method=complete_set_value, suppress_tab_hint=True) - @with_argparser(set_parser) + # 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: @@ -2864,6 +2866,8 @@ def do_set(self, args: argparse.Namespace) -> None: 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) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 9aaebc997..0615ed46d 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -108,7 +108,7 @@ def test_base_show_long(base_app): assert out == expected -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 @@ -119,6 +119,16 @@ def test_base_set(base_app): 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(""" From 34f00eda97d44922896beac608a9d4a085ae6e0d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 09:41:47 -0500 Subject: [PATCH 12/24] Updated documentation --- cmd2/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd2/utils.py b/cmd2/utils.py index ee53e9242..728c1ac38 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -85,8 +85,10 @@ def __init__(self, name: str, val_type: Callable, description: str, *, Settable Initializer :param name: name of the instance attribute being made settable - :param val_type: type or callable used to cast the string value from the command line - setting this to bool provides tab completion for true/false and validation using str_to_bool + :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 The following optional settings provide tab completion for a parameter's values. They correspond to the From 7519f742923a31599c56dfc5db9aad6901bfce73 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 11:38:06 -0500 Subject: [PATCH 13/24] Added remove_settable() since cmd2 has add_settable() Documented Settable.onchange_cb --- cmd2/cmd2.py | 11 +++++++++++ cmd2/utils.py | 11 +++++++---- tests/test_cmd2.py | 6 +++++- tests/test_utils.py | 2 ++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 37779f569..7373188b1 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -388,6 +388,17 @@ def add_settable(self, settable: Settable) -> None: """ 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, diff --git a/cmd2/utils.py b/cmd2/utils.py index 728c1ac38..9635be42a 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -69,18 +69,18 @@ def str_to_bool(val: str) -> bool: return True elif val.capitalize() == str(False): return False - raise ValueError("must be True or 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, - onchange_cb: Callable[[str, Any, Any], Any] = None): + completer_method: Optional[Callable] = None): """ Settable Initializer @@ -90,6 +90,9 @@ def __init__(self, name: str, val_type: Callable, description: str, *, 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 to call when the value of this settable is altered by the set command + Callback signature: + val_changed_cb(param_name: str, old_value: Any, new_value: Any) -> Any 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. @@ -107,12 +110,12 @@ def __init__(self, name: str, val_type: Callable, description: str, *, 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 - self.onchange_cb = onchange_cb def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]], diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 0615ed46d..fecab628f 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -630,7 +630,7 @@ def test_base_debug(base_app): def test_debug_not_settable(base_app): # Set debug to False and make it unsettable base_app.debug = False - del base_app.settables['debug'] + base_app.remove_settable('debug') # Cause an exception out, err = run_cmd(base_app, 'bad "quote') @@ -638,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' diff --git a/tests/test_utils.py b/tests/test_utils.py index 804e58be9..5030ce0e8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -537,11 +537,13 @@ 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): From f3285c801791b46acb4a3ddd0ef372316f314151 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 17:06:15 -0500 Subject: [PATCH 14/24] Updated help text --- cmd2/cmd2.py | 2 +- docs/features/builtin_commands.rst | 6 +++--- tests/conftest.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 7373188b1..5579be4b2 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -408,7 +408,7 @@ def build_settables(self): ansi.STYLE_NEVER), choices=[ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER])) - self.add_settable(Settable('debug', bool, "Show full error stack on error")) + 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")) diff --git a/docs/features/builtin_commands.rst b/docs/features/builtin_commands.rst index 025149b39..83d3176d8 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/tests/conftest.py b/tests/conftest.py index 9e3e2798c..7f77a2075 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,7 +100,7 @@ def verify_help_text(cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]]) -> 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 +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 From f254f4a9acd5e946892e86315e8313b3181dbb6e Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 20:55:07 -0500 Subject: [PATCH 15/24] Updated documentation --- docs/features/settings.rst | 2 +- examples/remove_settable.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/settings.rst b/docs/features/settings.rst index 6a8996e10..23c7686da 100644 --- a/docs/features/settings.rst +++ b/docs/features/settings.rst @@ -165,4 +165,4 @@ the :ref:`features/settings:debug` setting. To do so, remove it from the def __init__(self): super().__init__() - self.settables.pop('debug') + self.remove_settable('debug') diff --git a/examples/remove_settable.py b/examples/remove_settable.py index 13a75e118..6a2e40622 100755 --- a/examples/remove_settable.py +++ b/examples/remove_settable.py @@ -10,7 +10,7 @@ class MyApp(cmd2.Cmd): def __init__(self): super().__init__() - self.settables.pop('debug') + self.remove_settable('debug') if __name__ == '__main__': From 9b95f50975ecaa8dbfd37c375ebf38e1971fc960 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 21:00:51 -0500 Subject: [PATCH 16/24] Added more to onchange_cb documentation --- cmd2/utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd2/utils.py b/cmd2/utils.py index 9635be42a..2bb91ccdf 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -90,9 +90,13 @@ def __init__(self, name: str, val_type: Callable, description: str, *, 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 to call when the value of this settable is altered by the set command - Callback signature: - val_changed_cb(param_name: str, old_value: Any, new_value: Any) -> Any + :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. From de12ee4782799350233be7adadeac08907c13e84 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 21:18:47 -0500 Subject: [PATCH 17/24] Added more to Settable docstring --- cmd2/utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cmd2/utils.py b/cmd2/utils.py index 2bb91ccdf..cfe75f535 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -103,9 +103,16 @@ def __init__(self, name: str, val_type: Callable, description: str, *, :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 + :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 + :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 From 5997d96dd2e17a9e84af8a515313a02e546c27e0 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 21:27:36 -0500 Subject: [PATCH 18/24] Fixing tab-completion hints when completing the value field of set --- cmd2/cmd2.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 5579be4b2..9f304537b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2828,7 +2828,11 @@ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int, # Create a parser with a value field based on this settable settable_parser = DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent]) - settable_parser.add_argument('value', help=settable.description, + + # Settables that populate choices have the values of those choices overwrite 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, From 2fd85015d9f08a539dea1f8c63c1f1e2da54ab2d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 22:15:11 -0500 Subject: [PATCH 19/24] No longer displaying list of subcommands in usage text of alias and macro --- cmd2/cmd2.py | 4 ++-- tests/test_cmd2.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 9f304537b..65caa0dfd 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2279,7 +2279,7 @@ def _alias_list(self, args: argparse.Namespace) -> None: 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 @@ -2455,7 +2455,7 @@ def _macro_list(self, args: argparse.Namespace) -> None: 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 diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index fecab628f..0b4c60d6d 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1552,7 +1552,7 @@ def test_get_settable_completion_items(base_app): 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 @@ -1646,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 From 07e895d65fcbf35d2a9ad5c9ebffef6a067537a7 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 22:18:51 -0500 Subject: [PATCH 20/24] Updated change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c1e774a..5117f1ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.9.26 (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: From 58500cb0b86e8046b49aac5d554a6fa9630c0cd3 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 5 Feb 2020 22:32:21 -0500 Subject: [PATCH 21/24] Updated documentation --- cmd2/cmd2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 65caa0dfd..2c35a1638 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2829,7 +2829,7 @@ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int, # Create a parser with a value field based on this settable settable_parser = DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent]) - # Settables that populate choices have the values of those choices overwrite the arg name + # 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, From 7476c27e95b6a9c1ee1c93893d678012c019cf7c Mon Sep 17 00:00:00 2001 From: kotfu Date: Wed, 5 Feb 2020 20:56:29 -0700 Subject: [PATCH 22/24] Documentation updates --- CHANGELOG.md | 20 +++++++++++--------- docs/api/utility_classes.rst | 4 ++++ docs/conf.py | 3 --- docs/features/settings.rst | 25 ++++++++++++++++--------- examples/environment.py | 10 +++++++--- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5117f1ede..77ba6b009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 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 @@ -10,9 +10,11 @@ * `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) @@ -74,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 @@ -143,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 @@ -153,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` @@ -193,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 @@ -203,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` @@ -222,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. @@ -243,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/docs/api/utility_classes.rst b/docs/api/utility_classes.rst index 7ed0c584c..2ee92ced5 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/conf.py b/docs/conf.py index 7a8da9d1d..16e8ecb70 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,9 +25,6 @@ # 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/features/settings.rst b/docs/features/settings.rst index 23c7686da..40b9bc357 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 @@ -153,13 +159,14 @@ 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): diff --git a/examples/environment.py b/examples/environment.py index fb90838c4..a24734f1e 100755 --- a/examples/environment.py +++ b/examples/environment.py @@ -8,12 +8,16 @@ class EnvironmentApp(cmd2.Cmd): """ Example cmd2 application. """ - degrees_c = 22 - sunny = False def __init__(self): super().__init__() - self.add_settable(cmd2.Settable('degrees_c', int, 'Temperature in Celsius')) + 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): From 00bb9b0c08c7b7a791a3278a6b1003d7cad471bd Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Wed, 5 Feb 2020 23:19:35 -0500 Subject: [PATCH 23/24] Fix flake8 errors in doc config --- docs/conf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 16e8ecb70..02eb827f6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,9 +17,6 @@ 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 From c4893ea8a132c06bc71b0ddd63801604e6f85177 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 6 Feb 2020 17:57:09 -0500 Subject: [PATCH 24/24] Fixed example code --- examples/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/environment.py b/examples/environment.py index a24734f1e..670b63ac6 100755 --- a/examples/environment.py +++ b/examples/environment.py @@ -30,7 +30,7 @@ def do_sunbathe(self, arg): 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