diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-07-04 11:11:31 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-07-04 11:11:31 -0400 |
commit | 8f1bc02f2028ac869e61c9a88475a933046f4ee8 (patch) | |
tree | 1032bdabd7a26c5596878cc3c48a4e5738db9033 /cmd2 | |
parent | c9d5fc35166b4f6dcdb46fcb1255a013b3660f4a (diff) | |
download | cmd2-git-8f1bc02f2028ac869e61c9a88475a933046f4ee8.tar.gz |
No longer restricting nargs range support to Cmd2ArgParser
Diffstat (limited to 'cmd2')
-rw-r--r-- | cmd2/argparse_completer.py | 128 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 273 |
2 files changed, 179 insertions, 222 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 74853fa8..c5a4c004 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -61,115 +61,14 @@ How to supply completion choice lists or functions for sub-commands: import argparse import os from argparse import SUPPRESS -from typing import Any, Callable, Iterable, Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, List, Tuple, Union from . import utils from .ansi import ansi_safe_wcswidth -from .argparse_custom import _RangeAction +from .argparse_custom import ATTR_SUPPRESS_TAB_HINT, ATTR_DESCRIPTIVE_COMPLETION_HEADER, ATTR_NARGS_RANGE +from .argparse_custom import ChoicesCallable, ATTR_CHOICES_CALLABLE from .rl_utils import rl_force_redisplay -# Argparse argument attribute that stores an ArgChoicesCallable -ARG_CHOICES_CALLABLE = 'choices_callable' - -# Argparse argument attribute that suppresses tab-completion hints -ARG_SUPPRESS_HINT = 'suppress_hint' - -# Argparse argument attribute that prints descriptive header when using CompletionItems -ARG_DESCRIPTIVE_COMPLETION_HEADER = 'desc_header' - - -class ArgChoicesCallable: - """ - Enables using a callable as the choices provider for an argparse argument. - While argparse has the built-in choices attribute, it is limited to an iterable. - """ - def __init__(self, is_method: bool, is_completer: bool, to_call: Callable): - """ - Initializer - :param is_method: True if to_call is an instance method of a cmd2 app. False if it is a function. - :param is_completer: True if to_call is a tab completion routine which expects - the args: text, line, begidx, endidx - :param to_call: the callable object that will be called to provide choices for the argument - """ - self.is_method = is_method - self.is_completer = is_completer - self.to_call = to_call - - -# Save original _ActionsContainer.add_argument's value because we will replace it with our wrapper -orig_actions_container_add_argument = argparse._ActionsContainer.add_argument - - -def add_argument_wrapper(self, *args, - choices_function: Optional[Callable[[], Iterable[Any]]] = None, - choices_method: Optional[Callable[[Any], Iterable[Any]]] = None, - completer_function: Optional[Callable[[str, str, int, int], List[str]]] = None, - completer_method: Optional[Callable[[Any, str, str, int, int], List[str]]] = None, - suppress_hint: bool = False, - description_header: Optional[str] = None, - **kwargs) -> argparse.Action: - """ - This is a wrapper around _ActionsContainer.add_argument() that supports more settings needed by AutoCompleter - - # Args from original function - :param self: instance of the _ActionsContainer being added to - :param args: arguments expected by argparse._ActionsContainer.add_argument - - # Added args used by AutoCompleter - :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 - :param suppress_hint: when AutoCompleter has no choices to show during tab completion, it displays the current - argument's help text as a hint. Set this to True to suppress the hint. Defaults to False. - :param description_header: if the provided choices are CompletionItems, then this header will display - during tab completion. Defaults to None. - - # Args from original function - :param kwargs: keyword-arguments recognized by argparse._ActionsContainer.add_argument - - Note: You can only use 1 of the following in your argument: - choices, choices_function, choices_method, completer_function, completer_method - - See the header of this file for more information - - :return: the created argument action - """ - # Call the original add_argument function - new_arg = orig_actions_container_add_argument(self, *args, **kwargs) - - # Verify consistent use of arguments - choice_params = [new_arg.choices, choices_function, choices_method, completer_function, completer_method] - num_set = len(choice_params) - choice_params.count(None) - - if num_set > 1: - err_msg = ("Only one of the following may be used in an argparser argument at a time:\n" - "choices, choices_function, choices_method, completer_function, completer_method") - raise (ValueError(err_msg)) - - # Set the custom attributes used by AutoCompleter - if choices_function: - setattr(new_arg, ARG_CHOICES_CALLABLE, - ArgChoicesCallable(is_method=False, is_completer=False, to_call=choices_function)) - elif choices_method: - setattr(new_arg, ARG_CHOICES_CALLABLE, - ArgChoicesCallable(is_method=True, is_completer=False, to_call=choices_method)) - elif completer_function: - setattr(new_arg, ARG_CHOICES_CALLABLE, - ArgChoicesCallable(is_method=False, is_completer=True, to_call=completer_function)) - elif completer_method: - setattr(new_arg, ARG_CHOICES_CALLABLE, - ArgChoicesCallable(is_method=True, is_completer=True, to_call=completer_method)) - - setattr(new_arg, ARG_SUPPRESS_HINT, suppress_hint) - setattr(new_arg, ARG_DESCRIPTIVE_COMPLETION_HEADER, description_header) - - return new_arg - - -# Overwrite _ActionsContainer.add_argument with our wrapper -argparse._ActionsContainer.add_argument = add_argument_wrapper - class CompletionItem(str): """ @@ -301,8 +200,8 @@ class AutoCompleter(object): self._arg_choices[action.dest] = action.choices # otherwise check if a callable provides the choices for this argument - elif hasattr(action, ARG_CHOICES_CALLABLE): - arg_choice_callable = getattr(action, ARG_CHOICES_CALLABLE) + elif hasattr(action, ATTR_CHOICES_CALLABLE): + arg_choice_callable = getattr(action, ATTR_CHOICES_CALLABLE) self._arg_choices[action.dest] = arg_choice_callable # if the parameter is flag based, it will have option_strings @@ -411,9 +310,10 @@ class AutoCompleter(object): def process_action_nargs(action: argparse.Action, arg_state: AutoCompleter._ArgumentState) -> None: """Process the current argparse Action and initialize the ArgumentState object used to track what arguments we have processed for this action""" - if isinstance(action, _RangeAction): - arg_state.min = action.nargs_min - arg_state.max = action.nargs_max + nargs_range = getattr(action, ATTR_NARGS_RANGE, None) + if nargs_range is not None: + arg_state.min = nargs_range[0] + arg_state.max = nargs_range[1] arg_state.variable = True if arg_state.min is None or arg_state.max is None: if action.nargs is None: @@ -624,7 +524,7 @@ class AutoCompleter(object): completions_with_desc.append(entry) try: - desc_header = getattr(action, ARG_DESCRIPTIVE_COMPLETION_HEADER) + desc_header = getattr(action, ATTR_DESCRIPTIVE_COMPLETION_HEADER) except AttributeError: desc_header = 'Description' header = '\n{: <{token_width}}{}'.format(action.dest.upper(), desc_header, token_width=token_width + 2) @@ -669,7 +569,7 @@ class AutoCompleter(object): arg_choices = self._arg_choices[arg.dest] # Check if the argument uses a specific tab completion function to provide its choices - if isinstance(arg_choices, ArgChoicesCallable) and arg_choices.is_completer: + if isinstance(arg_choices, ChoicesCallable) and arg_choices.is_completer: if arg_choices.is_method: return arg_choices.to_call(self._cmd2_app, text, line, begidx, endidx) else: @@ -687,8 +587,8 @@ class AutoCompleter(object): if arg.dest in self._arg_choices: arg_choices = self._arg_choices[arg.dest] - # Check if arg_choices is an ArgChoicesCallable that generates a choice list - if isinstance(arg_choices, ArgChoicesCallable): + # Check if arg_choices is a ChoicesCallable that generates a choice list + if isinstance(arg_choices, ChoicesCallable): if arg_choices.is_completer: # Tab completion routines are handled in other functions return [] @@ -717,7 +617,7 @@ class AutoCompleter(object): return # is parameter hinting disabled for this parameter? - suppress_hint = getattr(action, ARG_SUPPRESS_HINT, False) + suppress_hint = getattr(action, ATTR_SUPPRESS_TAB_HINT, False) if suppress_hint: return diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 965b0bf0..475105ec 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -2,112 +2,165 @@ import argparse import re as _re import sys - +# noinspection PyUnresolvedReferences,PyProtectedMember from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _ -from typing import Callable, Tuple, Union +from typing import Any, Callable, Iterable, List, Optional, Tuple, Union from .ansi import ansi_aware_write, style_error - -class _RangeAction(object): - def __init__(self, nargs: Union[int, str, Tuple[int, int], None]) -> None: - self.nargs_min = None - self.nargs_max = None - - # pre-process special ranged nargs - if isinstance(nargs, tuple): - if len(nargs) != 2 or not isinstance(nargs[0], int) or not isinstance(nargs[1], int): - raise ValueError('Ranged values for nargs must be a tuple of 2 integers') - if nargs[0] >= nargs[1]: - raise ValueError('Invalid nargs range. The first value must be less than the second') - if nargs[0] < 0: - raise ValueError('Negative numbers are invalid for nargs range.') - narg_range = nargs - self.nargs_min = nargs[0] - self.nargs_max = nargs[1] - if narg_range[0] == 0: - if narg_range[1] > 1: - self.nargs_adjusted = '*' - else: - # this shouldn't use a range tuple, but yet here we are - self.nargs_adjusted = '?' +############################################################################################################ +# The following are names of custom argparse argument attributes added by cmd2 +############################################################################################################ + +# A tuple specifying nargs as a range (min, max) +ATTR_NARGS_RANGE = 'nargs_range' + +# ChoicesCallable object that specifies the function to be called which provides choices to the argument +ATTR_CHOICES_CALLABLE = 'choices_callable' + +# Pressing tab normally displays the help text for the argument if no choices are available +# Setting this attribute to True will suppress these hints +ATTR_SUPPRESS_TAB_HINT = 'suppress_tab_hint' + +# Descriptive header that prints when using CompletionItems +ATTR_DESCRIPTIVE_COMPLETION_HEADER = 'desc_completion_header' + + +class ChoicesCallable: + """ + Enables using a callable as the choices provider for an argparse argument. + While argparse has the built-in choices attribute, it is limited to an iterable. + """ + def __init__(self, is_method: bool, is_completer: bool, to_call: Callable): + """ + Initializer + :param is_method: True if to_call is an instance method of a cmd2 app. False if it is a function. + :param is_completer: True if to_call is a tab completion routine which expects + the args: text, line, begidx, endidx + :param to_call: the callable object that will be called to provide choices for the argument + """ + self.is_method = is_method + self.is_completer = is_completer + self.to_call = to_call + + +# Save original _ActionsContainer.add_argument's value because we will replace it with our wrapper +# noinspection PyProtectedMember +orig_actions_container_add_argument = argparse._ActionsContainer.add_argument + + +def add_argument_wrapper(self, *args, + nargs: Union[int, str, Tuple[int, int], None] = None, + choices_function: Optional[Callable[[], Iterable[Any]]] = None, + choices_method: Optional[Callable[[Any], Iterable[Any]]] = None, + completer_function: Optional[Callable[[str, str, int, int], List[str]]] = None, + completer_method: Optional[Callable[[Any, str, str, int, int], List[str]]] = None, + suppress_hint: bool = False, + descriptive_header: Optional[str] = None, + **kwargs) -> argparse.Action: + """ + This is a wrapper around _ActionsContainer.add_argument() which supports more settings used by cmd2 + + # Args from original function + :param self: instance of the _ActionsContainer being added to + :param args: arguments expected by argparse._ActionsContainer.add_argument + + # Customized arguments from original function + :param nargs: extends argparse nargs attribute by allowing tuples which specify a range (min, max) + + # Added args used by AutoCompleter + :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 + :param suppress_hint: when AutoCompleter has no choices to show during tab completion, it displays the current + argument's help text as a hint. Set this to True to suppress the hint. Defaults to False. + :param descriptive_header: if the provided choices are CompletionItems, then this header will display + during tab completion. Defaults to None. + + # Args from original function + :param kwargs: keyword-arguments recognized by argparse._ActionsContainer.add_argument + + Note: You can only use 1 of the following in your argument: + choices, choices_function, choices_method, completer_function, completer_method + + See the header of this file for more information + + :return: the created argument action + """ + # pre-process special ranged nargs + nargs_range = None + + if isinstance(nargs, tuple): + if len(nargs) != 2 or not isinstance(nargs[0], int) or not isinstance(nargs[1], int): + raise ValueError('Ranged values for nargs must be a tuple of 2 integers') + if nargs[0] >= nargs[1]: + raise ValueError('Invalid nargs range. The first value must be less than the second') + if nargs[0] < 0: + raise ValueError('Negative numbers are invalid for nargs range.') + + # nargs_range is a two-item tuple (min, max) + nargs_range = nargs + + if nargs[0] == 0: + if nargs[1] > 1: + nargs_adjusted = '*' else: - self.nargs_adjusted = '+' + # this shouldn't use a range tuple, but yet here we are + nargs_adjusted = '?' else: - self.nargs_adjusted = nargs - - -# noinspection PyShadowingBuiltins,PyShadowingBuiltins -class _StoreRangeAction(argparse._StoreAction, _RangeAction): - def __init__(self, - option_strings, - dest, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=False, - help=None, - metavar=None) -> None: - - _RangeAction.__init__(self, nargs) - - argparse._StoreAction.__init__(self, - option_strings=option_strings, - dest=dest, - nargs=self.nargs_adjusted, - const=const, - default=default, - type=type, - choices=choices, - required=required, - help=help, - metavar=metavar) - - -# noinspection PyShadowingBuiltins,PyShadowingBuiltins -class _AppendRangeAction(argparse._AppendAction, _RangeAction): - def __init__(self, - option_strings, - dest, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=False, - help=None, - metavar=None) -> None: - - _RangeAction.__init__(self, nargs) - - argparse._AppendAction.__init__(self, - option_strings=option_strings, - dest=dest, - nargs=self.nargs_adjusted, - const=const, - default=default, - type=type, - choices=choices, - required=required, - help=help, - metavar=metavar) - - -def register_custom_actions(parser: argparse.ArgumentParser) -> None: - """Register custom argument action types""" - parser.register('action', None, _StoreRangeAction) - parser.register('action', 'store', _StoreRangeAction) - parser.register('action', 'append', _AppendRangeAction) - - -############################################################################### + nargs_adjusted = '+' + else: + nargs_adjusted = nargs + + # Call the original add_argument function + if nargs_adjusted is not None: + kwargs['nargs'] = nargs_adjusted + new_arg = orig_actions_container_add_argument(self, *args, **kwargs) + + if nargs_range is not None: + setattr(new_arg, ATTR_NARGS_RANGE, nargs_range) + + # Verify consistent use of arguments + choice_params = [new_arg.choices, choices_function, choices_method, completer_function, completer_method] + num_set = len(choice_params) - choice_params.count(None) + + if num_set > 1: + err_msg = ("Only one of the following may be used in an argparser argument at a time:\n" + "choices, choices_function, choices_method, completer_function, completer_method") + raise (ValueError(err_msg)) + + # Set the custom attributes used by AutoCompleter + if choices_function: + setattr(new_arg, ATTR_CHOICES_CALLABLE, + ChoicesCallable(is_method=False, is_completer=False, to_call=choices_function)) + elif choices_method: + setattr(new_arg, ATTR_CHOICES_CALLABLE, + ChoicesCallable(is_method=True, is_completer=False, to_call=choices_method)) + elif completer_function: + setattr(new_arg, ATTR_CHOICES_CALLABLE, + ChoicesCallable(is_method=False, is_completer=True, to_call=completer_function)) + elif completer_method: + setattr(new_arg, ATTR_CHOICES_CALLABLE, + ChoicesCallable(is_method=True, is_completer=True, to_call=completer_method)) + + setattr(new_arg, ATTR_SUPPRESS_TAB_HINT, suppress_hint) + setattr(new_arg, ATTR_DESCRIPTIVE_COMPLETION_HEADER, descriptive_header) + + return new_arg + + +# Overwrite _ActionsContainer.add_argument with our wrapper +# noinspection PyProtectedMember +argparse._ActionsContainer.add_argument = add_argument_wrapper + + +############################################################################################################ # Unless otherwise noted, everything below this point are copied from Python's # argparse implementation with minor tweaks to adjust output. # Changes are noted if it's buried in a block of copied code. Otherwise the # function will check for a special case and fall back to the parent function -############################################################################### +############################################################################################################ # noinspection PyCompatibility,PyShadowingBuiltins,PyShadowingBuiltins @@ -273,12 +326,14 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): return (result, ) * tuple_size return format + # noinspection PyProtectedMember def _format_args(self, action, default_metavar) -> str: get_metavar = self._metavar_formatter(action, default_metavar) # Begin cmd2 customization (less verbose) - if isinstance(action, _RangeAction) and \ - action.nargs_min is not None and action.nargs_max is not None: - result = '{}{{{}..{}}}'.format('%s' % get_metavar(1), action.nargs_min, action.nargs_max) + nargs_range = getattr(action, ATTR_NARGS_RANGE, None) + + if nargs_range is not None: + result = '{}{{{}..{}}}'.format('%s' % get_metavar(1), nargs_range[0], nargs_range[1]) elif action.nargs == ZERO_OR_MORE: result = '[%s [...]]' % get_metavar(1) elif action.nargs == ONE_OR_MORE: @@ -298,7 +353,6 @@ class Cmd2ArgParser(argparse.ArgumentParser): kwargs['formatter_class'] = Cmd2HelpFormatter super().__init__(*args, **kwargs) - register_custom_actions(self) def add_subparsers(self, **kwargs): """Custom override. Sets a default title if one was not given.""" @@ -323,6 +377,7 @@ class Cmd2ArgParser(argparse.ArgumentParser): formatted_message = style_error(formatted_message) self.exit(2, '{}\n\n'.format(formatted_message)) + # noinspection PyProtectedMember def format_help(self) -> str: """Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters""" formatter = self._get_formatter() @@ -380,11 +435,12 @@ class Cmd2ArgParser(argparse.ArgumentParser): file = sys.stderr ansi_aware_write(file, message) + # noinspection PyProtectedMember def _get_nargs_pattern(self, action) -> str: - # Override _get_nargs_pattern behavior to use the nargs ranges provided by AutoCompleter - if isinstance(action, _RangeAction) and \ - action.nargs_min is not None and action.nargs_max is not None: - nargs_pattern = '(-*A{{{},{}}}-*)'.format(action.nargs_min, action.nargs_max) + # Override _get_nargs_pattern behavior to support the nargs ranges + nargs_range = getattr(action, ATTR_NARGS_RANGE, None) + if nargs_range is not None: + nargs_pattern = '(-*A{{{},{}}}-*)'.format(nargs_range[0], nargs_range[1]) # if this is an optional action, -- is not allowed if action.option_strings: @@ -394,16 +450,17 @@ class Cmd2ArgParser(argparse.ArgumentParser): return super()._get_nargs_pattern(action) + # noinspection PyProtectedMember def _match_argument(self, action, arg_strings_pattern) -> int: - # Override _match_argument behavior to use the nargs ranges provided by AutoCompleter + # Override _match_argument behavior to support nargs ranges nargs_pattern = self._get_nargs_pattern(action) match = _re.match(nargs_pattern, arg_strings_pattern) # raise an exception if we weren't able to find a match if match is None: - if isinstance(action, _RangeAction) and \ - action.nargs_min is not None and action.nargs_max is not None: + nargs_range = getattr(action, ATTR_NARGS_RANGE, None) + if nargs_range is not None: raise ArgumentError(action, - 'Expected between {} and {} arguments'.format(action.nargs_min, action.nargs_max)) + 'Expected between {} and {} arguments'.format(nargs_range[0], nargs_range[1])) return super()._match_argument(action, arg_strings_pattern) |