summaryrefslogtreecommitdiff
path: root/cmd2/argparse_completer.py
diff options
context:
space:
mode:
authorkotfu <kotfu@kotfu.net>2018-05-02 19:11:28 -0700
committerkotfu <kotfu@kotfu.net>2018-05-02 19:11:28 -0700
commit9ade8ac377b90f36445aeec1de01b650bbad2283 (patch)
tree78dede898fec07147302ce188a67f7ce09485a8c /cmd2/argparse_completer.py
parentad634b2e7f68392727246f796647b92d67172011 (diff)
parente8d952574743c00e2116d24044220cbaa95cfb38 (diff)
downloadcmd2-git-9ade8ac377b90f36445aeec1de01b650bbad2283.tar.gz
Merge branch 'ply' of https://github.com/python-cmd2/cmd2 into ply
Diffstat (limited to 'cmd2/argparse_completer.py')
-rwxr-xr-xcmd2/argparse_completer.py272
1 files changed, 269 insertions, 3 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 03f2d965..4964b1ec 100755
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -64,7 +64,8 @@ from typing import List, Dict, Tuple, Callable, Union
# imports copied from argparse to support our customized argparse functions
-from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _
+from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _, _get_action_name, SUPPRESS
+
import re as _re
@@ -75,6 +76,7 @@ from .rl_utils import rl_force_redisplay
# define the completion choices for the argument. You may provide a Collection or a Function.
ACTION_ARG_CHOICES = 'arg_choices'
+
class _RangeAction(object):
def __init__(self, nargs: Union[int, str, Tuple[int, int], None]):
self.nargs_min = None
@@ -103,6 +105,7 @@ class _RangeAction(object):
self.nargs_adjusted = nargs
+# noinspection PyShadowingBuiltins,PyShadowingBuiltins
class _StoreRangeAction(argparse._StoreAction, _RangeAction):
def __init__(self,
option_strings,
@@ -131,6 +134,7 @@ class _StoreRangeAction(argparse._StoreAction, _RangeAction):
metavar=metavar)
+# noinspection PyShadowingBuiltins,PyShadowingBuiltins
class _AppendRangeAction(argparse._AppendAction, _RangeAction):
def __init__(self,
option_strings,
@@ -433,7 +437,6 @@ class AutoCompleter(object):
return self.basic_complete(text, line, begidx, endidx, completers.keys())
return []
-
@staticmethod
def _process_action_nargs(action: argparse.Action, arg_state: _ArgumentState) -> None:
if isinstance(action, _RangeAction):
@@ -536,7 +539,10 @@ class AutoCompleter(object):
prefix = ' {0: <{width}} '.format(prefix, width=20)
pref_len = len(prefix)
- help_lines = action.help.splitlines()
+ if action.help is not None:
+ help_lines = action.help.splitlines()
+ else:
+ help_lines = ['']
if len(help_lines) == 1:
print('\nHint:\n{}{}\n'.format(prefix, help_lines[0]))
else:
@@ -571,6 +577,7 @@ class AutoCompleter(object):
###############################################################################
+# noinspection PyCompatibility,PyShadowingBuiltins,PyShadowingBuiltins
class ACHelpFormatter(argparse.HelpFormatter):
"""Custom help formatter to configure ordering of help text"""
@@ -631,6 +638,7 @@ class ACHelpFormatter(argparse.HelpFormatter):
# End cmd2 customization
# helper for wrapping lines
+ # noinspection PyMissingOrEmptyDocstring,PyShadowingNames
def get_lines(parts, indent, prefix=None):
lines = []
line = []
@@ -722,6 +730,7 @@ class ACHelpFormatter(argparse.HelpFormatter):
else:
result = default_metavar
+ # noinspection PyMissingOrEmptyDocstring
def format(tuple_size):
if isinstance(result, tuple):
return result
@@ -748,6 +757,7 @@ class ACHelpFormatter(argparse.HelpFormatter):
return text.splitlines()
+# noinspection PyCompatibility
class ACArgumentParser(argparse.ArgumentParser):
"""Custom argparse class to override error method to change default help text."""
@@ -866,3 +876,259 @@ class ACArgumentParser(argparse.ArgumentParser):
'Expected between {} and {} arguments'.format(action.nargs_min, action.nargs_max))
return super(ACArgumentParser, self)._match_argument(action, arg_strings_pattern)
+
+ def _parse_known_args(self, arg_strings, namespace):
+ # replace arg strings that are file references
+ if self.fromfile_prefix_chars is not None:
+ arg_strings = self._read_args_from_files(arg_strings)
+
+ # map all mutually exclusive arguments to the other arguments
+ # they can't occur with
+ action_conflicts = {}
+ for mutex_group in self._mutually_exclusive_groups:
+ group_actions = mutex_group._group_actions
+ for i, mutex_action in enumerate(mutex_group._group_actions):
+ conflicts = action_conflicts.setdefault(mutex_action, [])
+ conflicts.extend(group_actions[:i])
+ conflicts.extend(group_actions[i + 1:])
+
+ # find all option indices, and determine the arg_string_pattern
+ # which has an 'O' if there is an option at an index,
+ # an 'A' if there is an argument, or a '-' if there is a '--'
+ option_string_indices = {}
+ arg_string_pattern_parts = []
+ arg_strings_iter = iter(arg_strings)
+ for i, arg_string in enumerate(arg_strings_iter):
+
+ # all args after -- are non-options
+ if arg_string == '--':
+ arg_string_pattern_parts.append('-')
+ for arg_string in arg_strings_iter:
+ arg_string_pattern_parts.append('A')
+
+ # otherwise, add the arg to the arg strings
+ # and note the index if it was an option
+ else:
+ option_tuple = self._parse_optional(arg_string)
+ if option_tuple is None:
+ pattern = 'A'
+ else:
+ option_string_indices[i] = option_tuple
+ pattern = 'O'
+ arg_string_pattern_parts.append(pattern)
+
+ # join the pieces together to form the pattern
+ arg_strings_pattern = ''.join(arg_string_pattern_parts)
+
+ # converts arg strings to the appropriate and then takes the action
+ seen_actions = set()
+ seen_non_default_actions = set()
+
+ def take_action(action, argument_strings, option_string=None):
+ seen_actions.add(action)
+ argument_values = self._get_values(action, argument_strings)
+
+ # error if this argument is not allowed with other previously
+ # seen arguments, assuming that actions that use the default
+ # value don't really count as "present"
+ if argument_values is not action.default:
+ seen_non_default_actions.add(action)
+ for conflict_action in action_conflicts.get(action, []):
+ if conflict_action in seen_non_default_actions:
+ msg = _('not allowed with argument %s')
+ action_name = _get_action_name(conflict_action)
+ raise ArgumentError(action, msg % action_name)
+
+ # take the action if we didn't receive a SUPPRESS value
+ # (e.g. from a default)
+ if argument_values is not SUPPRESS:
+ action(self, namespace, argument_values, option_string)
+
+ # function to convert arg_strings into an optional action
+ def consume_optional(start_index):
+
+ # get the optional identified at this index
+ option_tuple = option_string_indices[start_index]
+ action, option_string, explicit_arg = option_tuple
+
+ # identify additional optionals in the same arg string
+ # (e.g. -xyz is the same as -x -y -z if no args are required)
+ match_argument = self._match_argument
+ action_tuples = []
+ while True:
+
+ # if we found no optional action, skip it
+ if action is None:
+ extras.append(arg_strings[start_index])
+ return start_index + 1
+
+ # if there is an explicit argument, try to match the
+ # optional's string arguments to only this
+ if explicit_arg is not None:
+ arg_count = match_argument(action, 'A')
+
+ # if the action is a single-dash option and takes no
+ # arguments, try to parse more single-dash options out
+ # of the tail of the option string
+ chars = self.prefix_chars
+ if arg_count == 0 and option_string[1] not in chars:
+ action_tuples.append((action, [], option_string))
+ char = option_string[0]
+ option_string = char + explicit_arg[0]
+ new_explicit_arg = explicit_arg[1:] or None
+ optionals_map = self._option_string_actions
+ if option_string in optionals_map:
+ action = optionals_map[option_string]
+ explicit_arg = new_explicit_arg
+ else:
+ msg = _('ignored explicit argument %r')
+ raise ArgumentError(action, msg % explicit_arg)
+
+ # if the action expect exactly one argument, we've
+ # successfully matched the option; exit the loop
+ elif arg_count == 1:
+ stop = start_index + 1
+ args = [explicit_arg]
+ action_tuples.append((action, args, option_string))
+ break
+
+ # error if a double-dash option did not use the
+ # explicit argument
+ else:
+ msg = _('ignored explicit argument %r')
+ raise ArgumentError(action, msg % explicit_arg)
+
+ # if there is no explicit argument, try to match the
+ # optional's string arguments with the following strings
+ # if successful, exit the loop
+ else:
+ start = start_index + 1
+ selected_patterns = arg_strings_pattern[start:]
+ arg_count = match_argument(action, selected_patterns)
+ stop = start + arg_count
+ args = arg_strings[start:stop]
+ action_tuples.append((action, args, option_string))
+ break
+
+ # add the Optional to the list and return the index at which
+ # the Optional's string args stopped
+ assert action_tuples
+ for action, args, option_string in action_tuples:
+ take_action(action, args, option_string)
+ return stop
+
+ # the list of Positionals left to be parsed; this is modified
+ # by consume_positionals()
+ positionals = self._get_positional_actions()
+
+ # function to convert arg_strings into positional actions
+ def consume_positionals(start_index):
+ # match as many Positionals as possible
+ match_partial = self._match_arguments_partial
+ selected_pattern = arg_strings_pattern[start_index:]
+ arg_counts = match_partial(positionals, selected_pattern)
+
+ ####################################################################
+ # Applied mixed.patch from https://bugs.python.org/issue15112
+ if 'O' in arg_strings_pattern[start_index:]:
+ # if there is an optional after this, remove
+ # 'empty' positionals from the current match
+
+ while len(arg_counts) > 1 and arg_counts[-1] == 0:
+ arg_counts = arg_counts[:-1]
+ ####################################################################
+
+ # slice off the appropriate arg strings for each Positional
+ # and add the Positional and its args to the list
+ for action, arg_count in zip(positionals, arg_counts):
+ args = arg_strings[start_index: start_index + arg_count]
+ start_index += arg_count
+ take_action(action, args)
+
+ # slice off the Positionals that we just parsed and return the
+ # index at which the Positionals' string args stopped
+ positionals[:] = positionals[len(arg_counts):]
+ return start_index
+
+ # consume Positionals and Optionals alternately, until we have
+ # passed the last option string
+ extras = []
+ start_index = 0
+ if option_string_indices:
+ max_option_string_index = max(option_string_indices)
+ else:
+ max_option_string_index = -1
+ while start_index <= max_option_string_index:
+
+ # consume any Positionals preceding the next option
+ next_option_string_index = min([
+ index
+ for index in option_string_indices
+ if index >= start_index])
+ if start_index != next_option_string_index:
+ positionals_end_index = consume_positionals(start_index)
+
+ # only try to parse the next optional if we didn't consume
+ # the option string during the positionals parsing
+ if positionals_end_index > start_index:
+ start_index = positionals_end_index
+ continue
+ else:
+ start_index = positionals_end_index
+
+ # if we consumed all the positionals we could and we're not
+ # at the index of an option string, there were extra arguments
+ if start_index not in option_string_indices:
+ strings = arg_strings[start_index:next_option_string_index]
+ extras.extend(strings)
+ start_index = next_option_string_index
+
+ # consume the next optional and any arguments for it
+ start_index = consume_optional(start_index)
+
+ # consume any positionals following the last Optional
+ stop_index = consume_positionals(start_index)
+
+ # if we didn't consume all the argument strings, there were extras
+ extras.extend(arg_strings[stop_index:])
+
+ # make sure all required actions were present and also convert
+ # action defaults which were not given as arguments
+ required_actions = []
+ for action in self._actions:
+ if action not in seen_actions:
+ if action.required:
+ required_actions.append(_get_action_name(action))
+ else:
+ # Convert action default now instead of doing it before
+ # parsing arguments to avoid calling convert functions
+ # twice (which may fail) if the argument was given, but
+ # only if it was defined already in the namespace
+ if (action.default is not None and
+ isinstance(action.default, str) and
+ hasattr(namespace, action.dest) and
+ action.default is getattr(namespace, action.dest)):
+ setattr(namespace, action.dest,
+ self._get_value(action, action.default))
+
+ if required_actions:
+ self.error(_('the following arguments are required: %s') %
+ ', '.join(required_actions))
+
+ # make sure all required groups had one option present
+ for group in self._mutually_exclusive_groups:
+ if group.required:
+ for action in group._group_actions:
+ if action in seen_non_default_actions:
+ break
+
+ # if no actions were used, report the error
+ else:
+ names = [_get_action_name(action)
+ for action in group._group_actions
+ if action.help is not SUPPRESS]
+ msg = _('one of the arguments %s is required')
+ self.error(msg % ' '.join(names))
+
+ # return the updated namespace and the extra arguments
+ return namespace, extras