summaryrefslogtreecommitdiff
path: root/cmd2
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2019-07-04 11:11:31 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2019-07-04 11:11:31 -0400
commit8f1bc02f2028ac869e61c9a88475a933046f4ee8 (patch)
tree1032bdabd7a26c5596878cc3c48a4e5738db9033 /cmd2
parentc9d5fc35166b4f6dcdb46fcb1255a013b3660f4a (diff)
downloadcmd2-git-8f1bc02f2028ac869e61c9a88475a933046f4ee8.tar.gz
No longer restricting nargs range support to Cmd2ArgParser
Diffstat (limited to 'cmd2')
-rw-r--r--cmd2/argparse_completer.py128
-rw-r--r--cmd2/argparse_custom.py273
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)