summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xREADME.md2
-rw-r--r--cmd2/__init__.py1
-rw-r--r--cmd2/argparse_completer.py1396
-rw-r--r--cmd2/argparse_custom.py552
-rw-r--r--cmd2/cmd2.py225
-rw-r--r--cmd2/utils.py16
-rwxr-xr-xexamples/subcommands.py6
-rwxr-xr-xexamples/tab_autocomp_dynamic.py234
-rwxr-xr-xexamples/tab_autocompletion.py324
-rwxr-xr-xexamples/tab_completion.py4
-rwxr-xr-xexamples/table_display.py5
-rw-r--r--tests/test_acargparse.py66
-rw-r--r--tests/test_argparse_completer.py648
-rw-r--r--tests/test_argparse_custom.py145
-rw-r--r--tests/test_autocompletion.py345
-rw-r--r--tests/test_cmd2.py31
-rw-r--r--tests/test_completion.py19
17 files changed, 1859 insertions, 2160 deletions
diff --git a/README.md b/README.md
index 451a7bf0..fa42e52c 100755
--- a/README.md
+++ b/README.md
@@ -150,7 +150,7 @@ Instructions for implementing each feature follow.
See https://cmd2.readthedocs.io/en/latest/argument_processing.html for more details
- NOTE: `cmd2` also provides the `ACArgumentParser` customization of `argparse.ArgumentParser` for prettier formatting
+ NOTE: `cmd2` also provides the `Cmd2ArgParser` customization of `argparse.ArgumentParser` for prettier formatting
of help and RangeAction type
- `cmd2` applications function like a full-featured shell in many ways (and are cross-platform)
diff --git a/cmd2/__init__.py b/cmd2/__init__.py
index d3c92636..3b149601 100644
--- a/cmd2/__init__.py
+++ b/cmd2/__init__.py
@@ -11,6 +11,7 @@ except DistributionNotFound:
pass
from .ansi import style
+from .argparse_custom import Cmd2ArgParser, 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 539132dd..1393db0e 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -1,8 +1,9 @@
# coding=utf-8
# flake8: noqa C901
-# NOTE: Ignoreing flake8 cyclomatic complexity in this file because the complexity due to copy-and-paste overrides from
-# argparse
+# NOTE: Ignoring flake8 cyclomatic complexity in this file
"""
+This module adds tab completion to argparse parsers within cmd2 apps.
+
AutoCompleter interprets the argparse.ArgumentParser internals to automatically
generate the completion options for each argument.
@@ -58,153 +59,21 @@ How to supply completion choice lists or functions for sub-commands:
"""
import argparse
-import os
-import re as _re
-import sys
-
-# imports copied from argparse to support our customized argparse functions
-from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _, _get_action_name, SUPPRESS
-from typing import List, Dict, Tuple, Callable, Union
-
-from .ansi import ansi_aware_write, ansi_safe_wcswidth, style_error
+import shutil
+from typing import List, Union
+
+from . import cmd2
+from . import utils
+from .ansi import ansi_safe_wcswidth
+from .argparse_custom import ATTR_SUPPRESS_TAB_HINT, ATTR_DESCRIPTIVE_COMPLETION_HEADER, ATTR_NARGS_RANGE
+from .argparse_custom import ChoicesCallable, CompletionItem, ATTR_CHOICES_CALLABLE
from .rl_utils import rl_force_redisplay
-# attribute that can optionally added to an argparse argument (called an Action) to
-# define the completion choices for the argument. You may provide a Collection or a Function.
-ACTION_ARG_CHOICES = 'arg_choices'
-ACTION_SUPPRESS_HINT = 'suppress_hint'
-ACTION_DESCRIPTIVE_COMPLETION_HEADER = 'desc_header'
-
-
-class CompletionItem(str):
- """
- Completion item with descriptive text attached
-
- Returning this instead of a regular string for completion results will signal the
- autocompleter to output the completions results in a table of completion tokens
- with descriptions instead of just a table of tokens.
-
- For example, you'd see this:
- TOKEN Description
- MY_TOKEN Info about my token
- SOME_TOKEN Info about some token
- YET_ANOTHER Yet more info
-
- Instead of this:
- TOKEN_ID SOME_TOKEN YET_ANOTHER
-
- This is especially useful if you want to complete ID numbers in a more
- user-friendly manner. For example, you can provide this:
-
- ITEM_ID Item Name
- 1 My item
- 2 Another item
- 3 Yet another item
-
- Instead of this:
- 1 2 3
- """
- def __new__(cls, o, desc='', *args, **kwargs) -> str:
- return str.__new__(cls, o, *args, **kwargs)
-
- # noinspection PyMissingConstructor,PyUnusedLocal
- def __init__(self, o, desc='', *args, **kwargs) -> None:
- self.description = desc
-
-
-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 = '?'
- else:
- self.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)
+# If no descriptive header is supplied, then this will be used instead
+DEFAULT_DESCRIPTIVE_HEADER = 'Description'
+# noinspection PyProtectedMember
def is_potential_flag(token: str, parser: argparse.ArgumentParser) -> bool:
"""Determine if a token looks like a potential flag. Based on argparse._parse_optional()."""
# if it's an empty string, it was meant to be a positional
@@ -215,10 +84,6 @@ def is_potential_flag(token: str, parser: argparse.ArgumentParser) -> bool:
if not token[0] in parser.prefix_chars:
return False
- # if it's just a single character, it was meant to be positional
- if len(token) == 1:
- return False
-
# if it looks like a negative number, it was meant to be positional
# unless there are negative-number-like options
if parser._negative_number_matcher.match(token):
@@ -233,58 +98,68 @@ def is_potential_flag(token: str, parser: argparse.ArgumentParser) -> bool:
return True
+# noinspection PyProtectedMember
class AutoCompleter(object):
- """Automatically command line tab completion based on argparse parameters"""
+ """Automatic command line tab completion based on argparse parameters"""
class _ArgumentState(object):
- def __init__(self) -> None:
- self.min = None
- self.max = None
- self.count = 0
- self.needed = False
- self.variable = False
+ """Keeps state of an argument being parsed"""
- def reset(self) -> None:
- """reset tracking values"""
+ def __init__(self, arg_action: argparse.Action) -> None:
+ self.action = arg_action
self.min = None
self.max = None
self.count = 0
self.needed = False
self.variable = False
+ self.is_remainder = (self.action.nargs == argparse.REMAINDER)
+
+ # Check if nargs is a range
+ nargs_range = getattr(self.action, ATTR_NARGS_RANGE, None)
+ if nargs_range is not None:
+ self.min = nargs_range[0]
+ self.max = nargs_range[1]
+ self.variable = True
+
+ # Otherwise check against argparse types
+ elif self.action.nargs is None:
+ self.min = 1
+ self.max = 1
+ elif self.action.nargs == argparse.OPTIONAL:
+ self.min = 0
+ self.max = 1
+ self.variable = True
+ elif self.action.nargs == argparse.ZERO_OR_MORE or self.action.nargs == argparse.REMAINDER:
+ self.min = 0
+ self.max = float('inf')
+ self.variable = True
+ elif self.action.nargs == argparse.ONE_OR_MORE:
+ self.min = 1
+ self.max = float('inf')
+ self.variable = True
+ else:
+ self.min = self.action.nargs
+ self.max = self.action.nargs
- def __init__(self,
- parser: argparse.ArgumentParser,
- cmd2_app,
- token_start_index: int = 1,
- arg_choices: Dict[str, Union[List, Tuple, Callable]] = None,
- subcmd_args_lookup: dict = None,
- tab_for_arg_help: bool = True) -> None:
+ def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *,
+ token_start_index: int = 1) -> None:
"""
Create an AutoCompleter
:param parser: ArgumentParser instance
- :param cmd2_app: reference to the Cmd2 application. Enables argparse argument completion with class methods
+ :param cmd2_app: reference to the Cmd2 application that owns this AutoCompleter
:param token_start_index: index of the token to start parsing at
- :param arg_choices: dictionary mapping from argparse argument 'dest' name to list of choices
- :param subcmd_args_lookup: mapping a sub-command group name to a tuple to fill the child\
- AutoCompleter's arg_choices and subcmd_args_lookup parameters
- :param tab_for_arg_help: Enable of disable argument help when there's no completion result
"""
- if not subcmd_args_lookup:
- subcmd_args_lookup = {}
- forward_arg_choices = True
- else:
- forward_arg_choices = False
self._parser = parser
self._cmd2_app = cmd2_app
- self._arg_choices = arg_choices.copy() if arg_choices is not None else {}
+ self._arg_choices = {}
self._token_start_index = token_start_index
- self._tab_for_arg_help = tab_for_arg_help
self._flags = [] # all flags in this command
self._flags_without_args = [] # all flags that don't take arguments
self._flag_to_action = {} # maps flags to the argparse action object
self._positional_actions = [] # argument names for positional arguments (by position index)
+
# maps action name to sub-command autocompleter:
# action_name -> dict(sub_command -> completer)
self._positional_completers = {}
@@ -295,10 +170,11 @@ class AutoCompleter(object):
# if there are choices defined, record them in the arguments dictionary
if action.choices is not None:
self._arg_choices[action.dest] = action.choices
- # if completion choices are tagged on the action, record them
- elif hasattr(action, ACTION_ARG_CHOICES):
- action_arg_choices = getattr(action, ACTION_ARG_CHOICES)
- self._arg_choices[action.dest] = action_arg_choices
+
+ # otherwise check if a callable provides the choices for this argument
+ 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
if action.option_strings:
@@ -308,31 +184,32 @@ class AutoCompleter(object):
self._flag_to_action[option] = action
if action.nargs == 0:
self._flags_without_args.append(option)
+
+ # Otherwise this is a positional parameter
else:
self._positional_actions.append(action)
if isinstance(action, argparse._SubParsersAction):
sub_completers = {}
sub_commands = []
- args_for_action = subcmd_args_lookup[action.dest]\
- if action.dest in subcmd_args_lookup else {}
+
+ # Create an AutoCompleter for each subcommand of this command
for subcmd in action.choices:
- (subcmd_args, subcmd_lookup) = args_for_action[subcmd] if \
- subcmd in args_for_action else \
- (arg_choices, subcmd_args_lookup) if forward_arg_choices else ({}, {})
+
subcmd_start = token_start_index + len(self._positional_actions)
sub_completers[subcmd] = AutoCompleter(action.choices[subcmd],
cmd2_app,
- token_start_index=subcmd_start,
- arg_choices=subcmd_args,
- subcmd_args_lookup=subcmd_lookup,
- tab_for_arg_help=tab_for_arg_help)
+ token_start_index=subcmd_start)
sub_commands.append(subcmd)
+
self._positional_completers[action.dest] = sub_completers
self._arg_choices[action.dest] = sub_commands
def complete_command(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""Complete the command using the argparse metadata and provided argument dictionary"""
+ if len(tokens) <= self._token_start_index:
+ return []
+
# Count which positional argument index we're at now. Loop through all tokens on the command line so far
# Skip any flags or flag parameter tokens
next_pos_arg_index = 0
@@ -341,14 +218,11 @@ class AutoCompleter(object):
# That can happen when -- is used or an argument with nargs=argparse.REMAINDER is used
skip_remaining_flags = False
- pos_arg = AutoCompleter._ArgumentState()
- pos_action = None
+ # _ArgumentState of the current positional
+ pos_arg_state = None
- flag_arg = AutoCompleter._ArgumentState()
- flag_action = None
-
- # dict is used because object wrapper is necessary to allow inner functions to modify outer variables
- remainder = {'arg': None, 'action': None}
+ # _ArgumentState of the current flag
+ flag_arg_state = None
matched_flags = []
current_is_positional = False
@@ -362,63 +236,35 @@ class AutoCompleter(object):
def consume_flag_argument() -> None:
"""Consuming token as a flag argument"""
- # we're consuming flag arguments
# if the token does not look like a new flag, then count towards flag arguments
- if not is_potential_flag(token, self._parser) and flag_action is not None:
- flag_arg.count += 1
+ if flag_arg_state is not None and not is_potential_flag(token, self._parser):
+ flag_arg_state.count += 1
+
+ # Does this complete an option item for the flag?
+ arg_choices = self._resolve_choices_for_arg(flag_arg_state.action)
- # does this complete a option item for the flag
- arg_choices = self._resolve_choices_for_arg(flag_action)
- # if the current token matches the current position's autocomplete argument list,
- # track that we've used it already. Unless this is the current token, then keep it.
+ # If the current token isn't the one being completed and it's in the flag
+ # argument's autocomplete list, then track that we've used it already.
if not is_last_token and token in arg_choices:
- consumed_arg_values.setdefault(flag_action.dest, [])
- consumed_arg_values[flag_action.dest].append(token)
+ consumed_arg_values.setdefault(flag_arg_state.action.dest, [])
+ consumed_arg_values[flag_arg_state.action.dest].append(token)
def consume_positional_argument() -> None:
"""Consuming token as positional argument"""
- pos_arg.count += 1
-
- # does this complete a option item for the flag
- arg_choices = self._resolve_choices_for_arg(pos_action)
- # if the current token matches the current position's autocomplete argument list,
- # track that we've used it already. Unless this is the current token, then keep it.
- if not is_last_token and token in arg_choices:
- consumed_arg_values.setdefault(pos_action.dest, [])
- consumed_arg_values[pos_action.dest].append(token)
-
- 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
- arg_state.variable = True
- if arg_state.min is None or arg_state.max is None:
- if action.nargs is None:
- arg_state.min = 1
- arg_state.max = 1
- elif action.nargs == '+':
- arg_state.min = 1
- arg_state.max = float('inf')
- arg_state.variable = True
- elif action.nargs == '*' or action.nargs == argparse.REMAINDER:
- arg_state.min = 0
- arg_state.max = float('inf')
- arg_state.variable = True
- if action.nargs == argparse.REMAINDER:
- remainder['action'] = action
- remainder['arg'] = arg_state
- elif action.nargs == '?':
- arg_state.min = 0
- arg_state.max = 1
- arg_state.variable = True
- else:
- arg_state.min = action.nargs
- arg_state.max = action.nargs
+ if pos_arg_state is not None:
+ pos_arg_state.count += 1
+
+ # Does this complete an option item for the positional?
+ arg_choices = self._resolve_choices_for_arg(pos_arg_state.action)
+
+ # If the current token isn't the one being completed and it's in the positional
+ # argument's autocomplete list, then track that we've used it already.
+ if not is_last_token and token in arg_choices:
+ consumed_arg_values.setdefault(pos_arg_state.action.dest, [])
+ consumed_arg_values[pos_arg_state.action.dest].append(token)
# This next block of processing tries to parse all parameters before the last parameter.
- # We're trying to determine what specific argument the current cursor positition should be
+ # We're trying to determine what specific argument the current cursor position should be
# matched with. When we finish parsing all of the arguments, we can determine whether the
# last token is a positional or flag argument and which specific argument it is.
#
@@ -428,159 +274,185 @@ class AutoCompleter(object):
# arguments will be hidden from the list of available flags. Also, arguments with a
# defined list of possible values will exclude values that have already been used.
- # notes when the last token has been reached
+ # Notes when the token being completed has been reached
is_last_token = False
- for idx, token in enumerate(tokens):
- is_last_token = idx >= len(tokens) - 1
+ # Enumerate over the sliced list
+ for loop_index, token in enumerate(tokens[self._token_start_index:]):
+ token_index = loop_index + self._token_start_index
+ if token_index >= len(tokens) - 1:
+ is_last_token = True
+
+ # If we're in a positional REMAINDER arg, force all future tokens to go to that
+ if pos_arg_state is not None and pos_arg_state.is_remainder:
+ consume_positional_argument()
+ continue
+
+ # If we're in a flag REMAINDER arg, force all future tokens to go to that until a double dash is hit
+ elif flag_arg_state is not None and flag_arg_state.is_remainder:
+ skip_remaining_flags = True
+ if token == '--':
+ flag_arg_state = None
+ else:
+ consume_flag_argument()
+ continue
+
+ # Handle '--' which tells argparse all remaining arguments are non-flags
+ elif token == '--' and not skip_remaining_flags:
+ if is_last_token:
+ # Exit loop and see if -- can be completed into a flag
+ break
+ else:
+ # End the current flag
+ flag_arg_state = None
+ skip_remaining_flags = True
+ continue
- # Only start at the start token index
- if idx >= self._token_start_index:
+ current_is_positional = False
- # If a remainder action is found, force all future tokens to go to that
- if remainder['arg'] is not None:
- if remainder['action'] == pos_action:
+ # Are we consuming flag arguments?
+ if flag_arg_state is not None and flag_arg_state.needed:
+ consume_flag_argument()
+ else:
+ if not skip_remaining_flags:
+ # Special case when each of the following is true:
+ # - We're not in the middle of consuming flag arguments
+ # - The current positional argument count has hit the max count
+ # - The next positional argument is a REMAINDER argument
+ # Argparse will now treat all future tokens as arguments to the positional including tokens that
+ # look like flags so the completer should skip any flag related processing once this happens
+ if pos_arg_state is not None and pos_arg_state.count >= pos_arg_state.max and \
+ next_pos_arg_index < len(self._positional_actions) and \
+ self._positional_actions[next_pos_arg_index].nargs == argparse.REMAINDER:
+ skip_remaining_flags = True
+
+ # At this point we're no longer consuming flag arguments. Is the current argument a potential flag?
+ if is_potential_flag(token, self._parser) and not skip_remaining_flags:
+ # Reset flag arg state but not positional tracking because flags can be
+ # interspersed anywhere between positionals
+ flag_arg_state = None
+ action = None
+
+ # does the token fully match a known flag?
+ if token in self._flag_to_action:
+ action = self._flag_to_action[token]
+ elif hasattr(self._parser, 'allow_abbrev') and self._parser.allow_abbrev:
+ candidates_flags = [flag for flag in self._flag_to_action if flag.startswith(token)]
+ if len(candidates_flags) == 1:
+ action = self._flag_to_action[candidates_flags[0]]
+
+ if action is not None:
+ flag_arg_state = AutoCompleter._ArgumentState(action)
+
+ # It's possible we already have consumed values for this flag if it was used earlier
+ # in the command line. Reset them now for this use of the flag.
+ consumed_arg_values[flag_arg_state.action.dest] = []
+
+ # Keep track of what flags have already been used
+ # Flags with action set to append, append_const, and count can be reused
+ if not is_last_token and \
+ not isinstance(flag_arg_state.action, argparse._AppendAction) and \
+ not isinstance(flag_arg_state.action, argparse._AppendConstAction) and \
+ not isinstance(flag_arg_state.action, argparse._CountAction):
+ matched_flags.extend(flag_arg_state.action.option_strings)
+
+ # current token isn't a potential flag
+ # - does the last flag accept variable arguments?
+ # - have we reached the max arg count for the flag?
+ elif flag_arg_state is None or \
+ not flag_arg_state.variable or \
+ flag_arg_state.count >= flag_arg_state.max:
+ # previous flag doesn't accept variable arguments, count this as a positional argument
+
+ # reset flag tracking variables
+ flag_arg_state = None
+ current_is_positional = True
+
+ if len(token) > 0 and pos_arg_state is not None and pos_arg_state.count < pos_arg_state.max:
+ # we have positional action match and we haven't reached the max arg count, consume
+ # the positional argument and move on.
consume_positional_argument()
- continue
- elif remainder['action'] == flag_action:
- consume_flag_argument()
- continue
-
- current_is_positional = False
- # Are we consuming flag arguments?
- if not flag_arg.needed:
-
- if not skip_remaining_flags:
- # Special case when each of the following is true:
- # - We're not in the middle of consuming flag arguments
- # - The current positional argument count has hit the max count
- # - The next positional argument is a REMAINDER argument
- # Argparse will now treat all future tokens as arguments to the positional including tokens that
- # look like flags so the completer should skip any flag related processing once this happens
- if (pos_action is not None) and pos_arg.count >= pos_arg.max and \
- next_pos_arg_index < len(self._positional_actions) and \
- self._positional_actions[next_pos_arg_index].nargs == argparse.REMAINDER:
- skip_remaining_flags = True
-
- # At this point we're no longer consuming flag arguments. Is the current argument a potential flag?
- if is_potential_flag(token, self._parser) and not skip_remaining_flags:
- # reset some tracking values
- flag_arg.reset()
- # don't reset positional tracking because flags can be interspersed anywhere between positionals
- flag_action = None
-
- if token == '--':
- if is_last_token:
- # Exit loop and see if -- can be completed into a flag
- break
- else:
- # In argparse, all args after -- are non-flags
- skip_remaining_flags = True
-
- # does the token fully match a known flag?
- if token in self._flag_to_action:
- flag_action = self._flag_to_action[token]
- elif hasattr(self._parser, 'allow_abbrev') and self._parser.allow_abbrev:
- candidates_flags = [flag for flag in self._flag_to_action if flag.startswith(token)]
- if len(candidates_flags) == 1:
- flag_action = self._flag_to_action[candidates_flags[0]]
-
- if flag_action is not None:
- # resolve argument counts
- process_action_nargs(flag_action, flag_arg)
- if not is_last_token and not isinstance(flag_action, argparse._AppendAction):
- matched_flags.extend(flag_action.option_strings)
-
- # current token isn't a potential flag
- # - does the last flag accept variable arguments?
- # - have we reached the max arg count for the flag?
- elif not flag_arg.variable or flag_arg.count >= flag_arg.max:
- # previous flag doesn't accept variable arguments, count this as a positional argument
-
- # reset flag tracking variables
- flag_arg.reset()
- flag_action = None
- current_is_positional = True
-
- if len(token) > 0 and pos_action is not None and pos_arg.count < pos_arg.max:
- # we have positional action match and we haven't reached the max arg count, consume
- # the positional argument and move on.
+ elif pos_arg_state is None or pos_arg_state.count >= pos_arg_state.max:
+ # if we don't have a current positional action or we've reached the max count for the action
+ # close out the current positional argument state and set up for the next one
+ pos_index = next_pos_arg_index
+ next_pos_arg_index += 1
+ pos_arg_state = None
+
+ # are we at a sub-command? If so, forward to the matching completer
+ if pos_index < len(self._positional_actions):
+ action = self._positional_actions[pos_index]
+ pos_name = action.dest
+ if pos_name in self._positional_completers:
+ sub_completers = self._positional_completers[pos_name]
+ if token in sub_completers:
+ return sub_completers[token].complete_command(tokens, text, line,
+ begidx, endidx)
+
+ pos_arg_state = AutoCompleter._ArgumentState(action)
consume_positional_argument()
- elif pos_action is None or pos_arg.count >= pos_arg.max:
- # if we don't have a current positional action or we've reached the max count for the action
- # close out the current positional argument state and set up for the next one
- pos_index = next_pos_arg_index
- next_pos_arg_index += 1
- pos_arg.reset()
- pos_action = None
-
- # are we at a sub-command? If so, forward to the matching completer
- if pos_index < len(self._positional_actions):
- action = self._positional_actions[pos_index]
- pos_name = action.dest
- if pos_name in self._positional_completers:
- sub_completers = self._positional_completers[pos_name]
- if token in sub_completers:
- return sub_completers[token].complete_command(tokens, text, line,
- begidx, endidx)
- pos_action = action
- process_action_nargs(pos_action, pos_arg)
- consume_positional_argument()
-
- elif not is_last_token and pos_arg.max is not None:
- pos_action = None
- pos_arg.reset()
- else:
- consume_flag_argument()
+ elif not is_last_token and pos_arg_state is not None:
+ pos_arg_state = None
else:
consume_flag_argument()
- if remainder['arg'] is not None:
- skip_remaining_flags = True
-
- # don't reset this if we're on the last token - this allows completion to occur on the current token
- elif not is_last_token and flag_arg.min is not None:
- flag_arg.needed = flag_arg.count < flag_arg.min
+ # To allow completion of the final token, we only do the following on preceding tokens
+ if not is_last_token and flag_arg_state is not None:
+ flag_arg_state.needed = flag_arg_state.count < flag_arg_state.min
# Here we're done parsing all of the prior arguments. We know what the next argument is.
- completion_results = []
-
# if we don't have a flag to populate with arguments and the last token starts with
# a flag prefix then we'll complete the list of flag options
- if not flag_arg.needed and len(tokens[-1]) > 0 and tokens[-1][0] in self._parser.prefix_chars and \
- not skip_remaining_flags:
- return self._cmd2_app.basic_complete(text, line, begidx, endidx,
- [flag for flag in self._flags if flag not in matched_flags])
+ if (flag_arg_state is None or not flag_arg_state.needed) and \
+ is_potential_flag(tokens[-1], self._parser) and not skip_remaining_flags:
+
+ # Build a list of flags that can be tab completed
+ match_against = []
+
+ for flag in self._flags:
+ # Make sure this flag hasn't already been used
+ if flag not in matched_flags:
+ # Make sure this flag isn't considered hidden
+ action = self._flag_to_action[flag]
+ if action.help != argparse.SUPPRESS:
+ match_against.append(flag)
+
+ return utils.basic_complete(text, line, begidx, endidx, match_against)
+
+ completion_results = []
+
# we're not at a positional argument, see if we're in a flag argument
- elif not current_is_positional:
- if flag_action is not None:
- consumed = consumed_arg_values[flag_action.dest]\
- if flag_action.dest in consumed_arg_values else []
- # current_items.extend(self._resolve_choices_for_arg(flag_action, consumed))
- completion_results = self._complete_for_arg(flag_action, text, line, begidx, endidx, consumed)
+ if not current_is_positional:
+ if flag_arg_state is not None:
+ consumed = consumed_arg_values.get(flag_arg_state.action.dest, [])
+ completion_results = self._complete_for_arg(flag_arg_state.action, text, line,
+ begidx, endidx, consumed)
if not completion_results:
- self._print_action_help(flag_action)
+ self._print_arg_hint(flag_arg_state.action)
elif len(completion_results) > 1:
- completion_results = self._format_completions(flag_action, completion_results)
+ completion_results = self._format_completions(flag_arg_state.action, completion_results)
# ok, we're not a flag, see if there's a positional argument to complete
else:
- if pos_action is not None:
- pos_name = pos_action.dest
- consumed = consumed_arg_values[pos_name] if pos_name in consumed_arg_values else []
- completion_results = self._complete_for_arg(pos_action, text, line, begidx, endidx, consumed)
+ if pos_arg_state is not None:
+ consumed = consumed_arg_values.get(pos_arg_state.action.dest, [])
+ completion_results = self._complete_for_arg(pos_arg_state.action, text, line,
+ begidx, endidx, consumed)
+
if not completion_results:
- self._print_action_help(pos_action)
+ self._print_arg_hint(pos_arg_state.action)
elif len(completion_results) > 1:
- completion_results = self._format_completions(pos_action, completion_results)
+ completion_results = self._format_completions(pos_arg_state.action, completion_results)
return completion_results
def _format_completions(self, action, completions: List[Union[str, CompletionItem]]) -> List[str]:
- if completions and len(completions) > 1 and isinstance(completions[0], CompletionItem):
+ # Check if the results are CompletionItems and that there aren't too many to display
+ if 1 < len(completions) <= self._cmd2_app.max_completion_items and \
+ isinstance(completions[0], CompletionItem):
# If the user has not already sorted the CompletionItems, then sort them before appending the descriptions
if not self._cmd2_app.matches_sorted:
@@ -595,7 +467,7 @@ class AutoCompleter(object):
if item_width > token_width:
token_width = item_width
- term_size = os.get_terminal_size()
+ term_size = shutil.get_terminal_size()
fill_width = int(term_size.columns * .6) - (token_width + 2)
for item in completions:
entry = '{: <{token_width}}{: <{fill_width}}'.format(item, item.description,
@@ -603,10 +475,9 @@ class AutoCompleter(object):
fill_width=fill_width)
completions_with_desc.append(entry)
- try:
- desc_header = action.desc_header
- except AttributeError:
- desc_header = 'Description'
+ desc_header = getattr(action, ATTR_DESCRIPTIVE_COMPLETION_HEADER, None)
+ if desc_header is None:
+ desc_header = DEFAULT_DESCRIPTIVE_HEADER
header = '\n{: <{token_width}}{}'.format(action.dest.upper(), desc_header, token_width=token_width + 2)
self._cmd2_app.completion_header = header
@@ -615,139 +486,115 @@ class AutoCompleter(object):
return completions
def complete_command_help(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]:
- """Supports the completion of sub-commands for commands through the cmd2 help command."""
- for idx, token in enumerate(tokens):
- if idx >= self._token_start_index:
- if self._positional_completers:
- # For now argparse only allows 1 sub-command group per level
- # so this will only loop once.
- for completers in self._positional_completers.values():
- if token in completers:
- return completers[token].complete_command_help(tokens, text, line, begidx, endidx)
- else:
- return self._cmd2_app.basic_complete(text, line, begidx, endidx, completers.keys())
+ """
+ Supports cmd2's help command in the completion of sub-command names
+ :param tokens: command line tokens
+ :param text: the string prefix we are attempting to match (all returned matches must begin with it)
+ :param line: the current input line with leading whitespace removed
+ :param begidx: the beginning index of the prefix text
+ :param endidx: the ending index of the prefix text
+ :return: List of subcommand completions
+ """
+ for token in tokens[self._token_start_index:]:
+ if self._positional_completers:
+ # For now argparse only allows 1 sub-command group per level
+ # so this will only loop once.
+ for completers in self._positional_completers.values():
+ if token in completers:
+ return completers[token].complete_command_help(tokens, text, line, begidx, endidx)
+ else:
+ return utils.basic_complete(text, line, begidx, endidx, completers.keys())
return []
def format_help(self, tokens: List[str]) -> str:
- """Supports the completion of sub-commands for commands through the cmd2 help command."""
- for idx, token in enumerate(tokens):
- if idx >= self._token_start_index:
- if self._positional_completers:
- # For now argparse only allows 1 sub-command group per level
- # so this will only loop once.
- for completers in self._positional_completers.values():
- if token in completers:
- return completers[token].format_help(tokens)
+ """
+ Retrieve help text of a subcommand
+ :param tokens: command line tokens
+ :return: help text of the subcommand being queried
+ """
+ for token in tokens[self._token_start_index:]:
+ if self._positional_completers:
+ # For now argparse only allows 1 sub-command group per level
+ # so this will only loop once.
+ for completers in self._positional_completers.values():
+ if token in completers:
+ return completers[token].format_help(tokens)
return self._parser.format_help()
- def _complete_for_arg(self, action: argparse.Action,
- text: str,
- line: str,
- begidx: int,
- endidx: int,
- used_values=()) -> List[str]:
- if action.dest in self._arg_choices:
- arg_choices = self._arg_choices[action.dest]
-
- # if arg_choices is a tuple
- # Let's see if it's a custom completion function. If it is, return what it provides
- # To do this, we make sure the first element is either a callable
- # or it's the name of a callable in the application
- if isinstance(arg_choices, tuple) and len(arg_choices) > 0 and \
- (callable(arg_choices[0]) or
- (isinstance(arg_choices[0], str) and hasattr(self._cmd2_app, arg_choices[0]) and
- callable(getattr(self._cmd2_app, arg_choices[0]))
- )
- ):
-
- if callable(arg_choices[0]):
- completer = arg_choices[0]
- else:
- completer = getattr(self._cmd2_app, arg_choices[0])
-
- # extract the positional and keyword arguments from the tuple
- list_args = None
- kw_args = None
- for index in range(1, len(arg_choices)):
- if isinstance(arg_choices[index], list) or isinstance(arg_choices[index], tuple):
- list_args = arg_choices[index]
- elif isinstance(arg_choices[index], dict):
- kw_args = arg_choices[index]
-
- # call the provided function differently depending on the provided positional and keyword arguments
- if list_args is not None and kw_args is not None:
- return completer(text, line, begidx, endidx, *list_args, **kw_args)
- elif list_args is not None:
- return completer(text, line, begidx, endidx, *list_args)
- elif kw_args is not None:
- return completer(text, line, begidx, endidx, **kw_args)
+ def _complete_for_arg(self, arg: argparse.Action,
+ text: str, line: str, begidx: int, endidx: int, used_values=()) -> List[str]:
+ """Tab completion routine for argparse arguments"""
+
+ # Check the arg provides choices to the user
+ if arg.dest in self._arg_choices:
+ 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, ChoicesCallable) and arg_choices.is_completer:
+ if arg_choices.is_method:
+ return arg_choices.to_call(self._cmd2_app, text, line, begidx, endidx)
else:
- return completer(text, line, begidx, endidx)
+ return arg_choices.to_call(text, line, begidx, endidx)
+
+ # Otherwise use basic_complete on the choices
else:
- return self._cmd2_app.basic_complete(text, line, begidx, endidx,
- self._resolve_choices_for_arg(action, used_values))
+ return utils.basic_complete(text, line, begidx, endidx,
+ self._resolve_choices_for_arg(arg, used_values))
return []
- def _resolve_choices_for_arg(self, action: argparse.Action, used_values=()) -> List[str]:
- if action.dest in self._arg_choices:
- args = self._arg_choices[action.dest]
+ def _resolve_choices_for_arg(self, arg: argparse.Action, used_values=()) -> List[str]:
+ """Retrieve a list of choices that are available for a particular argument"""
+ if arg.dest in self._arg_choices:
+ arg_choices = self._arg_choices[arg.dest]
- # is the argument a string? If so, see if we can find an attribute in the
- # application matching the string.
- if isinstance(args, str):
- args = getattr(self._cmd2_app, args)
+ # 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 []
+ else:
+ if arg_choices.is_method:
+ arg_choices = arg_choices.to_call(self._cmd2_app)
+ else:
+ arg_choices = arg_choices.to_call()
- # is the provided argument a callable. If so, call it
- if callable(args):
- try:
- args = args(self._cmd2_app)
- except TypeError:
- args = args()
+ # Since arg_choices can be any iterable type, convert to a list
+ arg_choices = list(arg_choices)
- # filter out arguments we already used
- return [arg for arg in args if arg not in used_values]
+ # Since choices can be various types like int, we must convert them to strings
+ for index, choice in enumerate(arg_choices):
+ if not isinstance(choice, str,):
+ arg_choices[index] = str(choice)
+
+ # Filter out arguments we already used
+ return [choice for choice in arg_choices if choice not in used_values]
return []
- def _print_action_help(self, action: argparse.Action) -> None:
- # is parameter hinting disabled globally?
- if not self._tab_for_arg_help:
- return
+ @staticmethod
+ def _print_arg_hint(arg: argparse.Action) -> None:
+ """Print argument hint to the terminal when tab completion results in no results"""
- # is parameter hinting disabled for this parameter?
- try:
- suppress_hint = getattr(action, ACTION_SUPPRESS_HINT)
- except AttributeError:
- pass
- else:
- if suppress_hint:
- return
-
- if action.option_strings:
- flags = ', '.join(action.option_strings)
- param = ''
- if action.nargs is None or action.nargs != 0:
- param += ' ' + str(action.dest).upper()
+ # Check if hinting is disabled
+ suppress_hint = getattr(arg, ATTR_SUPPRESS_TAB_HINT, False)
+ if suppress_hint or arg.help == argparse.SUPPRESS:
+ return
+ # Check if this is a flag
+ if arg.option_strings:
+ flags = ', '.join(arg.option_strings)
+ param = ' ' + str(arg.dest).upper()
prefix = '{}{}'.format(flags, param)
- else:
- if action.dest != SUPPRESS:
- prefix = '{}'.format(str(action.dest).upper())
- else:
- prefix = ''
- if action.help is None:
- help_text = ''
+ # Otherwise this is a positional
else:
- help_text = action.help
-
- # is there anything to print for this parameter?
- if not prefix and not help_text:
- return
+ prefix = '{}'.format(str(arg.dest).upper())
prefix = ' {0: <{width}} '.format(prefix, width=20)
pref_len = len(prefix)
+
+ help_text = '' if arg.help is None else arg.help
help_lines = help_text.splitlines()
if len(help_lines) == 1:
@@ -759,582 +606,3 @@ class AutoCompleter(object):
# Redraw prompt and input line
rl_force_redisplay()
-
-
-###############################################################################
-# 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
-class ACHelpFormatter(argparse.RawTextHelpFormatter):
- """Custom help formatter to configure ordering of help text"""
-
- def _format_usage(self, usage, actions, groups, prefix) -> str:
- if prefix is None:
- prefix = _('Usage: ')
-
- # if usage is specified, use that
- if usage is not None:
- usage %= dict(prog=self._prog)
-
- # if no optionals or positionals are available, usage is just prog
- elif usage is None and not actions:
- usage = '%(prog)s' % dict(prog=self._prog)
-
- # if optionals and positionals are available, calculate usage
- elif usage is None:
- prog = '%(prog)s' % dict(prog=self._prog)
-
- # split optionals from positionals
- optionals = []
- positionals = []
- # Begin cmd2 customization (separates required and optional, applies to all changes in this function)
- required_options = []
- for action in actions:
- if action.option_strings:
- if action.required:
- required_options.append(action)
- else:
- optionals.append(action)
- else:
- positionals.append(action)
- # End cmd2 customization
-
- # build full usage string
- format = self._format_actions_usage
- action_usage = format(required_options + optionals + positionals, groups)
- usage = ' '.join([s for s in [prog, action_usage] if s])
-
- # wrap the usage parts if it's too long
- text_width = self._width - self._current_indent
- if len(prefix) + len(usage) > text_width:
-
- # Begin cmd2 customization
-
- # break usage into wrappable parts
- part_regexp = r'\(.*?\)+|\[.*?\]+|\S+'
- req_usage = format(required_options, groups)
- opt_usage = format(optionals, groups)
- pos_usage = format(positionals, groups)
- req_parts = _re.findall(part_regexp, req_usage)
- opt_parts = _re.findall(part_regexp, opt_usage)
- pos_parts = _re.findall(part_regexp, pos_usage)
- assert ' '.join(req_parts) == req_usage
- assert ' '.join(opt_parts) == opt_usage
- assert ' '.join(pos_parts) == pos_usage
-
- # End cmd2 customization
-
- # helper for wrapping lines
- # noinspection PyMissingOrEmptyDocstring,PyShadowingNames
- def get_lines(parts, indent, prefix=None):
- lines = []
- line = []
- if prefix is not None:
- line_len = len(prefix) - 1
- else:
- line_len = len(indent) - 1
- for part in parts:
- if line_len + 1 + len(part) > text_width and line:
- lines.append(indent + ' '.join(line))
- line = []
- line_len = len(indent) - 1
- line.append(part)
- line_len += len(part) + 1
- if line:
- lines.append(indent + ' '.join(line))
- if prefix is not None:
- lines[0] = lines[0][len(indent):]
- return lines
-
- # if prog is short, follow it with optionals or positionals
- if len(prefix) + len(prog) <= 0.75 * text_width:
- indent = ' ' * (len(prefix) + len(prog) + 1)
- # Begin cmd2 customization
- if req_parts:
- lines = get_lines([prog] + req_parts, indent, prefix)
- lines.extend(get_lines(opt_parts, indent))
- lines.extend(get_lines(pos_parts, indent))
- elif opt_parts:
- lines = get_lines([prog] + opt_parts, indent, prefix)
- lines.extend(get_lines(pos_parts, indent))
- elif pos_parts:
- lines = get_lines([prog] + pos_parts, indent, prefix)
- else:
- lines = [prog]
- # End cmd2 customization
-
- # if prog is long, put it on its own line
- else:
- indent = ' ' * len(prefix)
- # Begin cmd2 customization
- parts = req_parts + opt_parts + pos_parts
- lines = get_lines(parts, indent)
- if len(lines) > 1:
- lines = []
- lines.extend(get_lines(req_parts, indent))
- lines.extend(get_lines(opt_parts, indent))
- lines.extend(get_lines(pos_parts, indent))
- # End cmd2 customization
- lines = [prog] + lines
-
- # join lines into usage
- usage = '\n'.join(lines)
-
- # prefix with 'Usage:'
- return '%s%s\n\n' % (prefix, usage)
-
- def _format_action_invocation(self, action) -> str:
- if not action.option_strings:
- default = self._get_default_metavar_for_positional(action)
- metavar, = self._metavar_formatter(action, default)(1)
- return metavar
-
- else:
- parts = []
-
- # if the Optional doesn't take a value, format is:
- # -s, --long
- if action.nargs == 0:
- parts.extend(action.option_strings)
- return ', '.join(parts)
-
- # Begin cmd2 customization (less verbose)
- # if the Optional takes a value, format is:
- # -s, --long ARGS
- else:
- default = self._get_default_metavar_for_optional(action)
- args_string = self._format_args(action, default)
-
- return ', '.join(action.option_strings) + ' ' + args_string
- # End cmd2 customization
-
- def _metavar_formatter(self, action, default_metavar) -> Callable:
- if action.metavar is not None:
- result = action.metavar
- elif action.choices is not None:
- choice_strs = [str(choice) for choice in action.choices]
- # Begin cmd2 customization (added space after comma)
- result = '{%s}' % ', '.join(choice_strs)
- # End cmd2 customization
- else:
- result = default_metavar
-
- # noinspection PyMissingOrEmptyDocstring
- def format(tuple_size):
- if isinstance(result, tuple):
- return result
- else:
- return (result, ) * tuple_size
- return format
-
- 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)
- elif action.nargs == ZERO_OR_MORE:
- result = '[%s [...]]' % get_metavar(1)
- elif action.nargs == ONE_OR_MORE:
- result = '%s [...]' % get_metavar(1)
- # End cmd2 customization
- else:
- result = super()._format_args(action, default_metavar)
- return result
-
-
-# noinspection PyCompatibility
-class ACArgumentParser(argparse.ArgumentParser):
- """Custom argparse class to override error method to change default help text."""
-
- def __init__(self, *args, **kwargs) -> None:
- if 'formatter_class' not in kwargs:
- kwargs['formatter_class'] = ACHelpFormatter
-
- super().__init__(*args, **kwargs)
- register_custom_actions(self)
-
- self._custom_error_message = ''
-
- # Begin cmd2 customization
- def set_custom_message(self, custom_message: str = '') -> None:
- """
- Allows an error message override to the error() function, useful when forcing a
- re-parse of arguments with newly required parameters
- """
- self._custom_error_message = custom_message
- # End cmd2 customization
-
- def add_subparsers(self, **kwargs):
- """Custom override. Sets a default title if one was not given."""
- if 'title' not in kwargs:
- kwargs['title'] = 'sub-commands'
-
- return super().add_subparsers(**kwargs)
-
- def error(self, message: str) -> None:
- """Custom error override. Allows application to control the error being displayed by argparse"""
- if self._custom_error_message:
- message = self._custom_error_message
- self._custom_error_message = ''
-
- lines = message.split('\n')
- linum = 0
- formatted_message = ''
- for line in lines:
- if linum == 0:
- formatted_message = 'Error: ' + line
- else:
- formatted_message += '\n ' + line
- linum += 1
-
- self.print_usage(sys.stderr)
- formatted_message = style_error(formatted_message)
- self.exit(2, '{}\n\n'.format(formatted_message))
-
- def format_help(self) -> str:
- """Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters"""
- formatter = self._get_formatter()
-
- # usage
- formatter.add_usage(self.usage, self._actions,
- self._mutually_exclusive_groups)
-
- # description
- formatter.add_text(self.description)
-
- # Begin cmd2 customization (separate required and optional arguments)
-
- # positionals, optionals and user-defined groups
- for action_group in self._action_groups:
- if action_group.title == 'optional arguments':
- # check if the arguments are required, group accordingly
- req_args = []
- opt_args = []
- for action in action_group._group_actions:
- if action.required:
- req_args.append(action)
- else:
- opt_args.append(action)
-
- # separately display required arguments
- formatter.start_section('required arguments')
- formatter.add_text(action_group.description)
- formatter.add_arguments(req_args)
- formatter.end_section()
-
- # now display truly optional arguments
- formatter.start_section(action_group.title)
- formatter.add_text(action_group.description)
- formatter.add_arguments(opt_args)
- formatter.end_section()
- else:
- formatter.start_section(action_group.title)
- formatter.add_text(action_group.description)
- formatter.add_arguments(action_group._group_actions)
- formatter.end_section()
-
- # End cmd2 customization
-
- # epilog
- formatter.add_text(self.epilog)
-
- # determine help from format above
- return formatter.format_help() + '\n'
-
- def _print_message(self, message, file=None):
- # Override _print_message to use ansi_aware_write() since we use ANSI escape characters to support color
- if message:
- if file is None:
- file = _sys.stderr
- ansi_aware_write(file, message)
-
- 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)
-
- # if this is an optional action, -- is not allowed
- if action.option_strings:
- nargs_pattern = nargs_pattern.replace('-*', '')
- nargs_pattern = nargs_pattern.replace('-', '')
- return nargs_pattern
- return super(ACArgumentParser, self)._get_nargs_pattern(action)
-
- def _match_argument(self, action, arg_strings_pattern) -> int:
- # match the pattern for this action to the arg strings
- 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:
- raise ArgumentError(action,
- 'Expected between {} and {} arguments'.format(action.nargs_min, action.nargs_max))
-
- return super(ACArgumentParser, self)._match_argument(action, arg_strings_pattern)
-
- # This is the official python implementation with a 5 year old patch applied
- # See the comment below describing the patch
- def _parse_known_args(self, arg_strings, namespace) -> Tuple[argparse.Namespace, List[str]]: # pragma: no cover
- # 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 cur_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
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
new file mode 100644
index 00000000..5e3ed7f5
--- /dev/null
+++ b/cmd2/argparse_custom.py
@@ -0,0 +1,552 @@
+# coding=utf-8
+import argparse
+import re as _re
+import sys
+# noinspection PyUnresolvedReferences,PyProtectedMember
+from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _
+from typing import Any, Callable, Iterable, List, Optional, Tuple, Union
+
+from .ansi import ansi_aware_write, style_error
+
+############################################################################################################
+# 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 CompletionItem(str):
+ """
+ Completion item with descriptive text attached
+
+ Returning this instead of a regular string for completion results will signal the
+ autocompleter to output the completions results in a table of completion tokens
+ with descriptions instead of just a table of tokens.
+
+ For example, you'd see this:
+ TOKEN Description
+ MY_TOKEN Info about my token
+ SOME_TOKEN Info about some token
+ YET_ANOTHER Yet more info
+
+ Instead of this:
+ TOKEN_ID SOME_TOKEN YET_ANOTHER
+
+ This is especially useful if you want to complete ID numbers in a more
+ user-friendly manner. For example, you can provide this:
+
+ ITEM_ID Item Name
+ 1 My item
+ 2 Another item
+ 3 Yet another item
+
+ Instead of this:
+ 1 2 3
+
+ Example:
+ token = 1
+ token_description = "My Item"
+ completion_item = CompletionItem(token, token_description)
+ """
+ def __new__(cls, value: object, *args, **kwargs) -> str:
+ return super().__new__(cls, value)
+
+ # noinspection PyUnusedLocal
+ def __init__(self, value: object, desc: str = '', *args, **kwargs) -> None:
+ """
+ CompletionItem Initializer
+
+ :param value: the value being tab completed
+ :param desc: description text to display
+ :param args: args for str __init__
+ :param kwargs: kwargs for str __init__
+ """
+ super().__init__(*args, **kwargs)
+ self.description = desc
+
+
+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
+
+
+############################################################################################################
+# Patch _ActionsContainer.add_argument with our wrapper to support more arguments
+############################################################################################################
+
+# Save original _ActionsContainer.add_argument so we can call it in 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_tab_hint: bool = False,
+ descriptive_header: Optional[str] = None,
+ **kwargs) -> argparse.Action:
+ """
+ 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 functionality 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_tab_hint: when AutoCompleter has no results to show during tab completion, it displays the current
+ argument's help text as a hint. Set this to True to suppress the hint. If this argument's
+ help text is set to argparse.SUPPRESS, then tab hints will not display regardless of the
+ value passed for suppress_tab_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 nargs is not None:
+ # Check if nargs was given as a range
+ if isinstance(nargs, tuple):
+
+ # Validate 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')
+
+ # Save the nargs tuple as our range setting
+ nargs_range = nargs
+
+ # Convert nargs into a format argparse recognizes
+ if nargs_range[0] == 0:
+ if nargs_range[1] > 1:
+ nargs_adjusted = argparse.ZERO_OR_MORE
+ else:
+ nargs_adjusted = argparse.OPTIONAL
+ else:
+ nargs_adjusted = argparse.ONE_OR_MORE
+ else:
+ nargs_adjusted = nargs
+
+ # Add the argparse-recognized version of nargs to kwargs
+ kwargs['nargs'] = nargs_adjusted
+
+ # Create the argument using 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
+ setattr(new_arg, ATTR_NARGS_RANGE, nargs_range)
+
+ 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_tab_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
+
+############################################################################################################
+# Patch ArgumentParser._get_nargs_pattern with our wrapper to nargs ranges
+############################################################################################################
+
+# Save original ArgumentParser._get_nargs_pattern so we can call it in our wrapper
+# noinspection PyProtectedMember
+orig_argument_parser_get_nargs_pattern = argparse.ArgumentParser._get_nargs_pattern
+
+
+# noinspection PyProtectedMember
+def _get_nargs_pattern_wrapper(self, action) -> str:
+ # Wrapper around ArgumentParser._get_nargs_pattern behavior to support 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:
+ nargs_pattern = nargs_pattern.replace('-*', '')
+ nargs_pattern = nargs_pattern.replace('-', '')
+ return nargs_pattern
+
+ return orig_argument_parser_get_nargs_pattern(self, action)
+
+
+# Overwrite ArgumentParser._get_nargs_pattern with our wrapper
+# noinspection PyProtectedMember
+argparse.ArgumentParser._get_nargs_pattern = _get_nargs_pattern_wrapper
+
+
+############################################################################################################
+# Patch ArgumentParser._match_argument with our wrapper to nargs ranges
+############################################################################################################
+# noinspection PyProtectedMember
+orig_argument_parser_match_argument = argparse.ArgumentParser._match_argument
+
+
+# noinspection PyProtectedMember
+def _match_argument_wrapper(self, action, arg_strings_pattern) -> int:
+ # Wrapper around ArgumentParser._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:
+ nargs_range = getattr(action, ATTR_NARGS_RANGE, None)
+ if nargs_range is not None:
+ raise ArgumentError(action,
+ 'Expected between {} and {} arguments'.format(nargs_range[0], nargs_range[1]))
+
+ return orig_argument_parser_match_argument(self, action, arg_strings_pattern)
+
+
+# Overwrite ArgumentParser._match_argument with our wrapper
+# noinspection PyProtectedMember
+argparse.ArgumentParser._match_argument = _match_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
+class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
+ """Custom help formatter to configure ordering of help text"""
+
+ def _format_usage(self, usage, actions, groups, prefix) -> str:
+ if prefix is None:
+ prefix = _('Usage: ')
+
+ # if usage is specified, use that
+ if usage is not None:
+ usage %= dict(prog=self._prog)
+
+ # if no optionals or positionals are available, usage is just prog
+ elif usage is None and not actions:
+ usage = '%(prog)s' % dict(prog=self._prog)
+
+ # if optionals and positionals are available, calculate usage
+ elif usage is None:
+ prog = '%(prog)s' % dict(prog=self._prog)
+
+ # split optionals from positionals
+ optionals = []
+ positionals = []
+ # Begin cmd2 customization (separates required and optional, applies to all changes in this function)
+ required_options = []
+ for action in actions:
+ if action.option_strings:
+ if action.required:
+ required_options.append(action)
+ else:
+ optionals.append(action)
+ else:
+ positionals.append(action)
+ # End cmd2 customization
+
+ # build full usage string
+ format = self._format_actions_usage
+ action_usage = format(required_options + optionals + positionals, groups)
+ usage = ' '.join([s for s in [prog, action_usage] if s])
+
+ # wrap the usage parts if it's too long
+ text_width = self._width - self._current_indent
+ if len(prefix) + len(usage) > text_width:
+
+ # Begin cmd2 customization
+
+ # break usage into wrappable parts
+ part_regexp = r'\(.*?\)+|\[.*?\]+|\S+'
+ req_usage = format(required_options, groups)
+ opt_usage = format(optionals, groups)
+ pos_usage = format(positionals, groups)
+ req_parts = _re.findall(part_regexp, req_usage)
+ opt_parts = _re.findall(part_regexp, opt_usage)
+ pos_parts = _re.findall(part_regexp, pos_usage)
+ assert ' '.join(req_parts) == req_usage
+ assert ' '.join(opt_parts) == opt_usage
+ assert ' '.join(pos_parts) == pos_usage
+
+ # End cmd2 customization
+
+ # helper for wrapping lines
+ # noinspection PyMissingOrEmptyDocstring,PyShadowingNames
+ def get_lines(parts, indent, prefix=None):
+ lines = []
+ line = []
+ if prefix is not None:
+ line_len = len(prefix) - 1
+ else:
+ line_len = len(indent) - 1
+ for part in parts:
+ if line_len + 1 + len(part) > text_width and line:
+ lines.append(indent + ' '.join(line))
+ line = []
+ line_len = len(indent) - 1
+ line.append(part)
+ line_len += len(part) + 1
+ if line:
+ lines.append(indent + ' '.join(line))
+ if prefix is not None:
+ lines[0] = lines[0][len(indent):]
+ return lines
+
+ # if prog is short, follow it with optionals or positionals
+ if len(prefix) + len(prog) <= 0.75 * text_width:
+ indent = ' ' * (len(prefix) + len(prog) + 1)
+ # Begin cmd2 customization
+ if req_parts:
+ lines = get_lines([prog] + req_parts, indent, prefix)
+ lines.extend(get_lines(opt_parts, indent))
+ lines.extend(get_lines(pos_parts, indent))
+ elif opt_parts:
+ lines = get_lines([prog] + opt_parts, indent, prefix)
+ lines.extend(get_lines(pos_parts, indent))
+ elif pos_parts:
+ lines = get_lines([prog] + pos_parts, indent, prefix)
+ else:
+ lines = [prog]
+ # End cmd2 customization
+
+ # if prog is long, put it on its own line
+ else:
+ indent = ' ' * len(prefix)
+ # Begin cmd2 customization
+ parts = req_parts + opt_parts + pos_parts
+ lines = get_lines(parts, indent)
+ if len(lines) > 1:
+ lines = []
+ lines.extend(get_lines(req_parts, indent))
+ lines.extend(get_lines(opt_parts, indent))
+ lines.extend(get_lines(pos_parts, indent))
+ # End cmd2 customization
+ lines = [prog] + lines
+
+ # join lines into usage
+ usage = '\n'.join(lines)
+
+ # prefix with 'Usage:'
+ return '%s%s\n\n' % (prefix, usage)
+
+ def _format_action_invocation(self, action) -> str:
+ if not action.option_strings:
+ default = self._get_default_metavar_for_positional(action)
+ metavar, = self._metavar_formatter(action, default)(1)
+ return metavar
+
+ else:
+ parts = []
+
+ # if the Optional doesn't take a value, format is:
+ # -s, --long
+ if action.nargs == 0:
+ parts.extend(action.option_strings)
+ return ', '.join(parts)
+
+ # Begin cmd2 customization (less verbose)
+ # if the Optional takes a value, format is:
+ # -s, --long ARGS
+ else:
+ default = self._get_default_metavar_for_optional(action)
+ args_string = self._format_args(action, default)
+
+ return ', '.join(action.option_strings) + ' ' + args_string
+ # End cmd2 customization
+
+ def _metavar_formatter(self, action, default_metavar) -> Callable:
+ if action.metavar is not None:
+ result = action.metavar
+ elif action.choices is not None:
+ choice_strs = [str(choice) for choice in action.choices]
+ # Begin cmd2 customization (added space after comma)
+ result = '{%s}' % ', '.join(choice_strs)
+ # End cmd2 customization
+ else:
+ result = default_metavar
+
+ # noinspection PyMissingOrEmptyDocstring
+ def format(tuple_size):
+ if isinstance(result, tuple):
+ return result
+ else:
+ 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)
+ 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:
+ result = '%s [...]' % get_metavar(1)
+ # End cmd2 customization
+ else:
+ result = super()._format_args(action, default_metavar)
+ return result
+
+
+# noinspection PyCompatibility
+class Cmd2ArgParser(argparse.ArgumentParser):
+ """Custom ArgumentParser class that improves error and help output"""
+
+ def __init__(self, *args, **kwargs) -> None:
+ if 'formatter_class' not in kwargs:
+ kwargs['formatter_class'] = Cmd2HelpFormatter
+
+ super().__init__(*args, **kwargs)
+
+ def add_subparsers(self, **kwargs):
+ """Custom override. Sets a default title if one was not given."""
+ if 'title' not in kwargs:
+ kwargs['title'] = 'sub-commands'
+
+ return super().add_subparsers(**kwargs)
+
+ def error(self, message: str) -> None:
+ """Custom override that applies custom formatting to the error message"""
+ lines = message.split('\n')
+ linum = 0
+ formatted_message = ''
+ for line in lines:
+ if linum == 0:
+ formatted_message = 'Error: ' + line
+ else:
+ formatted_message += '\n ' + line
+ linum += 1
+
+ self.print_usage(sys.stderr)
+ 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()
+
+ # usage
+ formatter.add_usage(self.usage, self._actions,
+ self._mutually_exclusive_groups)
+
+ # description
+ formatter.add_text(self.description)
+
+ # Begin cmd2 customization (separate required and optional arguments)
+
+ # positionals, optionals and user-defined groups
+ for action_group in self._action_groups:
+ if action_group.title == 'optional arguments':
+ # check if the arguments are required, group accordingly
+ req_args = []
+ opt_args = []
+ for action in action_group._group_actions:
+ if action.required:
+ req_args.append(action)
+ else:
+ opt_args.append(action)
+
+ # separately display required arguments
+ formatter.start_section('required arguments')
+ formatter.add_text(action_group.description)
+ formatter.add_arguments(req_args)
+ formatter.end_section()
+
+ # now display truly optional arguments
+ formatter.start_section(action_group.title)
+ formatter.add_text(action_group.description)
+ formatter.add_arguments(opt_args)
+ formatter.end_section()
+ else:
+ formatter.start_section(action_group.title)
+ formatter.add_text(action_group.description)
+ formatter.add_arguments(action_group._group_actions)
+ formatter.end_section()
+
+ # End cmd2 customization
+
+ # epilog
+ formatter.add_text(self.epilog)
+
+ # determine help from format above
+ return formatter.format_help() + '\n'
+
+ def _print_message(self, message, file=None):
+ # Override _print_message to use ansi_aware_write() since we use ANSI escape characters to support color
+ if message:
+ if file is None:
+ file = sys.stderr
+ ansi_aware_write(file, message)
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 3aa4beb3..14107f5b 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -46,7 +46,7 @@ from . import ansi
from . import constants
from . import plugin
from . import utils
-from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
+from .argparse_custom import Cmd2ArgParser, CompletionItem
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .history import History, HistoryItem
from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split
@@ -467,6 +467,11 @@ class Cmd(cmd.Cmd):
# Otherwise it can be set to any custom key to meet your needs.
self.matches_sort_key = ALPHABETICAL_SORT_KEY
+ # The maximum number of CompletionItems to display during tab completion. If the number of possible
+ # completions exceeds this number, suggestions will be displayed in the typical columnized format and
+ # will not include the description value of the CompletionItems.
+ self.max_completion_items = 50
+
############################################################################################################
# The following variables are used by tab-completion functions. They are reset each time complete() is run
# in reset_completion_defaults() and it is up to completer functions to set them before returning results.
@@ -594,7 +599,8 @@ class Cmd(cmd.Cmd):
if self.broken_pipe_warning:
sys.stderr.write(self.broken_pipe_warning)
- def perror(self, msg: Any, *, end: str = '\n', apply_style: bool = True) -> None:
+ @staticmethod
+ def perror(msg: Any, *, end: str = '\n', apply_style: bool = True) -> None:
"""Print message to sys.stderr
:param msg: message to print (anything convertible to a str with '{}'.format() is OK)
@@ -835,23 +841,8 @@ class Cmd(cmd.Cmd):
return tokens, raw_tokens
- # noinspection PyUnusedLocal
- @staticmethod
- def basic_complete(text: str, line: str, begidx: int, endidx: int, match_against: Iterable) -> List[str]:
- """
- Performs tab completion against a list
-
- :param text: the string prefix we are attempting to match (all returned matches must begin with it)
- :param line: the current input line with leading whitespace removed
- :param begidx: the beginning index of the prefix text
- :param endidx: the ending index of the prefix text
- :param match_against: the list being matched against
- :return: a list of possible tab completions
- """
- return [cur_match for cur_match in match_against if cur_match.startswith(text)]
-
- def delimiter_complete(self, text: str, line: str, begidx: int, endidx: int, match_against: Iterable,
- delimiter: str) -> List[str]:
+ def delimiter_complete(self, text: str, line: str, begidx: int, endidx: int,
+ match_against: Iterable, delimiter: str) -> List[str]:
"""
Performs tab completion against a list but each match is split on a delimiter and only
the portion of the match being tab completed is shown as the completion suggestions.
@@ -884,7 +875,7 @@ class Cmd(cmd.Cmd):
:param delimiter: what delimits each portion of the matches (ex: paths are delimited by a slash)
:return: a list of possible tab completions
"""
- matches = self.basic_complete(text, line, begidx, endidx, match_against)
+ matches = utils.basic_complete(text, line, begidx, endidx, match_against)
# Display only the portion of the match that's being completed based on delimiter
if matches:
@@ -912,7 +903,7 @@ class Cmd(cmd.Cmd):
return matches
def flag_based_complete(self, text: str, line: str, begidx: int, endidx: int,
- flag_dict: Dict[str, Union[Iterable, Callable]],
+ flag_dict: Dict[str, Union[Iterable, Callable]], *,
all_else: Union[None, Iterable, Callable] = None) -> List[str]:
"""Tab completes based on a particular flag preceding the token being completed.
@@ -945,7 +936,7 @@ class Cmd(cmd.Cmd):
# Perform tab completion using an Iterable
if isinstance(match_against, Iterable):
- completions_matches = self.basic_complete(text, line, begidx, endidx, match_against)
+ completions_matches = utils.basic_complete(text, line, begidx, endidx, match_against)
# Perform tab completion using a function
elif callable(match_against):
@@ -954,7 +945,7 @@ class Cmd(cmd.Cmd):
return completions_matches
def index_based_complete(self, text: str, line: str, begidx: int, endidx: int,
- index_dict: Mapping[int, Union[Iterable, Callable]],
+ index_dict: Mapping[int, Union[Iterable, Callable]], *,
all_else: Union[None, Iterable, Callable] = None) -> List[str]:
"""Tab completes based on a fixed position in the input string.
@@ -989,7 +980,7 @@ class Cmd(cmd.Cmd):
# Perform tab completion using a Iterable
if isinstance(match_against, Iterable):
- matches = self.basic_complete(text, line, begidx, endidx, match_against)
+ matches = utils.basic_complete(text, line, begidx, endidx, match_against)
# Perform tab completion using a function
elif callable(match_against):
@@ -998,7 +989,7 @@ class Cmd(cmd.Cmd):
return matches
# noinspection PyUnusedLocal
- def path_complete(self, text: str, line: str, begidx: int, endidx: int,
+ def path_complete(self, text: str, line: str, begidx: int, endidx: int, *,
path_filter: Optional[Callable[[str], bool]] = None) -> List[str]:
"""Performs completion of local file system paths
@@ -1142,7 +1133,7 @@ class Cmd(cmd.Cmd):
return matches
- def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int,
+ def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *,
complete_blank: bool = False) -> List[str]:
"""Performs completion of executables either in a user's path or a given path
@@ -1165,7 +1156,7 @@ class Cmd(cmd.Cmd):
# Otherwise look for executables in the given path
else:
return self.path_complete(text, line, begidx, endidx,
- lambda path: os.path.isdir(path) or os.access(path, os.X_OK))
+ path_filter=lambda path: os.path.isdir(path) or os.access(path, os.X_OK))
def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: Callable) -> List[str]:
"""Called by complete() as the first tab completion function for all commands
@@ -1541,8 +1532,8 @@ class Cmd(cmd.Cmd):
else:
# Complete token against anything a user can run
- self.completion_matches = self.basic_complete(text, line, begidx, endidx,
- self._get_commands_aliases_and_macros_for_completion())
+ self.completion_matches = utils.basic_complete(text, line, begidx, endidx,
+ self._get_commands_aliases_and_macros_for_completion())
# Handle single result
if len(self.completion_matches) == 1:
@@ -1589,22 +1580,19 @@ class Cmd(cmd.Cmd):
def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int,
argparser: argparse.ArgumentParser) -> List[str]:
- """Default completion function for argparse commands."""
+ """Default completion function for argparse commands"""
+ from .argparse_completer import AutoCompleter
completer = AutoCompleter(argparser, self)
-
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
- if not tokens:
- return []
-
return completer.complete_command(tokens, text, line, begidx, endidx)
def get_all_commands(self) -> List[str]:
- """Returns a list of all commands."""
+ """Return a list of all commands"""
return [name[len(COMMAND_FUNC_PREFIX):] for name in self.get_names()
if name.startswith(COMMAND_FUNC_PREFIX) and callable(getattr(self, name))]
def get_visible_commands(self) -> List[str]:
- """Returns a list of commands that have not been hidden or disabled."""
+ """Return a list of commands that have not been hidden or disabled"""
commands = self.get_all_commands()
# Remove the hidden commands
@@ -1619,23 +1607,23 @@ class Cmd(cmd.Cmd):
return commands
- def _get_alias_names(self) -> List[str]:
- """Return list of current alias names"""
- return list(self.aliases)
+ def _get_alias_completion_items(self) -> List[CompletionItem]:
+ """Return list of current alias names and values as CompletionItems"""
+ return [CompletionItem(cur_key, self.aliases[cur_key]) for cur_key in self.aliases]
- def _get_macro_names(self) -> List[str]:
- """Return list of current macro names"""
- return list(self.macros)
+ def _get_macro_completion_items(self) -> List[CompletionItem]:
+ """Return list of current macro names and values as CompletionItems"""
+ return [CompletionItem(cur_key, self.macros[cur_key].value) for cur_key in self.macros]
- def _get_settable_names(self) -> List[str]:
- """Return list of current settable names"""
- return list(self.settable)
+ 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]
def _get_commands_aliases_and_macros_for_completion(self) -> List[str]:
"""Return a list of visible commands, aliases, and macros for tab completion"""
visible_commands = set(self.get_visible_commands())
- alias_names = set(self._get_alias_names())
- macro_names = set(self._get_macro_names())
+ alias_names = set(self.aliases)
+ macro_names = set(self.macros)
return list(visible_commands | alias_names | macro_names)
def get_help_topics(self) -> List[str]:
@@ -1938,8 +1926,6 @@ class Cmd(cmd.Cmd):
:param statement: the parsed statement from the command line
:return: the resolved macro or None on error
"""
- from itertools import islice
-
if statement.command not in self.macros.keys():
raise KeyError('{} is not a macro'.format(statement.command))
@@ -1972,7 +1958,7 @@ class Cmd(cmd.Cmd):
resolved = parts[0] + replacement + parts[1]
# Append extra arguments and use statement.arg_list since these arguments need their quotes preserved
- for arg in islice(statement.arg_list, macro.minimum_arg_count, None):
+ for arg in statement.arg_list[macro.minimum_arg_count:]:
resolved += ' ' + arg
# Restore any terminator, suffix, redirection, etc.
@@ -2348,7 +2334,7 @@ class Cmd(cmd.Cmd):
"An alias is a command that enables replacement of a word by another string.")
alias_epilog = ("See also:\n"
" macro")
- alias_parser = ACArgumentParser(description=alias_description, epilog=alias_epilog, prog='alias')
+ alias_parser = Cmd2ArgParser(description=alias_description, epilog=alias_epilog, prog='alias')
# Add sub-commands to alias
alias_subparsers = alias_parser.add_subparsers()
@@ -2373,11 +2359,10 @@ class Cmd(cmd.Cmd):
description=alias_create_description,
epilog=alias_create_epilog)
alias_create_parser.add_argument('name', help='name of this alias')
- setattr(alias_create_parser.add_argument('command', help='what the alias resolves to'),
- ACTION_ARG_CHOICES, _get_commands_aliases_and_macros_for_completion)
- setattr(alias_create_parser.add_argument('command_args', nargs=argparse.REMAINDER,
- help='arguments to pass to command'),
- ACTION_ARG_CHOICES, ('path_complete',))
+ alias_create_parser.add_argument('command', help='what the alias resolves to',
+ choices_method=_get_commands_aliases_and_macros_for_completion)
+ alias_create_parser.add_argument('command_args', nargs=argparse.REMAINDER, help='arguments to pass to command',
+ completer_method=path_complete)
alias_create_parser.set_defaults(func=_alias_create)
# alias -> delete
@@ -2385,8 +2370,8 @@ class Cmd(cmd.Cmd):
alias_delete_description = "Delete specified aliases or all aliases if --all is used"
alias_delete_parser = alias_subparsers.add_parser('delete', help=alias_delete_help,
description=alias_delete_description)
- setattr(alias_delete_parser.add_argument('name', nargs='*', help='alias to delete'),
- ACTION_ARG_CHOICES, _get_alias_names)
+ alias_delete_parser.add_argument('name', nargs=argparse.ZERO_OR_MORE, help='alias to delete',
+ choices_method=_get_alias_completion_items, descriptive_header='Value')
alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases")
alias_delete_parser.set_defaults(func=_alias_delete)
@@ -2399,8 +2384,8 @@ class Cmd(cmd.Cmd):
alias_list_parser = alias_subparsers.add_parser('list', help=alias_list_help,
description=alias_list_description)
- setattr(alias_list_parser.add_argument('name', nargs="*", help='alias to list'),
- ACTION_ARG_CHOICES, _get_alias_names)
+ alias_list_parser.add_argument('name', nargs=argparse.ZERO_OR_MORE, help='alias to list',
+ choices_method=_get_alias_completion_items, descriptive_header='Value')
alias_list_parser.set_defaults(func=_alias_list)
# Preserve quotes since we are passing strings to other commands
@@ -2530,7 +2515,7 @@ class Cmd(cmd.Cmd):
"A macro is similar to an alias, but it can contain argument placeholders.")
macro_epilog = ("See also:\n"
" alias")
- macro_parser = ACArgumentParser(description=macro_description, epilog=macro_epilog, prog='macro')
+ macro_parser = Cmd2ArgParser(description=macro_description, epilog=macro_epilog, prog='macro')
# Add sub-commands to macro
macro_subparsers = macro_parser.add_subparsers()
@@ -2578,11 +2563,10 @@ class Cmd(cmd.Cmd):
description=macro_create_description,
epilog=macro_create_epilog)
macro_create_parser.add_argument('name', help='name of this macro')
- setattr(macro_create_parser.add_argument('command', help='what the macro resolves to'),
- ACTION_ARG_CHOICES, _get_commands_aliases_and_macros_for_completion)
- setattr(macro_create_parser.add_argument('command_args', nargs=argparse.REMAINDER,
- help='arguments to pass to command'),
- ACTION_ARG_CHOICES, ('path_complete',))
+ macro_create_parser.add_argument('command', help='what the macro resolves to',
+ choices_method=_get_commands_aliases_and_macros_for_completion)
+ macro_create_parser.add_argument('command_args', nargs=argparse.REMAINDER,
+ help='arguments to pass to command', completer_method=path_complete)
macro_create_parser.set_defaults(func=_macro_create)
# macro -> delete
@@ -2590,8 +2574,8 @@ class Cmd(cmd.Cmd):
macro_delete_description = "Delete specified macros or all macros if --all is used"
macro_delete_parser = macro_subparsers.add_parser('delete', help=macro_delete_help,
description=macro_delete_description)
- setattr(macro_delete_parser.add_argument('name', nargs='*', help='macro to delete'),
- ACTION_ARG_CHOICES, _get_macro_names)
+ macro_delete_parser.add_argument('name', nargs=argparse.ZERO_OR_MORE, help='macro to delete',
+ choices_method=_get_macro_completion_items, descriptive_header='Value')
macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros")
macro_delete_parser.set_defaults(func=_macro_delete)
@@ -2603,8 +2587,8 @@ class Cmd(cmd.Cmd):
"Without arguments, all macros will be listed.")
macro_list_parser = macro_subparsers.add_parser('list', help=macro_list_help, description=macro_list_description)
- setattr(macro_list_parser.add_argument('name', nargs="*", help='macro to list'),
- ACTION_ARG_CHOICES, _get_macro_names)
+ macro_list_parser.add_argument('name', nargs=argparse.ZERO_OR_MORE, help='macro to list',
+ choices_method=_get_macro_completion_items, descriptive_header='Value')
macro_list_parser.set_defaults(func=_macro_list)
# Preserve quotes since we are passing strings to other commands
@@ -2626,7 +2610,7 @@ class Cmd(cmd.Cmd):
topics = set(self.get_help_topics())
visible_commands = set(self.get_visible_commands())
strs_to_match = list(topics | visible_commands)
- return self.basic_complete(text, line, begidx, endidx, strs_to_match)
+ return utils.basic_complete(text, line, begidx, endidx, strs_to_match)
def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""Completes the subcommand argument of help"""
@@ -2657,18 +2641,17 @@ class Cmd(cmd.Cmd):
# Check if this is a command with an argparse function
func = self.cmd_func(command)
if func and hasattr(func, 'argparser'):
+ from .argparse_completer import AutoCompleter
completer = AutoCompleter(getattr(func, 'argparser'), self)
matches = completer.complete_command_help(tokens[cmd_index:], text, line, begidx, endidx)
return matches
- help_parser = ACArgumentParser()
-
- setattr(help_parser.add_argument('command', help="command to retrieve help for", nargs="?"),
- ACTION_ARG_CHOICES, ('complete_help_command',))
- setattr(help_parser.add_argument('subcommand', help="sub-command to retrieve help for",
- nargs=argparse.REMAINDER),
- ACTION_ARG_CHOICES, ('complete_help_subcommand',))
+ help_parser = Cmd2ArgParser()
+ help_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to retrieve help for",
+ completer_method=complete_help_command)
+ help_parser.add_argument('subcommand', nargs=argparse.REMAINDER, help="sub-command to retrieve help for",
+ completer_method=complete_help_subcommand)
help_parser.add_argument('-v', '--verbose', action='store_true',
help="print a list of all commands with descriptions of each")
@@ -2689,6 +2672,7 @@ class Cmd(cmd.Cmd):
# If the command function uses argparse, then use argparse's help
if func and hasattr(func, 'argparser'):
+ from .argparse_completer import AutoCompleter
completer = AutoCompleter(getattr(func, 'argparser'), self)
tokens = [args.command] + args.subcommand
self.poutput(completer.format_help(tokens))
@@ -2827,19 +2811,19 @@ class Cmd(cmd.Cmd):
command = ''
self.stdout.write("\n")
- @with_argparser(ACArgumentParser())
+ @with_argparser(Cmd2ArgParser())
def do_shortcuts(self, _: argparse.Namespace) -> None:
"""List available shortcuts"""
result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.statement_parser.shortcuts))
self.poutput("Shortcuts for other commands:\n{}".format(result))
- @with_argparser(ACArgumentParser(epilog=INTERNAL_COMMAND_EPILOG))
+ @with_argparser(Cmd2ArgParser(epilog=INTERNAL_COMMAND_EPILOG))
def do_eof(self, _: argparse.Namespace) -> bool:
"""Called when <Ctrl>-D is pressed"""
# Return True to stop the command loop
return True
- @with_argparser(ACArgumentParser())
+ @with_argparser(Cmd2ArgParser())
def do_quit(self, _: argparse.Namespace) -> bool:
"""Exit this application"""
# Return True to stop the command loop
@@ -2934,12 +2918,12 @@ class Cmd(cmd.Cmd):
"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 = ACArgumentParser(description=set_description)
+ set_parser = Cmd2ArgParser(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')
- setattr(set_parser.add_argument('param', nargs='?', help='parameter to set or view'),
- ACTION_ARG_CHOICES, _get_settable_names)
- set_parser.add_argument('value', nargs='?', help='the new value for settable')
+ 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')
@with_argparser(set_parser)
def do_set(self, args: argparse.Namespace) -> None:
@@ -2979,12 +2963,10 @@ class Cmd(cmd.Cmd):
if onchange_hook is not None:
onchange_hook(old=orig_value, new=new_value)
- shell_parser = ACArgumentParser()
- setattr(shell_parser.add_argument('command', help='the command to run'),
- ACTION_ARG_CHOICES, ('shell_cmd_complete',))
- setattr(shell_parser.add_argument('command_args', nargs=argparse.REMAINDER,
- help='arguments to pass to command'),
- ACTION_ARG_CHOICES, ('path_complete',))
+ shell_parser = Cmd2ArgParser()
+ shell_parser.add_argument('command', help='the command to run', completer_method=shell_cmd_complete)
+ shell_parser.add_argument('command_args', nargs=argparse.REMAINDER, help='arguments to pass to command',
+ completer_method=path_complete)
# Preserve quotes since we are passing these strings to the shell
@with_argparser(shell_parser, preserve_quotes=True)
@@ -3044,9 +3026,9 @@ class Cmd(cmd.Cmd):
"If you see strange parsing behavior, it's best to just open the Python shell\n"
"by providing no arguments to py and run more complex statements there.")
- py_parser = ACArgumentParser(description=py_description)
- py_parser.add_argument('command', help="command to run", nargs='?')
- py_parser.add_argument('remainder', help="remainder of command", nargs=argparse.REMAINDER)
+ py_parser = Cmd2ArgParser(description=py_description)
+ py_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to run")
+ py_parser.add_argument('remainder', nargs=argparse.REMAINDER, help="remainder of command")
# Preserve quotes since we are passing these strings to Python
@with_argparser(py_parser, preserve_quotes=True)
@@ -3230,12 +3212,10 @@ class Cmd(cmd.Cmd):
return bridge.stop
- run_pyscript_parser = ACArgumentParser()
- setattr(run_pyscript_parser.add_argument('script_path', help='path to the script file'),
- ACTION_ARG_CHOICES, ('path_complete',))
- setattr(run_pyscript_parser.add_argument('script_arguments', nargs=argparse.REMAINDER,
- help='arguments to pass to script'),
- ACTION_ARG_CHOICES, ('path_complete',))
+ run_pyscript_parser = Cmd2ArgParser()
+ run_pyscript_parser.add_argument('script_path', help='path to the script file', completer_method=path_complete)
+ run_pyscript_parser.add_argument('script_arguments', nargs=argparse.REMAINDER,
+ help='arguments to pass to script', completer_method=path_complete)
@with_argparser(run_pyscript_parser)
def do_run_pyscript(self, args: argparse.Namespace) -> bool:
@@ -3265,7 +3245,7 @@ class Cmd(cmd.Cmd):
# Only include the do_ipy() method if IPython is available on the system
if ipython_available: # pragma: no cover
- @with_argparser(ACArgumentParser())
+ @with_argparser(Cmd2ArgParser())
def do_ipy(self, _: argparse.Namespace) -> None:
"""Enter an interactive IPython shell"""
from .pyscript_bridge import PyscriptBridge
@@ -3288,18 +3268,17 @@ class Cmd(cmd.Cmd):
history_description = "View, run, edit, save, or clear previously entered commands"
- history_parser = ACArgumentParser(description=history_description)
+ history_parser = Cmd2ArgParser(description=history_description)
history_action_group = history_parser.add_mutually_exclusive_group()
history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
history_action_group.add_argument('-e', '--edit', action='store_true',
help='edit and then run selected history items')
- setattr(history_action_group.add_argument('-o', '--output-file', metavar='FILE',
- help='output commands to a script file, implies -s'),
- ACTION_ARG_CHOICES, ('path_complete',))
- setattr(history_action_group.add_argument('-t', '--transcript',
- help='output commands and results to a transcript file,\n'
- 'implies -s'),
- ACTION_ARG_CHOICES, ('path_complete',))
+ history_action_group.add_argument('-o', '--output-file', metavar='FILE',
+ help='output commands to a script file, implies -s',
+ completer_method=path_complete)
+ history_action_group.add_argument('-t', '--transcript', help='output commands and results to a transcript file,\n'
+ 'implies -s',
+ completer_method=path_complete)
history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history')
history_format_group = history_parser.add_argument_group(title='formatting')
@@ -3321,7 +3300,7 @@ class Cmd(cmd.Cmd):
"a..b, a:b, a:, ..b items by indices (inclusive)\n"
"string items containing string\n"
"/regex/ items matching regular expression")
- history_parser.add_argument('arg', nargs='?', help=history_arg_help)
+ history_parser.add_argument('arg', nargs=argparse.OPTIONAL, help=history_arg_help)
@with_argparser(history_parser)
def do_history(self, args: argparse.Namespace) -> Optional[bool]:
@@ -3588,9 +3567,9 @@ class Cmd(cmd.Cmd):
"\n"
" set editor (program-name)")
- edit_parser = ACArgumentParser(description=edit_description)
- setattr(edit_parser.add_argument('file_path', help="path to a file to open in editor", nargs="?"),
- ACTION_ARG_CHOICES, ('path_complete',))
+ edit_parser = Cmd2ArgParser(description=edit_description)
+ edit_parser.add_argument('file_path', nargs=argparse.OPTIONAL,
+ help="path to a file to open in editor", completer_method=path_complete)
@with_argparser(edit_parser)
def do_edit(self, args: argparse.Namespace) -> None:
@@ -3618,15 +3597,12 @@ class Cmd(cmd.Cmd):
"typed in the console.\n"
"\n"
"If the -r/--record_transcript flag is used, this command instead records\n"
- "the output of the script commands to a transcript for testing purposes.\n"
- )
+ "the output of the script commands to a transcript for testing purposes.\n")
- run_script_parser = ACArgumentParser(description=run_script_description)
- setattr(run_script_parser.add_argument('-t', '--transcript',
- help='record the output of the script as a transcript file'),
- ACTION_ARG_CHOICES, ('path_complete',))
- setattr(run_script_parser.add_argument('script_path', help="path to the script file"),
- ACTION_ARG_CHOICES, ('path_complete',))
+ run_script_parser = Cmd2ArgParser(description=run_script_description)
+ run_script_parser.add_argument('-t', '--transcript', help='record the output of the script as a transcript file',
+ completer_method=path_complete)
+ run_script_parser.add_argument('script_path', help="path to the script file", completer_method=path_complete)
@with_argparser(run_script_parser)
def do_run_script(self, args: argparse.Namespace) -> Optional[bool]:
@@ -3689,8 +3665,8 @@ class Cmd(cmd.Cmd):
relative_run_script_epilog = ("Notes:\n"
" This command is intended to only be used within text file scripts.")
- relative_run_script_parser = ACArgumentParser(description=relative_run_script_description,
- epilog=relative_run_script_epilog)
+ relative_run_script_parser = Cmd2ArgParser(description=relative_run_script_description,
+ epilog=relative_run_script_epilog)
relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script')
@with_argparser(relative_run_script_parser)
@@ -3955,7 +3931,8 @@ class Cmd(cmd.Cmd):
self.disable_command(cmd_name, message_to_print)
# noinspection PyUnusedLocal
- def _report_disabled_command_usage(self, *args, message_to_print: str, **kwargs) -> None:
+ @staticmethod
+ def _report_disabled_command_usage(*args, message_to_print: str, **kwargs) -> None:
"""
Report when a disabled command has been run or had help called on it
:param args: not used
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 872c2192..7f357a6c 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -616,3 +616,19 @@ class RedirectionSavedState(object):
# If the command created a process to pipe to, then then is its reader
self.pipe_proc_reader = None
+
+
+# noinspection PyUnusedLocal
+def basic_complete(text: str, line: str, begidx: int, endidx: int, match_against: Iterable) -> List[str]:
+ """
+ Basic tab completion function that matches against a list of strings without considering line contents
+ or cursor position. The args required by this function are defined in the header of Pythons's cmd.py.
+
+ :param text: the string prefix we are attempting to match (all returned matches must begin with it)
+ :param line: the current input line with leading whitespace removed
+ :param begidx: the beginning index of the prefix text
+ :param endidx: the ending index of the prefix text
+ :param match_against: the strings being matched against
+ :return: a list of possible tab completions
+ """
+ return [cur_match for cur_match in match_against if cur_match.startswith(text)]
diff --git a/examples/subcommands.py b/examples/subcommands.py
index d1b7c9db..89bcaf85 100755
--- a/examples/subcommands.py
+++ b/examples/subcommands.py
@@ -33,8 +33,7 @@ bar_subparsers.add_parser('cranberries', help='cranberries help')
# create the parser for the "sport" sub-command
parser_sport = base_subparsers.add_parser('sport', help='sport help')
-sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport')
-setattr(sport_arg, 'arg_choices', sport_item_strs)
+sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs)
# create the top-level parser for the alternate command
@@ -60,8 +59,7 @@ bar2_subparsers.add_parser('cranberries', help='cranberries help')
# create the parser for the "sport" sub-command
parser_sport2 = base2_subparsers.add_parser('sport', help='sport help')
-sport2_arg = parser_sport2.add_argument('sport', help='Enter name of a sport')
-setattr(sport2_arg, 'arg_choices', sport_item_strs)
+sport2_arg = parser_sport2.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs)
class SubcommandsExample(cmd2.Cmd):
diff --git a/examples/tab_autocomp_dynamic.py b/examples/tab_autocomp_dynamic.py
deleted file mode 100755
index b518c013..00000000
--- a/examples/tab_autocomp_dynamic.py
+++ /dev/null
@@ -1,234 +0,0 @@
-#!/usr/bin/env python3
-# coding=utf-8
-"""
-A example usage of AutoCompleter with delayed initialization of the argparse object
-"""
-from typing import List
-
-import cmd2
-from cmd2 import argparse_completer, utils
-
-actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew',
- 'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac',
- 'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman',
- 'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee']
-
-
-def query_actors() -> List[str]:
- """Simulating a function that queries and returns a completion values"""
- return actors
-
-
-class TabCompleteExample(cmd2.Cmd):
- """ Example cmd2 application where we a base command which has a couple sub-commands."""
-
- CAT_AUTOCOMPLETE = 'AutoComplete Examples'
-
- def __init__(self):
- super().__init__()
-
- video_types_subparsers = TabCompleteExample.video_parser.add_subparsers(title='Media Types', dest='type')
-
- vid_movies_parser = argparse_completer.ACArgumentParser(prog='movies')
- vid_movies_parser.set_defaults(func=TabCompleteExample._do_vid_media_movies)
-
- vid_movies_commands_subparsers = vid_movies_parser.add_subparsers(title='Commands', dest='command')
-
- vid_movies_list_parser = vid_movies_commands_subparsers.add_parser('list')
-
- vid_movies_list_parser.add_argument('-t', '--title', help='Title Filter')
- vid_movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+',
- choices=TabCompleteExample.ratings_types)
- # save a reference to the action object
- director_action = vid_movies_list_parser.add_argument('-d', '--director', help='Director Filter')
- actor_action = vid_movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append')
-
- # tag the action objects with completion providers. This can be a collection or a callable
- setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.static_list_directors)
- setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, query_actors)
-
- vid_movies_add_parser = vid_movies_commands_subparsers.add_parser('add')
- vid_movies_add_parser.add_argument('title', help='Movie Title')
- vid_movies_add_parser.add_argument('rating', help='Movie Rating', choices=TabCompleteExample.ratings_types)
-
- # save a reference to the action object
- director_action = vid_movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2),
- required=True)
- actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*')
-
- vid_movies_load_parser = vid_movies_commands_subparsers.add_parser('load')
- vid_movie_file_action = vid_movies_load_parser.add_argument('movie_file', help='Movie database')
-
- vid_movies_read_parser = vid_movies_commands_subparsers.add_parser('read')
- vid_movie_fread_action = vid_movies_read_parser.add_argument('movie_file', help='Movie database')
-
- # tag the action objects with completion providers. This can be a collection or a callable
- setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.static_list_directors)
- setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors')
-
- # tag the file property with a custom completion function 'delimiter_complete' provided by cmd2.
- setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES,
- ('delimiter_complete',
- {'delimiter': '/',
- 'match_against': TabCompleteExample.file_list}))
- setattr(vid_movie_fread_action, argparse_completer.ACTION_ARG_CHOICES,
- ('path_complete',))
-
- vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete')
- vid_delete_movie_id = vid_movies_delete_parser.add_argument('movie_id', help='Movie ID')
- setattr(vid_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.instance_query_movie_ids)
- setattr(vid_delete_movie_id, argparse_completer.ACTION_DESCRIPTIVE_COMPLETION_HEADER, 'Title')
-
- # Add the 'movies' parser as a parent of sub-parser
- video_types_subparsers.add_parser('movies', parents=[vid_movies_parser], add_help=False)
-
- vid_shows_parser = argparse_completer.ACArgumentParser(prog='shows')
- vid_shows_parser.set_defaults(func=TabCompleteExample._do_vid_media_shows)
-
- vid_shows_commands_subparsers = vid_shows_parser.add_subparsers(title='Commands', dest='command')
-
- vid_shows_commands_subparsers.add_parser('list')
-
- video_types_subparsers.add_parser('shows', parents=[vid_shows_parser], add_help=False)
-
- # For mocking a data source for the example commands
- ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17']
- show_ratings = ['TV-Y', 'TV-Y7', 'TV-G', 'TV-PG', 'TV-14', 'TV-MA']
- static_list_directors = ['J. J. Abrams', 'Irvin Kershner', 'George Lucas', 'Richard Marquand',
- 'Rian Johnson', 'Gareth Edwards']
- USER_MOVIE_LIBRARY = ['ROGUE1', 'SW_EP04', 'SW_EP05']
- MOVIE_DATABASE_IDS = ['SW_EP1', 'SW_EP02', 'SW_EP03', 'ROGUE1', 'SW_EP04',
- 'SW_EP05', 'SW_EP06', 'SW_EP07', 'SW_EP08', 'SW_EP09']
- MOVIE_DATABASE = {'SW_EP04': {'title': 'Star Wars: Episode IV - A New Hope',
- 'rating': 'PG',
- 'director': ['George Lucas'],
- 'actor': ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher',
- 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels']
- },
- 'SW_EP05': {'title': 'Star Wars: Episode V - The Empire Strikes Back',
- 'rating': 'PG',
- 'director': ['Irvin Kershner'],
- 'actor': ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher',
- 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels']
- },
- 'SW_EP06': {'title': 'Star Wars: Episode VI - Return of the Jedi',
- 'rating': 'PG',
- 'director': ['Richard Marquand'],
- 'actor': ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher',
- 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels']
- },
- 'SW_EP1': {'title': 'Star Wars: Episode I - The Phantom Menace',
- 'rating': 'PG',
- 'director': ['George Lucas'],
- 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman', 'Jake Lloyd']
- },
- 'SW_EP02': {'title': 'Star Wars: Episode II - Attack of the Clones',
- 'rating': 'PG',
- 'director': ['George Lucas'],
- 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman',
- 'Hayden Christensen', 'Christopher Lee']
- },
- 'SW_EP03': {'title': 'Star Wars: Episode III - Revenge of the Sith',
- 'rating': 'PG-13',
- 'director': ['George Lucas'],
- 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman',
- 'Hayden Christensen']
- },
-
- }
- USER_SHOW_LIBRARY = {'SW_REB': ['S01E01', 'S02E02']}
- SHOW_DATABASE_IDS = ['SW_CW', 'SW_TCW', 'SW_REB']
- SHOW_DATABASE = {'SW_CW': {'title': 'Star Wars: Clone Wars',
- 'rating': 'TV-Y7',
- 'seasons': {1: ['S01E01', 'S01E02', 'S01E03'],
- 2: ['S02E01', 'S02E02', 'S02E03']}
- },
- 'SW_TCW': {'title': 'Star Wars: The Clone Wars',
- 'rating': 'TV-PG',
- 'seasons': {1: ['S01E01', 'S01E02', 'S01E03'],
- 2: ['S02E01', 'S02E02', 'S02E03']}
- },
- 'SW_REB': {'title': 'Star Wars: Rebels',
- 'rating': 'TV-Y7',
- 'seasons': {1: ['S01E01', 'S01E02', 'S01E03'],
- 2: ['S02E01', 'S02E02', 'S02E03']}
- },
- }
-
- file_list = \
- [
- '/home/user/file.db',
- '/home/user/file space.db',
- '/home/user/another.db',
- '/home/other user/maps.db',
- '/home/other user/tests.db'
- ]
-
- def instance_query_actors(self) -> List[str]:
- """Simulating a function that queries and returns a completion values"""
- return actors
-
- def instance_query_movie_ids(self) -> List[str]:
- """Demonstrates showing tabular hinting of tab completion information"""
- completions_with_desc = []
-
- # Sort the movie id strings with a natural sort since they contain numbers
- for movie_id in utils.natural_sort(self.MOVIE_DATABASE_IDS):
- if movie_id in self.MOVIE_DATABASE:
- movie_entry = self.MOVIE_DATABASE[movie_id]
- completions_with_desc.append(argparse_completer.CompletionItem(movie_id, movie_entry['title']))
-
- # Mark that we already sorted the matches
- self.matches_sorted = True
- return completions_with_desc
-
- ###################################################################################
- # The media command demonstrates a completer with multiple layers of subcommands
- # - This example demonstrates how to tag a completion attribute on each action, enabling argument
- # completion without implementing a complete_COMMAND function
- def _do_vid_media_movies(self, args) -> None:
- if not args.command:
- self.do_help('video movies')
- elif args.command == 'list':
- for movie_id in TabCompleteExample.MOVIE_DATABASE:
- movie = TabCompleteExample.MOVIE_DATABASE[movie_id]
- print('{}\n-----------------------------\n{} ID: {}\nDirector: {}\nCast:\n {}\n\n'
- .format(movie['title'], movie['rating'], movie_id,
- ', '.join(movie['director']),
- '\n '.join(movie['actor'])))
-
- def _do_vid_media_shows(self, args) -> None:
- if not args.command:
- self.do_help('video shows')
-
- elif args.command == 'list':
- for show_id in TabCompleteExample.SHOW_DATABASE:
- show = TabCompleteExample.SHOW_DATABASE[show_id]
- print('{}\n-----------------------------\n{} ID: {}'
- .format(show['title'], show['rating'], show_id))
- for season in show['seasons']:
- ep_list = show['seasons'][season]
- print(' Season {}:\n {}'
- .format(season,
- '\n '.join(ep_list)))
- print()
-
- video_parser = argparse_completer.ACArgumentParser(prog='video')
-
- @cmd2.with_category(CAT_AUTOCOMPLETE)
- @cmd2.with_argparser(video_parser)
- def do_video(self, args):
- """Video management command demonstrates multiple layers of sub-commands being handled by AutoCompleter"""
- func = getattr(args, 'func', None)
- if func is not None:
- # Call whatever subcommand function was selected
- func(self, args)
- else:
- # No subcommand was provided, so call help
- self.do_help('video')
-
-
-if __name__ == '__main__':
- import sys
- app = TabCompleteExample()
- sys.exit(app.cmdloop())
diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py
index 8f27cb90..c4fc6218 100755
--- a/examples/tab_autocompletion.py
+++ b/examples/tab_autocompletion.py
@@ -4,11 +4,11 @@
A example usage of the AutoCompleter
"""
import argparse
-import itertools
+import functools
from typing import List
import cmd2
-from cmd2 import argparse_completer, utils
+from cmd2 import utils, Cmd2ArgParser, CompletionItem
actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew',
'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac',
@@ -102,6 +102,7 @@ class TabCompleteExample(cmd2.Cmd):
'/home/other user/tests.db'
]
+ # noinspection PyMethodMayBeStatic
def instance_query_actors(self) -> List[str]:
"""Simulating a function that queries and returns a completion values"""
return actors
@@ -114,7 +115,7 @@ class TabCompleteExample(cmd2.Cmd):
for movie_id in utils.natural_sort(self.MOVIE_DATABASE_IDS):
if movie_id in self.MOVIE_DATABASE:
movie_entry = self.MOVIE_DATABASE[movie_id]
- completions_with_desc.append(argparse_completer.CompletionItem(movie_id, movie_entry['title']))
+ completions_with_desc.append(CompletionItem(movie_id, movie_entry['title']))
# Mark that we already sorted the matches
self.matches_sorted = True
@@ -123,11 +124,11 @@ class TabCompleteExample(cmd2.Cmd):
# This demonstrates a number of customizations of the AutoCompleter version of ArgumentParser
# - The help output will separately group required vs optional flags
# - The help output for arguments with multiple flags or with append=True is more concise
- # - ACArgumentParser adds the ability to specify ranges of argument counts in 'nargs'
+ # - cmd2 adds the ability to specify ranges of argument counts in 'nargs'
suggest_description = "Suggest command demonstrates argparse customizations.\n"
suggest_description += "See hybrid_suggest and orig_suggest to compare the help output."
- suggest_parser = argparse_completer.ACArgumentParser(description=suggest_description)
+ suggest_parser = Cmd2ArgParser(description=suggest_description)
suggest_parser.add_argument('-t', '--type', choices=['movie', 'show'], required=True)
suggest_parser.add_argument('-d', '--duration', nargs=(1, 2), action='append',
@@ -146,9 +147,6 @@ class TabCompleteExample(cmd2.Cmd):
# to enable narg ranges without the help changes using this method
suggest_parser_hybrid = argparse.ArgumentParser()
- # This registers the custom narg range handling
- argparse_completer.register_custom_actions(suggest_parser_hybrid)
-
suggest_parser_hybrid.add_argument('-t', '--type', choices=['movie', 'show'], required=True)
suggest_parser_hybrid.add_argument('-d', '--duration', nargs=(1, 2), action='append',
help='Duration constraint in minutes.\n'
@@ -179,14 +177,9 @@ class TabCompleteExample(cmd2.Cmd):
if not args.type:
self.do_help('orig_suggest')
- ###################################################################################
- # The media command demonstrates a completer with multiple layers of subcommands
- # - This example demonstrates how to tag a completion attribute on each action, enabling argument
- # completion without implementing a complete_COMMAND function
-
- def _do_vid_media_movies(self, args) -> None:
+ def _do_vid_movies(self, args) -> None:
if not args.command:
- self.do_help('media movies')
+ self.do_help('video movies')
elif args.command == 'list':
for movie_id in TabCompleteExample.MOVIE_DATABASE:
movie = TabCompleteExample.MOVIE_DATABASE[movie_id]
@@ -195,9 +188,9 @@ class TabCompleteExample(cmd2.Cmd):
', '.join(movie['director']),
'\n '.join(movie['actor'])))
- def _do_vid_media_shows(self, args) -> None:
+ def _do_vid_shows(self, args) -> None:
if not args.command:
- self.do_help('media shows')
+ self.do_help('video shows')
elif args.command == 'list':
for show_id in TabCompleteExample.SHOW_DATABASE:
@@ -211,12 +204,12 @@ class TabCompleteExample(cmd2.Cmd):
'\n '.join(ep_list)))
print()
- video_parser = argparse_completer.ACArgumentParser(prog='media')
+ video_parser = Cmd2ArgParser(prog='media')
video_types_subparsers = video_parser.add_subparsers(title='Media Types', dest='type')
vid_movies_parser = video_types_subparsers.add_parser('movies')
- vid_movies_parser.set_defaults(func=_do_vid_media_movies)
+ vid_movies_parser.set_defaults(func=_do_vid_movies)
vid_movies_commands_subparsers = vid_movies_parser.add_subparsers(title='Commands', dest='command')
@@ -225,48 +218,32 @@ class TabCompleteExample(cmd2.Cmd):
vid_movies_list_parser.add_argument('-t', '--title', help='Title Filter')
vid_movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+',
choices=ratings_types)
- # save a reference to the action object
- director_action = vid_movies_list_parser.add_argument('-d', '--director', help='Director Filter')
- actor_action = vid_movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append')
-
- # tag the action objects with completion providers. This can be a collection or a callable
- setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors)
- setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, query_actors)
+ vid_movies_list_parser.add_argument('-d', '--director', help='Director Filter', choices=static_list_directors)
+ vid_movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append',
+ choices_function=query_actors)
vid_movies_add_parser = vid_movies_commands_subparsers.add_parser('add')
vid_movies_add_parser.add_argument('title', help='Movie Title')
vid_movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types)
- # save a reference to the action object
- director_action = vid_movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2),
- required=True)
- actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*')
+ vid_movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True,
+ choices=static_list_directors)
+ vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*', choices_method=instance_query_actors)
vid_movies_load_parser = vid_movies_commands_subparsers.add_parser('load')
- vid_movie_file_action = vid_movies_load_parser.add_argument('movie_file', help='Movie database')
+ vid_movies_load_parser.add_argument('movie_file', help='Movie database',
+ completer_method=functools.partial(cmd2.Cmd.delimiter_complete,
+ delimiter='/', match_against=file_list))
vid_movies_read_parser = vid_movies_commands_subparsers.add_parser('read')
- vid_movie_fread_action = vid_movies_read_parser.add_argument('movie_file', help='Movie database')
-
- # tag the action objects with completion providers. This can be a collection or a callable
- setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors)
- setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors')
-
- # tag the file property with a custom completion function 'delimiter_complete' provided by cmd2.
- setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES,
- ('delimiter_complete',
- {'delimiter': '/',
- 'match_against': file_list}))
- setattr(vid_movie_fread_action, argparse_completer.ACTION_ARG_CHOICES,
- ('path_complete',))
+ vid_movies_read_parser.add_argument('movie_file', help='Movie database', completer_method=cmd2.Cmd.path_complete)
vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete')
- vid_delete_movie_id = vid_movies_delete_parser.add_argument('movie_id', help='Movie ID')
- setattr(vid_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, instance_query_movie_ids)
- setattr(vid_delete_movie_id, argparse_completer.ACTION_DESCRIPTIVE_COMPLETION_HEADER, 'Title')
+ vid_movies_delete_parser.add_argument('movie_id', help='Movie ID', choices_method=instance_query_movie_ids,
+ descriptive_header='Title')
vid_shows_parser = video_types_subparsers.add_parser('shows')
- vid_shows_parser.set_defaults(func=_do_vid_media_shows)
+ vid_shows_parser.set_defaults(func=_do_vid_shows)
vid_shows_commands_subparsers = vid_shows_parser.add_subparsers(title='Commands', dest='command')
@@ -284,257 +261,6 @@ class TabCompleteExample(cmd2.Cmd):
# No subcommand was provided, so call help
self.do_help('video')
- ###################################################################################
- # The media command demonstrates a completer with multiple layers of subcommands
- # - This example uses a flat completion lookup dictionary
-
- def _do_media_movies(self, args) -> None:
- if not args.command:
- self.do_help('media movies')
- elif args.command == 'list':
- for movie_id in TabCompleteExample.MOVIE_DATABASE:
- movie = TabCompleteExample.MOVIE_DATABASE[movie_id]
- print('{}\n-----------------------------\n{} ID: {}\nDirector: {}\nCast:\n {}\n\n'
- .format(movie['title'], movie['rating'], movie_id,
- ', '.join(movie['director']),
- '\n '.join(movie['actor'])))
- elif args.command == 'add':
- print('Adding Movie\n----------------\nTitle: {}\nRating: {}\nDirectors: {}\nActors: {}\n\n'
- .format(args.title, args.rating, ', '.join(args.director), ', '.join(args.actor)))
-
- def _do_media_shows(self, args) -> None:
- if not args.command:
- self.do_help('media shows')
-
- elif args.command == 'list':
- for show_id in TabCompleteExample.SHOW_DATABASE:
- show = TabCompleteExample.SHOW_DATABASE[show_id]
- print('{}\n-----------------------------\n{} ID: {}'
- .format(show['title'], show['rating'], show_id))
- for season in show['seasons']:
- ep_list = show['seasons'][season]
- print(' Season {}:\n {}'
- .format(season,
- '\n '.join(ep_list)))
- print()
-
- media_parser = argparse_completer.ACArgumentParser(prog='media')
-
- media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type')
-
- movies_parser = media_types_subparsers.add_parser('movies')
- movies_parser.set_defaults(func=_do_media_movies)
-
- movies_commands_subparsers = movies_parser.add_subparsers(title='Commands', dest='command')
-
- movies_list_parser = movies_commands_subparsers.add_parser('list')
-
- movies_list_parser.add_argument('-t', '--title', help='Title Filter')
- movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+',
- choices=ratings_types)
- movies_list_parser.add_argument('-d', '--director', help='Director Filter')
- movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append')
-
- movies_add_parser = movies_commands_subparsers.add_parser('add')
- movies_add_parser.add_argument('title', help='Movie Title')
- movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types)
- movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True)
- movies_add_parser.add_argument('actor', help='Actors', nargs=argparse.REMAINDER)
-
- movies_delete_parser = movies_commands_subparsers.add_parser('delete')
- movies_delete_movie_id = movies_delete_parser.add_argument('movie_id', help='Movie ID')
- setattr(movies_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_movie_ids')
- setattr(movies_delete_movie_id, argparse_completer.ACTION_DESCRIPTIVE_COMPLETION_HEADER, 'Title')
-
- movies_load_parser = movies_commands_subparsers.add_parser('load')
- movie_file_action = movies_load_parser.add_argument('movie_file', help='Movie database')
-
- shows_parser = media_types_subparsers.add_parser('shows')
- shows_parser.set_defaults(func=_do_media_shows)
-
- shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command')
-
- shows_list_parser = shows_commands_subparsers.add_parser('list')
-
- @cmd2.with_category(CAT_AUTOCOMPLETE)
- @cmd2.with_argparser(media_parser)
- def do_media(self, args):
- """Media management command demonstrates multiple layers of sub-commands being handled by AutoCompleter"""
- func = getattr(args, 'func', None)
- if func is not None:
- # Call whatever subcommand function was selected
- func(self, args)
- else:
- # No subcommand was provided, so call help
- self.do_help('media')
-
- # This completer is implemented using a single dictionary to look up completion lists for all layers of
- # subcommands. For each argument, AutoCompleter will search for completion values from the provided
- # arg_choices dict. This requires careful naming of argparse arguments so that there are no unintentional
- # name collisions.
- def complete_media(self, text, line, begidx, endidx):
- """ Adds tab completion to media"""
- choices = {'actor': query_actors, # function
- 'director': TabCompleteExample.static_list_directors, # static list
- 'movie_file': (self.path_complete,)
- }
- completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser,
- self,
- arg_choices=choices)
-
- tokens, _ = self.tokens_for_completion(line, begidx, endidx)
- results = completer.complete_command(tokens, text, line, begidx, endidx)
-
- return results
-
- ###################################################################################
- # The library command demonstrates a completer with multiple layers of subcommands
- # with different completion results per sub-command
- # - This demonstrates how to build a tree of completion lookups to pass down
- #
- # Only use this method if you absolutely need to as it dramatically
- # increases the complexity and decreases readability.
-
- def _do_library_movie(self, args):
- if not args.type or not args.command:
- self.do_help('library movie')
-
- def _do_library_show(self, args):
- if not args.type:
- self.do_help('library show')
-
- def _query_movie_database(self):
- return list(set(TabCompleteExample.MOVIE_DATABASE_IDS).difference(set(TabCompleteExample.USER_MOVIE_LIBRARY)))
-
- def _query_movie_user_library(self):
- return TabCompleteExample.USER_MOVIE_LIBRARY
-
- def _filter_library(self, text, line, begidx, endidx, full, exclude=()):
- candidates = list(set(full).difference(set(exclude)))
- return [entry for entry in candidates if entry.startswith(text)]
-
- library_parser = argparse_completer.ACArgumentParser(prog='library')
-
- library_subcommands = library_parser.add_subparsers(title='Media Types', dest='type')
-
- library_movie_parser = library_subcommands.add_parser('movie')
- library_movie_parser.set_defaults(func=_do_library_movie)
-
- library_movie_subcommands = library_movie_parser.add_subparsers(title='Command', dest='command')
-
- library_movie_add_parser = library_movie_subcommands.add_parser('add')
- library_movie_add_parser.add_argument('movie_id', help='ID of movie to add', action='append')
- library_movie_add_parser.add_argument('-b', '--borrowed', action='store_true')
-
- library_movie_remove_parser = library_movie_subcommands.add_parser('remove')
- library_movie_remove_parser.add_argument('movie_id', help='ID of movie to remove', action='append')
-
- library_show_parser = library_subcommands.add_parser('show')
- library_show_parser.set_defaults(func=_do_library_show)
-
- library_show_subcommands = library_show_parser.add_subparsers(title='Command', dest='command')
-
- library_show_add_parser = library_show_subcommands.add_parser('add')
- library_show_add_parser.add_argument('show_id', help='Show IDs to add')
- library_show_add_parser.add_argument('episode_id', nargs='*', help='Show IDs to add')
-
- library_show_rmv_parser = library_show_subcommands.add_parser('remove')
-
- # Demonstrates a custom completion function that does more with the command line than is
- # allowed by the standard completion functions
- def _filter_episodes(self, text, line, begidx, endidx, show_db, user_lib):
- tokens, _ = self.tokens_for_completion(line, begidx, endidx)
- show_id = tokens[3]
- if show_id:
- if show_id in show_db:
- show = show_db[show_id]
- all_episodes = itertools.chain(*(show['seasons'].values()))
-
- if show_id in user_lib:
- user_eps = user_lib[show_id]
- else:
- user_eps = []
-
- return self._filter_library(text, line, begidx, endidx, all_episodes, user_eps)
- return []
-
- @cmd2.with_category(CAT_AUTOCOMPLETE)
- @cmd2.with_argparser(library_parser)
- def do_library(self, args):
- """Media management command demonstrates multiple layers of sub-commands being handled by AutoCompleter"""
- func = getattr(args, 'func', None)
- if func is not None:
- # Call whatever subcommand function was selected
- func(self, args)
- else:
- # No subcommand was provided, so call help
- self.do_help('library')
-
- def complete_library(self, text, line, begidx, endidx):
-
- # this demonstrates the much more complicated scenario of having
- # unique completion parameters per sub-command that use the same
- # argument name. To do this we build a multi-layer nested tree
- # of lookups far AutoCompleter to traverse. This nested tree must
- # match the structure of the argparse parser
- #
-
- movie_add_choices = {'movie_id': self._query_movie_database}
- movie_remove_choices = {'movie_id': self._query_movie_user_library}
-
- # This demonstrates the ability to mix custom completion functions with argparse completion.
- # By specifying a tuple for a completer, AutoCompleter expects a custom completion function
- # with optional index-based as well as keyword based arguments. This is an alternative to using
- # a partial function.
-
- show_add_choices = {'show_id': (self._filter_library, # This is a custom completion function
- # This tuple represents index-based args to append to the function call
- (list(TabCompleteExample.SHOW_DATABASE.keys()),)
- ),
- 'episode_id': (self._filter_episodes, # this is a custom completion function
- # this list represents index-based args to append to the function call
- [TabCompleteExample.SHOW_DATABASE],
- # this dict contains keyword-based args to append to the function call
- {'user_lib': TabCompleteExample.USER_SHOW_LIBRARY})}
- show_remove_choices = {}
-
- # The library movie sub-parser group 'command' has 2 sub-parsers:
- # 'add' and 'remove'
- library_movie_command_params = \
- {'add': (movie_add_choices, None),
- 'remove': (movie_remove_choices, None)}
-
- library_show_command_params = \
- {'add': (show_add_choices, None),
- 'remove': (show_remove_choices, None)}
-
- # The 'library movie' command has a sub-parser group called 'command'
- library_movie_subcommand_groups = {'command': library_movie_command_params}
- library_show_subcommand_groups = {'command': library_show_command_params}
-
- # Mapping of a specific sub-parser of the 'type' group to a tuple. Each
- # tuple has 2 values corresponding what's passed to the constructor
- # parameters (arg_choices,subcmd_args_lookup) of the nested
- # instance of AutoCompleter
- library_type_params = {'movie': (None, library_movie_subcommand_groups),
- 'show': (None, library_show_subcommand_groups)}
-
- # maps the a subcommand group to a dictionary mapping a specific
- # sub-command to a tuple of (arg_choices, subcmd_args_lookup)
- #
- # In this example, 'library_parser' has a sub-parser group called 'type'
- # under the type sub-parser group, there are 2 sub-parsers: 'movie', 'show'
- library_subcommand_groups = {'type': library_type_params}
-
- completer = argparse_completer.AutoCompleter(TabCompleteExample.library_parser,
- self,
- subcmd_args_lookup=library_subcommand_groups)
-
- tokens, _ = self.tokens_for_completion(line, begidx, endidx)
- results = completer.complete_command(tokens, text, line, begidx, endidx)
-
- return results
-
if __name__ == '__main__':
import sys
diff --git a/examples/tab_completion.py b/examples/tab_completion.py
index 48d7cb05..2830b002 100755
--- a/examples/tab_completion.py
+++ b/examples/tab_completion.py
@@ -1,6 +1,8 @@
#!/usr/bin/env python
# coding=utf-8
-"""A simple example demonstrating how to use flag and index based tab-completion functions
+"""
+A simple example demonstrating how to use flag and index based tab-completion functions
+For argparse-based tab completion, see tab_autocompletion.py
"""
import argparse
diff --git a/examples/table_display.py b/examples/table_display.py
index cedd2ca0..54d5b7a4 100755
--- a/examples/table_display.py
+++ b/examples/table_display.py
@@ -15,6 +15,7 @@ and either the colored or colorama module
from typing import Tuple
import cmd2
+from cmd2.argparse_custom import Cmd2ArgParser
import tableformatter as tf
# Configure colors for when users chooses the "-c" flag to enable color in the table output
@@ -142,14 +143,14 @@ def high_density_objs(row_obj: CityInfo) -> dict:
return opts
-def make_table_parser() -> cmd2.argparse_completer.ACArgumentParser:
+def make_table_parser() -> Cmd2ArgParser:
"""Create a unique instance of an argparse Argument parser for processing table arguments.
NOTE: The two cmd2 argparse decorators require that each parser be unique, even if they are essentially a deep copy
of each other. For cases like that, you can create a function to return a unique instance of a parser, which is
what is being done here.
"""
- table_parser = cmd2.argparse_completer.ACArgumentParser()
+ table_parser = Cmd2ArgParser()
table_item_group = table_parser.add_mutually_exclusive_group()
table_item_group.add_argument('-c', '--color', action='store_true', help='Enable color')
table_item_group.add_argument('-f', '--fancy', action='store_true', help='Fancy Grid')
diff --git a/tests/test_acargparse.py b/tests/test_acargparse.py
deleted file mode 100644
index 436158db..00000000
--- a/tests/test_acargparse.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# flake8: noqa E302
-"""
-Unit/functional testing for argparse customizations in cmd2
-"""
-import pytest
-from cmd2.argparse_completer import ACArgumentParser, is_potential_flag
-
-
-def test_acarg_narg_empty_tuple():
- with pytest.raises(ValueError) as excinfo:
- parser = ACArgumentParser(prog='test')
- parser.add_argument('invalid_tuple', nargs=())
- assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
-
-
-def test_acarg_narg_single_tuple():
- with pytest.raises(ValueError) as excinfo:
- parser = ACArgumentParser(prog='test')
- parser.add_argument('invalid_tuple', nargs=(1,))
- assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
-
-
-def test_acarg_narg_tuple_triple():
- with pytest.raises(ValueError) as excinfo:
- parser = ACArgumentParser(prog='test')
- parser.add_argument('invalid_tuple', nargs=(1, 2, 3))
- assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
-
-
-def test_acarg_narg_tuple_order():
- with pytest.raises(ValueError) as excinfo:
- parser = ACArgumentParser(prog='test')
- parser.add_argument('invalid_tuple', nargs=(2, 1))
- assert 'Invalid nargs range. The first value must be less than the second' in str(excinfo.value)
-
-
-def test_acarg_narg_tuple_negative():
- with pytest.raises(ValueError) as excinfo:
- parser = ACArgumentParser(prog='test')
- parser.add_argument('invalid_tuple', nargs=(-1, 1))
- assert 'Negative numbers are invalid for nargs range' in str(excinfo.value)
-
-
-def test_acarg_narg_tuple_zero_base():
- parser = ACArgumentParser(prog='test')
- parser.add_argument('tuple', nargs=(0, 3))
-
-
-def test_acarg_narg_tuple_zero_to_one():
- parser = ACArgumentParser(prog='test')
- parser.add_argument('tuple', nargs=(0, 1))
-
-
-def test_is_potential_flag():
- parser = ACArgumentParser()
-
- # Not valid flags
- assert not is_potential_flag('', parser)
- assert not is_potential_flag('non-flag', parser)
- assert not is_potential_flag('-', parser)
- assert not is_potential_flag('--has space', parser)
- assert not is_potential_flag('-2', parser)
-
- # Valid flags
- assert is_potential_flag('-flag', parser)
- assert is_potential_flag('--flag', parser)
diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py
new file mode 100644
index 00000000..f1faa66a
--- /dev/null
+++ b/tests/test_argparse_completer.py
@@ -0,0 +1,648 @@
+# coding=utf-8
+# flake8: noqa E302
+"""
+Unit/functional testing for argparse completer in cmd2
+"""
+import argparse
+from typing import List
+
+import pytest
+
+import cmd2
+from cmd2 import with_argparser, Cmd2ArgParser, CompletionItem
+from cmd2.utils import StdSim, basic_complete
+from .conftest import run_cmd, complete_tester
+
+# Lists used in our tests
+static_int_choices_list = [1, 2, 3, 4, 5]
+static_choices_list = ['static', 'choices', 'stop', 'here']
+choices_from_function = ['choices', 'function', 'chatty', 'smith']
+choices_from_method = ['choices', 'method', 'most', 'improved']
+
+set_value_choices = ['set', 'value', 'choices']
+one_or_more_choices = ['one', 'or', 'more', 'choices']
+optional_choices = ['a', 'few', 'optional', 'choices']
+range_choices = ['some', 'range', 'choices']
+remainder_choices = ['remainder', 'choices']
+
+positional_choices = ['the', 'positional', 'choices']
+
+completions_from_function = ['completions', 'function', 'fairly', 'complete']
+completions_from_method = ['completions', 'method', 'missed', 'spot']
+
+
+def choices_function() -> List[str]:
+ """Function that provides choices"""
+ return choices_from_function
+
+
+def completer_function(text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ """Tab completion function"""
+ return basic_complete(text, line, begidx, endidx, completions_from_function)
+
+
+# noinspection PyMethodMayBeStatic,PyUnusedLocal
+class AutoCompleteTester(cmd2.Cmd):
+ """Cmd2 app that exercises AutoCompleter class"""
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ ############################################################################################################
+ # Begin code related to help and command name completion
+ ############################################################################################################
+ def _music_create(self, args: argparse.Namespace) -> None:
+ """Implements the 'music create' command"""
+ self.poutput('music create')
+
+ def _music_create_jazz(self, args: argparse.Namespace) -> None:
+ """Implements the 'music create jazz' command"""
+ self.poutput('music create jazz')
+
+ def _music_create_rock(self, args: argparse.Namespace) -> None:
+ """Implements the 'music create rock' command"""
+ self.poutput('music create rock')
+
+ # Top level parser for music command
+ music_parser = Cmd2ArgParser(description='Manage music', prog='music')
+
+ # Add sub-commands to music
+ music_subparsers = music_parser.add_subparsers()
+
+ # music -> create
+ music_create_parser = music_subparsers.add_parser('create', help='Create music')
+ music_create_parser.set_defaults(func=_music_create)
+
+ # Add sub-commands to music -> create
+ music_create_subparsers = music_create_parser.add_subparsers()
+
+ # music -> create -> jazz
+ music_create_jazz_parser = music_create_subparsers.add_parser('jazz', help='Create jazz')
+ music_create_jazz_parser.set_defaults(func=_music_create_jazz)
+
+ # music -> create -> rock
+ music_create_rock_parser = music_create_subparsers.add_parser('rock', help='Create rocks')
+ music_create_rock_parser.set_defaults(func=_music_create_rock)
+
+ @with_argparser(music_parser)
+ def do_music(self, args: argparse.Namespace) -> None:
+ """Music command"""
+ func = getattr(args, 'func', None)
+ if func is not None:
+ # Call whatever sub-command function was selected
+ func(self, args)
+ else:
+ # No sub-command was provided, so call help
+ # noinspection PyTypeChecker
+ self.do_help('music')
+
+ ############################################################################################################
+ # Begin code related to flag completion
+ ############################################################################################################
+
+ # Uses default flag prefix value (-)
+ flag_parser = Cmd2ArgParser()
+ flag_parser.add_argument('-n', '--normal_flag', help='A normal flag', action='store_true')
+ flag_parser.add_argument('-a', '--append_flag', help='Append flag', action='append')
+ flag_parser.add_argument('-o', '--append_const_flag', help='Append const flag', action='append_const', const=True)
+ flag_parser.add_argument('-c', '--count_flag', help='Count flag', action='count')
+ flag_parser.add_argument('-s', '--suppressed_flag', help=argparse.SUPPRESS, action='store_true')
+ flag_parser.add_argument('-r', '--remainder_flag', nargs=argparse.REMAINDER, help='a remainder flag')
+
+ @with_argparser(flag_parser)
+ def do_flag(self, args: argparse.Namespace) -> None:
+ pass
+
+ # Uses non-default flag prefix value (+)
+ plus_flag_parser = Cmd2ArgParser(prefix_chars='+')
+ plus_flag_parser.add_argument('+n', '++normal_flag', help='A normal flag', action='store_true')
+
+ @with_argparser(plus_flag_parser)
+ def do_plus_flag(self, args: argparse.Namespace) -> None:
+ pass
+
+ ############################################################################################################
+ # Begin code related to testing choices, choices_function, and choices_method parameters
+ ############################################################################################################
+ def choices_method(self) -> List[str]:
+ """Method that provides choices"""
+ return choices_from_method
+
+ def completion_item_method(self) -> List[CompletionItem]:
+ """Choices method that returns CompletionItems"""
+ items = []
+ for i in range(0, 10):
+ main_str = 'main_str{}'.format(i)
+ items.append(CompletionItem(main_str, desc='blah blah'))
+ return items
+
+ choices_parser = Cmd2ArgParser()
+
+ # Flag args for choices command. Include string and non-string arg types.
+ choices_parser.add_argument("-l", "--list", help="a flag populated with a choices list",
+ choices=static_choices_list)
+ choices_parser.add_argument("-f", "--function", help="a flag populated with a choices function",
+ choices_function=choices_function)
+ choices_parser.add_argument("-m", "--method", help="a flag populated with a choices method",
+ choices_method=choices_method)
+ choices_parser.add_argument('-n', "--no_header", help='this arg has a no descriptive header',
+ choices_method=completion_item_method)
+ choices_parser.add_argument('-i', '--int', type=int, help='a flag with an int type',
+ choices=static_int_choices_list)
+
+ # Positional args for choices command
+ choices_parser.add_argument("list_pos", help="a positional populated with a choices list",
+ choices=static_choices_list)
+ choices_parser.add_argument("function_pos", help="a positional populated with a choices function",
+ choices_function=choices_function)
+ choices_parser.add_argument("method_pos", help="a positional populated with a choices method",
+ choices_method=choices_method)
+
+ @with_argparser(choices_parser)
+ def do_choices(self, args: argparse.Namespace) -> None:
+ pass
+
+ ############################################################################################################
+ # Begin code related to testing completer_function and completer_method parameters
+ ############################################################################################################
+ def completer_method(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ """Tab completion method"""
+ return basic_complete(text, line, begidx, endidx, completions_from_method)
+
+ completer_parser = Cmd2ArgParser()
+
+ # Flag args for completer command
+ completer_parser.add_argument("-f", "--function", help="a flag using a completer function",
+ completer_function=completer_function)
+ completer_parser.add_argument("-m", "--method", help="a flag using a completer method",
+ completer_method=completer_method)
+
+ # Positional args for completer command
+ completer_parser.add_argument("function_pos", help="a positional using a completer function",
+ completer_function=completer_function)
+ completer_parser.add_argument("method_pos", help="a positional using a completer method",
+ completer_method=completer_method)
+
+ @with_argparser(completer_parser)
+ def do_completer(self, args: argparse.Namespace) -> None:
+ pass
+
+ ############################################################################################################
+ # Begin code related to nargs
+ ############################################################################################################
+ nargs_parser = Cmd2ArgParser()
+
+ # Flag args for nargs command
+ nargs_parser.add_argument("--set_value", help="a flag with a set value for nargs", nargs=2,
+ choices=set_value_choices)
+ nargs_parser.add_argument("--one_or_more", help="a flag wanting one or more args", nargs=argparse.ONE_OR_MORE,
+ choices=one_or_more_choices)
+ nargs_parser.add_argument("--optional", help="a flag with an optional value", nargs=argparse.OPTIONAL,
+ choices=optional_choices)
+ nargs_parser.add_argument("--range", help="a flag with nargs range", nargs=(1, 2),
+ choices=range_choices)
+ nargs_parser.add_argument("--remainder", help="a flag wanting remaining", nargs=argparse.REMAINDER,
+ choices=remainder_choices)
+
+ nargs_parser.add_argument("normal_pos", help="a remainder positional", nargs=2,
+ choices=positional_choices)
+ nargs_parser.add_argument("remainder_pos", help="a remainder positional", nargs=argparse.REMAINDER,
+ choices=remainder_choices)
+
+ @with_argparser(nargs_parser)
+ def do_nargs(self, args: argparse.Namespace) -> None:
+ pass
+
+ ############################################################################################################
+ # Begin code related to testing tab hints
+ ############################################################################################################
+ hint_parser = Cmd2ArgParser()
+ hint_parser.add_argument('-f', '--flag', help='a flag arg')
+ hint_parser.add_argument('-s', '--suppressed_help', help=argparse.SUPPRESS)
+ hint_parser.add_argument('-t', '--suppressed_hint', help='a flag arg', suppress_tab_hint=True)
+
+ hint_parser.add_argument('hint_pos', help='here is a hint\nwith new lines')
+ hint_parser.add_argument('no_help_pos')
+
+ @with_argparser(hint_parser)
+ def do_hint(self, args: argparse.Namespace) -> None:
+ pass
+
+
+@pytest.fixture
+def ac_app():
+ app = AutoCompleteTester()
+ app.stdout = StdSim(app.stdout)
+ return app
+
+
+@pytest.mark.parametrize('command', [
+ 'music',
+ 'music create',
+ 'music create rock',
+ 'music create jazz'
+])
+def test_help(ac_app, command):
+ out1, err1 = run_cmd(ac_app, '{} -h'.format(command))
+ out2, err2 = run_cmd(ac_app, 'help {}'.format(command))
+ assert out1 == out2
+
+
+@pytest.mark.parametrize('command, text, completions', [
+ ('', 'mu', ['music ']),
+ ('music', 'cre', ['create ']),
+ ('music create', '', ['jazz', 'rock'])
+])
+def test_complete_help(ac_app, command, text, completions):
+ line = 'help {} {}'.format(command, text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ if completions:
+ assert first_match is not None
+ else:
+ assert first_match is None
+
+ assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key)
+
+
+@pytest.mark.parametrize('command_and_args, text, completions', [
+ # Complete all flags (suppressed will not show)
+ ('flag', '-', ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag',
+ '--remainder_flag', '-a', '-c', '-h', '-n', '-o', '-r']),
+ ('flag', '--', ['--append_const_flag', '--append_flag', '--count_flag', '--help',
+ '--normal_flag', '--remainder_flag']),
+
+ # Complete individual flag
+ ('flag', '-n', ['-n ']),
+ ('flag', '--n', ['--normal_flag ']),
+
+ # No flags should complete until current flag has its args
+ ('flag --append_flag', '-', []),
+
+ # Complete REMAINDER flag name
+ ('flag', '-r', ['-r ']),
+ ('flag', '--r', ['--remainder_flag ']),
+
+ # No flags after a REMAINDER should complete
+ ('flag -r value', '-', []),
+ ('flag --remainder_flag value', '--', []),
+
+ # Suppressed flag should not complete
+ ('flag', '-s', []),
+ ('flag', '--s', []),
+
+ # A used flag should not show in completions
+ ('flag -n', '--', ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--remainder_flag']),
+
+ # Flags with actions set to append, append_const, and count will always show even if they've been used
+ ('flag --append_const_flag -c --append_flag value', '--', ['--append_const_flag', '--append_flag', '--count_flag',
+ '--help', '--normal_flag', '--remainder_flag']),
+
+ # Non-default flag prefix character (+)
+ ('plus_flag', '+', ['++help', '++normal_flag', '+h', '+n']),
+ ('plus_flag', '++', ['++help', '++normal_flag']),
+
+ # Flag completion should not occur after '--' since that tells argparse all remaining arguments are non-flags
+ ('flag --', '--', []),
+ ('flag --help --', '--', []),
+ ('plus_flag --', '++', []),
+ ('plus_flag ++help --', '++', [])
+])
+def test_autcomp_flag_completion(ac_app, command_and_args, text, completions):
+ line = '{} {}'.format(command_and_args, text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ if completions:
+ assert first_match is not None
+ else:
+ assert first_match is None
+
+ assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key)
+
+
+@pytest.mark.parametrize('flag, text, completions', [
+ ('-l', '', static_choices_list),
+ ('--list', 's', ['static', 'stop']),
+ ('-f', '', choices_from_function),
+ ('--function', 'ch', ['choices', 'chatty']),
+ ('-m', '', choices_from_method),
+ ('--method', 'm', ['method', 'most']),
+ ('-i', '', [str(i) for i in static_int_choices_list]),
+ ('--int', '1', ['1 '])
+])
+def test_autocomp_flag_choices_completion(ac_app, flag, text, completions):
+ line = 'choices {} {}'.format(flag, text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ if completions:
+ assert first_match is not None
+ else:
+ assert first_match is None
+
+ assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key)
+
+
+@pytest.mark.parametrize('pos, text, completions', [
+ (1, '', static_choices_list),
+ (1, 's', ['static', 'stop']),
+ (2, '', choices_from_function),
+ (2, 'ch', ['choices', 'chatty']),
+ (3, '', choices_from_method),
+ (3, 'm', ['method', 'most'])
+])
+def test_autocomp_positional_choices_completion(ac_app, pos, text, completions):
+ # Generate line were preceding positionals are already filled
+ line = 'choices {} {}'.format('foo ' * (pos - 1), text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ if completions:
+ assert first_match is not None
+ else:
+ assert first_match is None
+
+ assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key)
+
+
+@pytest.mark.parametrize('flag, text, completions', [
+ ('-f', '', completions_from_function),
+ ('--function', 'f', ['function', 'fairly']),
+ ('-m', '', completions_from_method),
+ ('--method', 'm', ['method', 'missed'])
+])
+def test_autocomp_flag_completers(ac_app, flag, text, completions):
+ line = 'completer {} {}'.format(flag, text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ if completions:
+ assert first_match is not None
+ else:
+ assert first_match is None
+
+ assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key)
+
+
+@pytest.mark.parametrize('pos, text, completions', [
+ (1, '', completions_from_function),
+ (1, 'c', ['completions', 'complete']),
+ (2, '', completions_from_method),
+ (2, 'm', ['method', 'missed'])
+])
+def test_autocomp_positional_completers(ac_app, pos, text, completions):
+ # Generate line were preceding positionals are already filled
+ line = 'completer {} {}'.format('foo ' * (pos - 1), text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ if completions:
+ assert first_match is not None
+ else:
+ assert first_match is None
+
+ assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key)
+
+
+@pytest.mark.parametrize('num_aliases, show_description', [
+ # The number of completion results determines if the description field of CompletionItems gets displayed
+ # in the tab completions. The count must be greater than 1 and less than ac_app.max_completion_items,
+ # which defaults to 50.
+ (1, False),
+ (5, True),
+ (100, False)
+])
+def test_completion_items(ac_app, num_aliases, show_description):
+ # Create aliases
+ for i in range(0, num_aliases):
+ run_cmd(ac_app, 'alias create fake{} help'.format(i))
+
+ assert len(ac_app.aliases) == num_aliases
+
+ text = 'fake'
+ line = 'alias list {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ assert first_match is not None
+ assert len(ac_app.completion_matches) == num_aliases
+ assert len(ac_app.display_matches) == num_aliases
+
+ # If show_description is True, the alias's value will be in the display text
+ assert ('help' in ac_app.display_matches[0]) == show_description
+
+
+@pytest.mark.parametrize('args, completions', [
+ # Flag with nargs = 2
+ ('--set_value', set_value_choices),
+ ('--set_value set', ['value', 'choices']),
+
+ # Both args are filled. At positional arg now.
+ ('--set_value set value', positional_choices),
+
+ # Another flag can't start until all expected args are filled out
+ ('--set_value --one_or_more', set_value_choices),
+
+ # Using the flag again will reset the choices available
+ ('--set_value set value --set_value', set_value_choices),
+
+ # Flag with nargs = ONE_OR_MORE
+ ('--one_or_more', one_or_more_choices),
+ ('--one_or_more one', ['or', 'more', 'choices']),
+
+ # Flag with nargs = OPTIONAL
+ ('--optional', optional_choices),
+
+ # Only one arg allowed for an OPTIONAL. At positional now.
+ ('--optional optional', positional_choices),
+
+ # Flag with nargs range (1, 2)
+ ('--range', range_choices),
+ ('--range some', ['range', 'choices']),
+
+ # Already used 2 args so at positional
+ ('--range some range', positional_choices),
+
+ # Flag with nargs = REMAINDER
+ ('--remainder', remainder_choices),
+ ('--remainder remainder ', ['choices ']),
+
+ # No more flags can appear after a REMAINDER flag)
+ ('--remainder choices --set_value', ['remainder ']),
+
+ # Double dash ends the current flag (even if all expected args aren't entered)
+ ('--set_value --', positional_choices),
+
+ # Double dash ends a REMAINDER flag
+ ('--remainder remainder --', positional_choices),
+
+ # No more flags after a double dash
+ ('-- --one_or_more ', positional_choices),
+
+ # Consume positional
+ ('', positional_choices),
+ ('positional', ['the', 'choices']),
+
+ # Intermixed flag and positional
+ ('positional --set_value', set_value_choices),
+ ('positional --set_value set', ['value', 'choices']),
+
+ # Intermixed flag and positional with flag finishing
+ ('positional --set_value set value', ['the', 'choices']),
+ ('positional --set_value set --', ['the', 'choices']),
+
+ # REMAINDER positional
+ ('the positional', remainder_choices),
+ ('the positional remainder', ['choices ']),
+
+ # REMAINDER positional. Flags don't work in REMAINDER
+ ('the positional --set_value', remainder_choices),
+])
+def test_autcomp_nargs(ac_app, args, completions):
+ text = ''
+ line = 'nargs {} {}'.format(args, text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ if completions:
+ assert first_match is not None
+ else:
+ assert first_match is None
+
+ assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key)
+
+
+def test_completion_items_default_header(ac_app):
+ from cmd2.argparse_completer import DEFAULT_DESCRIPTIVE_HEADER
+
+ text = ''
+ line = 'choices -n {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ # This positional argument did not provide a descriptive header, so it should be DEFAULT_DESCRIPTIVE_HEADER
+ complete_tester(text, line, begidx, endidx, ac_app)
+ assert DEFAULT_DESCRIPTIVE_HEADER in ac_app.completion_header
+
+
+def test_autocomp_hint_flag(ac_app, capsys):
+ text = ''
+ line = 'hint --flag {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ out, err = capsys.readouterr()
+
+ assert first_match is None
+ assert out == '''
+Hint:
+ -f, --flag FLAG a flag arg
+
+'''
+
+
+def test_autocomp_hint_suppressed_help(ac_app, capsys):
+ text = ''
+ line = 'hint --suppressed_help {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ out, err = capsys.readouterr()
+
+ assert first_match is None
+ assert not out
+
+
+def test_autocomp_hint_suppressed_hint(ac_app, capsys):
+ text = ''
+ line = 'hint --suppressed_hint {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ out, err = capsys.readouterr()
+
+ assert first_match is None
+ assert not out
+
+
+def test_autocomp_hint_pos(ac_app, capsys):
+ text = ''
+ line = 'hint {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ out, err = capsys.readouterr()
+
+ assert first_match is None
+ assert out == '''
+Hint:
+ HINT_POS here is a hint
+ with new lines
+
+'''
+
+
+def test_autocomp_hint_no_help(ac_app, capsys):
+ text = ''
+ line = 'hint foo {}'.format(text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, ac_app)
+ out, err = capsys.readouterr()
+
+ assert first_match is None
+ assert not out == '''
+Hint:
+ NO_HELP_POS
+
+'''
+
+def test_is_potential_flag():
+ from cmd2.argparse_completer import is_potential_flag
+ parser = Cmd2ArgParser()
+
+ # Not potential flags
+ assert not is_potential_flag('', parser)
+ assert not is_potential_flag('non-flag', parser)
+ assert not is_potential_flag('--has space', parser)
+ assert not is_potential_flag('-2', parser)
+
+ # Potential flags
+ assert is_potential_flag('-', parser)
+ assert is_potential_flag('--', parser)
+ assert is_potential_flag('-flag', parser)
+ assert is_potential_flag('--flag', parser)
+
+
+def test_complete_command_no_tokens(ac_app):
+ from cmd2.argparse_completer import AutoCompleter
+
+ parser = Cmd2ArgParser()
+ ac = AutoCompleter(parser, ac_app)
+
+ completions = ac.complete_command(tokens=[], text='', line='', begidx=0, endidx=0)
+ assert not completions
+
+
+def test_complete_command_help_no_tokens(ac_app):
+ from cmd2.argparse_completer import AutoCompleter
+
+ parser = Cmd2ArgParser()
+ ac = AutoCompleter(parser, ac_app)
+
+ completions = ac.complete_command_help(tokens=[], text='', line='', begidx=0, endidx=0)
+ assert not completions
diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py
new file mode 100644
index 00000000..35d97974
--- /dev/null
+++ b/tests/test_argparse_custom.py
@@ -0,0 +1,145 @@
+# flake8: noqa E302
+"""
+Unit/functional testing for argparse customizations in cmd2
+"""
+import argparse
+
+import pytest
+
+import cmd2
+from cmd2.argparse_custom import Cmd2ArgParser
+from .conftest import run_cmd
+
+
+class ApCustomTestApp(cmd2.Cmd):
+ """Test app for cmd2's argparse customization"""
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ range_parser = Cmd2ArgParser()
+ range_parser.add_argument('--arg1', nargs=(2, 3))
+ range_parser.add_argument('--arg2', nargs=argparse.ZERO_OR_MORE)
+ range_parser.add_argument('--arg3', nargs=argparse.ONE_OR_MORE)
+
+ @cmd2.with_argparser(range_parser)
+ def do_range(self, _):
+ pass
+
+
+@pytest.fixture
+def cust_app():
+ return ApCustomTestApp()
+
+
+def fake_func():
+ pass
+
+
+@pytest.mark.parametrize('args, is_valid', [
+ ({'choices': []}, True),
+ ({'choices_function': fake_func}, True),
+ ({'choices_method': fake_func}, True),
+ ({'completer_function': fake_func}, True),
+ ({'completer_method': fake_func}, True),
+ ({'choices': [], 'choices_function': fake_func}, False),
+ ({'choices': [], 'choices_method': fake_func}, False),
+ ({'choices_method': fake_func, 'completer_function': fake_func}, False),
+ ({'choices_method': fake_func, 'completer_method': fake_func}, False),
+])
+def test_apcustom_invalid_args(args, is_valid):
+ parser = Cmd2ArgParser(prog='test')
+ try:
+ parser.add_argument('name', **args)
+ assert is_valid
+ except ValueError as ex:
+ assert not is_valid
+ assert 'Only one of the following may be used' in str(ex)
+
+
+def test_apcustom_usage():
+ usage = "A custom usage statement"
+ parser = Cmd2ArgParser(usage=usage)
+ help = parser.format_help()
+ assert usage in help
+
+
+def test_apcustom_nargs_help_format(cust_app):
+ out, err = run_cmd(cust_app, 'help range')
+ assert 'Usage: range [-h] [--arg1 ARG1{2..3}] [--arg2 [ARG2 [...]]]' in out[0]
+ assert ' [--arg3 ARG3 [...]]' in out[1]
+
+
+def test_apcustom_nargs_not_enough(cust_app):
+ out, err = run_cmd(cust_app, 'range --arg1 one')
+ assert 'Error: argument --arg1: Expected between 2 and 3 arguments' in err[2]
+
+
+def test_apcustom_narg_empty_tuple():
+ with pytest.raises(ValueError) as excinfo:
+ parser = Cmd2ArgParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=())
+ assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
+
+
+def test_apcustom_narg_single_tuple():
+ with pytest.raises(ValueError) as excinfo:
+ parser = Cmd2ArgParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=(1,))
+ assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
+
+
+def test_apcustom_narg_tuple_triple():
+ with pytest.raises(ValueError) as excinfo:
+ parser = Cmd2ArgParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=(1, 2, 3))
+ assert 'Ranged values for nargs must be a tuple of 2 integers' in str(excinfo.value)
+
+
+def test_apcustom_narg_tuple_order():
+ with pytest.raises(ValueError) as excinfo:
+ parser = Cmd2ArgParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=(2, 1))
+ assert 'Invalid nargs range. The first value must be less than the second' in str(excinfo.value)
+
+
+def test_apcustom_narg_tuple_negative():
+ with pytest.raises(ValueError) as excinfo:
+ parser = Cmd2ArgParser(prog='test')
+ parser.add_argument('invalid_tuple', nargs=(-1, 1))
+ assert 'Negative numbers are invalid for nargs range' in str(excinfo.value)
+
+
+def test_apcustom_narg_tuple_zero_base():
+ parser = Cmd2ArgParser(prog='test')
+ parser.add_argument('tuple', nargs=(0, 3))
+
+
+def test_apcustom_narg_tuple_zero_to_one():
+ parser = Cmd2ArgParser(prog='test')
+ parser.add_argument('tuple', nargs=(0, 1))
+
+
+def test_apcustom_print_message(capsys):
+ import sys
+ test_message = 'The test message'
+
+ # Specify the file
+ parser = Cmd2ArgParser(prog='test')
+ parser._print_message(test_message, file=sys.stdout)
+ out, err = capsys.readouterr()
+ assert test_message in out
+
+ # Make sure file defaults to sys.stderr
+ parser = Cmd2ArgParser(prog='test')
+ parser._print_message(test_message)
+ out, err = capsys.readouterr()
+ assert test_message in err
+
+
+def test_apcustom_required_options():
+ # Make sure a 'required arguments' section shows when a flag is marked required
+ parser = Cmd2ArgParser(prog='test')
+ parser.add_argument('--required_flag', required=True)
+ help = parser.format_help()
+
+ assert 'required arguments' in help
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
deleted file mode 100644
index 4e1ceff0..00000000
--- a/tests/test_autocompletion.py
+++ /dev/null
@@ -1,345 +0,0 @@
-# coding=utf-8
-# flake8: noqa E302
-"""
-Unit/functional testing for argparse completer in cmd2
-"""
-import pytest
-
-from cmd2.utils import StdSim
-from .conftest import run_cmd, normalize, complete_tester
-
-from examples.tab_autocompletion import TabCompleteExample
-
-@pytest.fixture
-def cmd2_app():
- app = TabCompleteExample()
- app.stdout = StdSim(app.stdout)
- return app
-
-
-SUGGEST_HELP = '''Usage: suggest -t {movie, show} [-h] [-d DURATION{1..2}]
-
-Suggest command demonstrates argparse customizations.
-See hybrid_suggest and orig_suggest to compare the help output.
-
-required arguments:
- -t, --type {movie, show}
-
-optional arguments:
- -h, --help show this help message and exit
- -d, --duration DURATION{1..2}
- Duration constraint in minutes.
- single value - maximum duration
- [a, b] - duration range'''
-
-MEDIA_MOVIES_ADD_HELP = '''Usage: media movies add -d DIRECTOR{1..2}
- [-h]
- title {G, PG, PG-13, R, NC-17} ...
-
-positional arguments:
- title Movie Title
- {G, PG, PG-13, R, NC-17}
- Movie Rating
- actor Actors
-
-required arguments:
- -d, --director DIRECTOR{1..2}
- Director
-
-optional arguments:
- -h, --help show this help message and exit'''
-
-def test_help_required_group(cmd2_app):
- out1, err1 = run_cmd(cmd2_app, 'suggest -h')
- out2, err2 = run_cmd(cmd2_app, 'help suggest')
-
- assert out1 == out2
- assert out1[0].startswith('Usage: suggest')
- assert out1[1] == ''
- assert out1[2].startswith('Suggest command demonstrates argparse customizations.')
- assert out1 == normalize(SUGGEST_HELP)
-
-
-def test_help_required_group_long(cmd2_app):
- out1, err1 = run_cmd(cmd2_app, 'media movies add -h')
- out2, err2 = run_cmd(cmd2_app, 'help media movies add')
-
- assert out1 == out2
- assert out1[0].startswith('Usage: media movies add')
- assert out1 == normalize(MEDIA_MOVIES_ADD_HELP)
-
-
-def test_autocomp_flags(cmd2_app):
- text = '-'
- line = 'suggest {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['--duration', '--help', '--type', '-d', '-h', '-t']
-
-def test_autcomp_hint(cmd2_app, capsys):
- text = ''
- line = 'suggest -d {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- out, err = capsys.readouterr()
-
- assert out == '''
-Hint:
- -d, --duration DURATION Duration constraint in minutes.
- single value - maximum duration
- [a, b] - duration range
-
-'''
-
-def test_autcomp_flag_comp(cmd2_app, capsys):
- text = '--d'
- line = 'suggest {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- out, err = capsys.readouterr()
-
- assert first_match is not None and \
- cmd2_app.completion_matches == ['--duration ']
-
-
-def test_autocomp_flags_choices(cmd2_app):
- text = ''
- line = 'suggest -t {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['movie', 'show']
-
-
-def test_autcomp_hint_in_narg_range(cmd2_app, capsys):
- text = ''
- line = 'suggest -d 2 {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- out, err = capsys.readouterr()
-
- assert out == '''
-Hint:
- -d, --duration DURATION Duration constraint in minutes.
- single value - maximum duration
- [a, b] - duration range
-
-'''
-
-def test_autocomp_flags_narg_max(cmd2_app):
- text = ''
- line = 'suggest d 2 3 {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is None
-
-
-def test_autcomp_narg_beyond_max(cmd2_app):
- out, err = run_cmd(cmd2_app, 'suggest -t movie -d 3 4 5')
- assert 'Error: unrecognized arguments: 5' in err[1]
-
-
-def test_autocomp_subcmd_nested(cmd2_app):
- text = ''
- line = 'media movies {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['add', 'delete', 'list', 'load']
-
-
-def test_autocomp_subcmd_flag_choices_append(cmd2_app):
- text = ''
- line = 'media movies list -r {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['G', 'NC-17', 'PG', 'PG-13', 'R']
-
-def test_autocomp_subcmd_flag_choices_append_exclude(cmd2_app):
- text = ''
- line = 'media movies list -r PG PG-13 {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['G', 'NC-17', 'R']
-
-
-def test_autocomp_subcmd_flag_comp_func(cmd2_app):
- text = 'A'
- line = 'media movies list -a "{}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['Adam Driver', 'Alec Guinness', 'Andy Serkis', 'Anthony Daniels']
-
-
-def test_autocomp_subcmd_flag_comp_list(cmd2_app):
- text = 'G'
- line = 'media movies list -d {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and first_match == '"Gareth Edwards'
-
-
-def test_autocomp_subcmd_flag_comp_func_attr(cmd2_app):
- text = 'A'
- line = 'video movies list -a "{}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['Adam Driver', 'Alec Guinness', 'Andy Serkis', 'Anthony Daniels']
-
-
-def test_autocomp_subcmd_flag_comp_list_attr(cmd2_app):
- text = 'G'
- line = 'video movies list -d {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and first_match == '"Gareth Edwards'
-
-
-def test_autocomp_pos_consumed(cmd2_app):
- text = ''
- line = 'library movie add SW_EP01 {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is None
-
-
-def test_autocomp_pos_after_flag(cmd2_app):
- text = 'Joh'
- line = 'video movies add -d "George Lucas" -- "Han Solo" PG "Emilia Clarke" "{}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['John Boyega" ']
-
-
-def test_autocomp_custom_func_list_arg(cmd2_app):
- text = 'SW_'
- line = 'library show add {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['SW_CW', 'SW_REB', 'SW_TCW']
-
-
-def test_autocomp_custom_func_list_and_dict_arg(cmd2_app):
- text = ''
- line = 'library show add SW_REB {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['S01E02', 'S01E03', 'S02E01', 'S02E03']
-
-
-def test_autocomp_custom_func_dict_arg(cmd2_app):
- text = '/home/user/'
- line = 'video movies load {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and \
- cmd2_app.completion_matches == ['/home/user/another.db', '/home/user/file space.db', '/home/user/file.db']
-
-
-def test_argparse_remainder_flag_completion(cmd2_app):
- import cmd2
- import argparse
-
- # Test flag completion as first arg of positional with nargs=argparse.REMAINDER
- text = '--h'
- line = 'help command {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- # --h should not complete into --help because we are in the argparse.REMAINDER section
- assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
-
- # Test flag completion within an already started positional with nargs=argparse.REMAINDER
- text = '--h'
- line = 'help command subcommand {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- # --h should not complete into --help because we are in the argparse.REMAINDER section
- assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
-
- # Test a flag with nargs=argparse.REMAINDER
- parser = argparse.ArgumentParser()
- parser.add_argument('-f', nargs=argparse.REMAINDER)
-
- # Overwrite eof's parser for this test
- cmd2.Cmd.do_eof.argparser = parser
-
- text = '--h'
- line = 'eof -f {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- # --h should not complete into --help because we are in the argparse.REMAINDER section
- assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
-
-
-def test_completion_after_double_dash(cmd2_app):
- """
- Test completion after --, which argparse says (all args after -- are non-options)
- All of these tests occur outside of an argparse.REMAINDER section since those tests
- are handled in test_argparse_remainder_flag_completion
- """
-
- # Test -- as the last token
- text = '--'
- line = 'help {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- # Since -- is the last token, then it should show flag choices
- first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
- assert first_match is not None and '--help' in cmd2_app.completion_matches
-
- # Test -- to end all flag completion
- text = '--'
- line = 'help -- {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- # Since -- appeared before the -- being completed, nothing should be completed
- assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 9ffe547a..1bdbea5f 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1504,22 +1504,33 @@ invalid_command_name = [
'noembedded"quotes',
]
-def test_get_alias_names(base_app):
- assert len(base_app.aliases) == 0
+def test_get_alias_completion_items(base_app):
run_cmd(base_app, 'alias create fake run_pyscript')
run_cmd(base_app, 'alias create ls !ls -hal')
- assert len(base_app.aliases) == 2
- assert sorted(base_app._get_alias_names()) == ['fake', 'ls']
-def test_get_macro_names(base_app):
- assert len(base_app.macros) == 0
+ results = base_app._get_alias_completion_items()
+ assert len(results) == len(base_app.aliases)
+
+ for cur_res in results:
+ assert cur_res in base_app.aliases
+ assert cur_res.description == base_app.aliases[cur_res]
+
+def test_get_macro_completion_items(base_app):
run_cmd(base_app, 'macro create foo !echo foo')
run_cmd(base_app, 'macro create bar !echo bar')
- assert len(base_app.macros) == 2
- assert sorted(base_app._get_macro_names()) == ['bar', 'foo']
-def test_get_settable_names(base_app):
- assert sorted(base_app._get_settable_names()) == sorted(base_app.settable.keys())
+ results = base_app._get_macro_completion_items()
+ assert len(results) == len(base_app.macros)
+
+ for cur_res in results:
+ assert cur_res in base_app.macros
+ assert cur_res.description == base_app.macros[cur_res].value
+
+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]
def test_alias_no_subcommand(base_app):
out, err = run_cmd(base_app, 'alias')
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 5cfc741c..1411cc49 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -67,7 +67,7 @@ class CompletionsExample(cmd2.Cmd):
pass
def complete_test_basic(self, text, line, begidx, endidx):
- return self.basic_complete(text, line, begidx, endidx, food_item_strs)
+ return utils.basic_complete(text, line, begidx, endidx, food_item_strs)
def do_test_delimited(self, args):
pass
@@ -80,7 +80,7 @@ class CompletionsExample(cmd2.Cmd):
def complete_test_sort_key(self, text, line, begidx, endidx):
num_strs = ['2', '11', '1']
- return self.basic_complete(text, line, begidx, endidx, num_strs)
+ return utils.basic_complete(text, line, begidx, endidx, num_strs)
def do_test_raise_exception(self, args):
pass
@@ -516,7 +516,7 @@ def test_path_completion_directories_only(cmd2_app, request):
expected = [text + 'cripts' + os.path.sep]
- assert cmd2_app.path_complete(text, line, begidx, endidx, os.path.isdir) == expected
+ assert cmd2_app.path_complete(text, line, begidx, endidx, path_filter=os.path.isdir) == expected
def test_basic_completion_single(cmd2_app):
text = 'Pi'
@@ -524,7 +524,7 @@ def test_basic_completion_single(cmd2_app):
endidx = len(line)
begidx = endidx - len(text)
- assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza']
+ assert utils.basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza']
def test_basic_completion_multiple(cmd2_app):
text = ''
@@ -532,7 +532,7 @@ def test_basic_completion_multiple(cmd2_app):
endidx = len(line)
begidx = endidx - len(text)
- matches = sorted(cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs))
+ matches = sorted(utils.basic_complete(text, line, begidx, endidx, food_item_strs))
assert matches == sorted(food_item_strs)
def test_basic_completion_nomatch(cmd2_app):
@@ -541,7 +541,7 @@ def test_basic_completion_nomatch(cmd2_app):
endidx = len(line)
begidx = endidx - len(text)
- assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == []
+ assert utils.basic_complete(text, line, begidx, endidx, food_item_strs) == []
def test_delimiter_completion(cmd2_app):
text = '/home/'
@@ -592,7 +592,7 @@ def test_flag_based_default_completer(cmd2_app, request):
begidx = endidx - len(text)
assert cmd2_app.flag_based_complete(text, line, begidx, endidx,
- flag_dict, cmd2_app.path_complete) == [text + 'onftest.py']
+ flag_dict, all_else=cmd2_app.path_complete) == [text + 'onftest.py']
def test_flag_based_callable_completer(cmd2_app, request):
test_dir = os.path.dirname(request.module.__file__)
@@ -642,7 +642,7 @@ def test_index_based_default_completer(cmd2_app, request):
begidx = endidx - len(text)
assert cmd2_app.index_based_complete(text, line, begidx, endidx,
- index_dict, cmd2_app.path_complete) == [text + 'onftest.py']
+ index_dict, all_else=cmd2_app.path_complete) == [text + 'onftest.py']
def test_index_based_callable_completer(cmd2_app, request):
test_dir = os.path.dirname(request.module.__file__)
@@ -1072,8 +1072,7 @@ class SubcommandsWithUnknownExample(cmd2.Cmd):
# create the parser for the "sport" sub-command
parser_sport = base_subparsers.add_parser('sport', help='sport help')
- sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport')
- setattr(sport_arg, 'arg_choices', sport_item_strs)
+ sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs)
@cmd2.with_argparser_and_unknown_args(base_parser)
def do_base(self, args):