diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-09-23 20:57:26 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-09-23 20:57:26 -0400 |
commit | b72edb07d59d45d4006dc596218a52d54396378a (patch) | |
tree | 9875db04a91bbcef2815a32113268ffeed7e39cf /cmd2 | |
parent | 27a0adbe53c7cdeda240c325a6f59b7cca0e7bfd (diff) | |
parent | 88db3b44fb0b28b01b73376e1d2822d6c5995856 (diff) | |
download | cmd2-git-b72edb07d59d45d4006dc596218a52d54396378a.tar.gz |
Merge pull request #781 from python-cmd2/CompletionError
CompletionError class
Diffstat (limited to 'cmd2')
-rw-r--r-- | cmd2/__init__.py | 2 | ||||
-rw-r--r-- | cmd2/argparse_completer.py | 92 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 20 | ||||
-rwxr-xr-x | cmd2/cmd2.py | 4 |
4 files changed, 84 insertions, 34 deletions
diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 2653051a..c496a4f7 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -11,7 +11,7 @@ except DistributionNotFound: pass from .ansi import style -from .argparse_custom import Cmd2ArgumentParser, CompletionItem +from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem from .cmd2 import Cmd, Statement, EmptyStatement, categorize from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category from .constants import DEFAULT_SHORTCUTS diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index fb485348..a866b3ff 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -15,8 +15,9 @@ from typing import Dict, List, Optional, Union from . import cmd2 from . import utils from .ansi import ansi_safe_wcswidth, style_error +from .argparse_custom import ATTR_CHOICES_CALLABLE, INFINITY, generate_range_error from .argparse_custom import ATTR_SUPPRESS_TAB_HINT, ATTR_DESCRIPTIVE_COMPLETION_HEADER, ATTR_NARGS_RANGE -from .argparse_custom import ChoicesCallable, CompletionItem, ATTR_CHOICES_CALLABLE, INFINITY, generate_range_error +from .argparse_custom import ChoicesCallable, CompletionError, CompletionItem from .rl_utils import rl_force_redisplay # If no descriptive header is supplied, then this will be used instead @@ -319,8 +320,12 @@ class AutoCompleter(object): # Check if we are completing a flag's argument if flag_arg_state is not None: - completion_results = self._complete_for_arg(flag_arg_state.action, text, line, - begidx, endidx, consumed_arg_values) + try: + completion_results = self._complete_for_arg(flag_arg_state.action, text, line, + begidx, endidx, consumed_arg_values) + except CompletionError as ex: + self._print_completion_error(flag_arg_state.action, ex) + return [] # If we have results, then return them if completion_results: @@ -341,8 +346,12 @@ class AutoCompleter(object): action = self._positional_actions[pos_index] pos_arg_state = AutoCompleter._ArgumentState(action) - completion_results = self._complete_for_arg(pos_arg_state.action, text, line, - begidx, endidx, consumed_arg_values) + try: + completion_results = self._complete_for_arg(pos_arg_state.action, text, line, + begidx, endidx, consumed_arg_values) + except CompletionError as ex: + self._print_completion_error(pos_arg_state.action, ex) + return [] # If we have results, then return them if completion_results: @@ -456,7 +465,11 @@ class AutoCompleter(object): def _complete_for_arg(self, arg_action: argparse.Action, text: str, line: str, begidx: int, endidx: int, consumed_arg_values: Dict[str, List[str]]) -> List[str]: - """Tab completion routine for an argparse argument""" + """ + Tab completion routine for an argparse argument + :return: list of completions + :raises CompletionError if the completer or choices function this calls raises one + """ # Check if the arg provides choices to the user if arg_action.choices is not None: arg_choices = arg_action.choices @@ -520,24 +533,35 @@ class AutoCompleter(object): return self._format_completions(arg_action, results) @staticmethod - def _print_arg_hint(arg_action: argparse.Action) -> None: - """Print argument hint to the terminal when tab completion results in no results""" - - # Check if hinting is disabled - suppress_hint = getattr(arg_action, ATTR_SUPPRESS_TAB_HINT, False) - if suppress_hint or arg_action.help == argparse.SUPPRESS or arg_action.dest == argparse.SUPPRESS: - return - + def _format_message_prefix(arg_action: argparse.Action) -> str: + """Format the arg prefix text that appears before messages printed to the user""" # Check if this is a flag if arg_action.option_strings: flags = ', '.join(arg_action.option_strings) param = ' ' + str(arg_action.dest).upper() - prefix = '{}{}'.format(flags, param) + return '{}{}'.format(flags, param) # Otherwise this is a positional else: - prefix = '{}'.format(str(arg_action.dest).upper()) + return '{}'.format(str(arg_action.dest).upper()) + + @staticmethod + def _print_message(msg: str) -> None: + """Print a message instead of tab completions and redraw the prompt and input line""" + print(msg) + rl_force_redisplay() + + def _print_arg_hint(self, arg_action: argparse.Action) -> None: + """ + Print argument hint to the terminal when tab completion results in no results + :param arg_action: action being tab completed + """ + # Check if hinting is disabled + suppress_hint = getattr(arg_action, ATTR_SUPPRESS_TAB_HINT, False) + if suppress_hint or arg_action.help == argparse.SUPPRESS or arg_action.dest == argparse.SUPPRESS: + return + prefix = self._format_message_prefix(arg_action) prefix = ' {0: <{width}} '.format(prefix, width=20) pref_len = len(prefix) @@ -545,28 +569,36 @@ class AutoCompleter(object): help_lines = help_text.splitlines() if len(help_lines) == 1: - print('\nHint:\n{}{}\n'.format(prefix, help_lines[0])) + self._print_message('\nHint:\n{}{}\n'.format(prefix, help_lines[0])) else: out_str = '\n{}'.format(prefix) out_str += '\n{0: <{width}}'.format('', width=pref_len).join(help_lines) - print('\nHint:' + out_str + '\n') + self._print_message('\nHint:' + out_str + '\n') - # Redraw prompt and input line - rl_force_redisplay() - - @staticmethod - def _print_unfinished_flag_error(flag_arg_state: _ArgumentState) -> None: - """Print an error during tab completion when the user has not finished the current flag""" - flags = ', '.join(flag_arg_state.action.option_strings) - param = ' ' + str(flag_arg_state.action.dest).upper() - prefix = '{}{}'.format(flags, param) + def _print_unfinished_flag_error(self, flag_arg_state: _ArgumentState) -> None: + """ + Print an error during tab completion when the user has not finished the current flag + :param flag_arg_state: information about the unfinished flag action + """ + prefix = self._format_message_prefix(flag_arg_state.action) out_str = "\nError:\n" out_str += ' {0: <{width}} '.format(prefix, width=20) out_str += generate_range_error(flag_arg_state.min, flag_arg_state.max) out_str += ' ({} entered)'.format(flag_arg_state.count) - print(style_error('{}\n'.format(out_str))) + self._print_message(style_error('{}\n'.format(out_str))) - # Redraw prompt and input line - rl_force_redisplay() + def _print_completion_error(self, arg_action: argparse.Action, completion_error: CompletionError) -> None: + """ + Print a CompletionError to the user + :param arg_action: action being tab completed + :param completion_error: error that occurred + """ + prefix = self._format_message_prefix(arg_action) + + out_str = "\nError:\n" + out_str += ' {0: <{width}} '.format(prefix, width=20) + out_str += str(completion_error) + + self._print_message(style_error('{}\n'.format(out_str))) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 940d6064..f7dbc8a3 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -94,7 +94,7 @@ Tab Completion: as dynamic. Therefore it is up to the developer to validate if the user has typed an acceptable value for these arguments. - The following functions exist in cases where you may want to manually add choice providing function/methods to + The following functions exist in cases where you may want to manually a add choice-providing function/method to an existing argparse action. For instance, in __init__() of a custom action class. set_choices_function(action, func) @@ -116,6 +116,13 @@ Tab Completion: their values. All tokens are stored in the dictionary as the raw strings provided on the command line. It is up to the developer to determine if the user entered the correct argument type (e.g. int) and validate their values. +CompletionError Class: + Raised during tab-completion operations to report any sort of error you want printed by the AutoCompleter + + Example use cases + - Reading a database to retrieve a tab completion data set failed + - A previous command line argument that determines the data set being completed is invalid + CompletionItem Class: This class was added to help in cases where uninformative data is being tab completed. For instance, tab completing ID numbers isn't very helpful to a user without context. Returning a list of CompletionItems @@ -221,6 +228,17 @@ def generate_range_error(range_min: int, range_max: Union[int, float]) -> str: return err_str +class CompletionError(Exception): + """ + Raised during tab-completion operations to report any sort of error you want printed by the AutoCompleter + + Example use cases + - Reading a database to retrieve a tab completion data set failed + - A previous command line argument that determines the data set being completed is invalid + """ + pass + + class CompletionItem(str): """ Completion item with descriptive text attached diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 87f8684d..6bfcfdc8 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -341,7 +341,7 @@ class Cmd(cmd.Cmd): def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *, persistent_history_file: str = '', persistent_history_length: int = 1000, - startup_script: Optional[str] = None, use_ipython: bool = False, + startup_script: str = '', use_ipython: bool = False, allow_cli_args: bool = True, transcript_files: Optional[List[str]] = None, allow_redirection: bool = True, multiline_commands: Optional[List[str]] = None, terminators: Optional[List[str]] = None, shortcuts: Optional[Dict[str, str]] = None) -> None: @@ -499,7 +499,7 @@ class Cmd(cmd.Cmd): self._startup_commands = [] # If a startup script is provided, then execute it in the startup commands - if startup_script is not None: + if startup_script: startup_script = os.path.abspath(os.path.expanduser(startup_script)) if os.path.exists(startup_script) and os.path.getsize(startup_script) > 0: self._startup_commands.append("run_script '{}'".format(startup_script)) |