summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-10-09 20:04:50 -0400
committerGitHub <noreply@github.com>2018-10-09 20:04:50 -0400
commitf38e100fd77f4a136a4883d23b2f4f8b3cd934b7 (patch)
treec289c216807646567953191d35ebdc5c07198c24
parent467be57e647112f536becc8625ffa080cb67a0ce (diff)
parent84f290bfdd82eb1c2eaf26b5936f7088b4911f2c (diff)
downloadcmd2-git-f38e100fd77f4a136a4883d23b2f4f8b3cd934b7.tar.gz
Merge pull request #571 from python-cmd2/argparse_remainder
Fixes related to handling of argparse.REMAINDER
-rw-r--r--CHANGELOG.md11
-rwxr-xr-xcmd2/argparse_completer.py138
-rw-r--r--cmd2/cmd2.py4
-rw-r--r--cmd2/pyscript_bridge.py84
-rw-r--r--cmd2/utils.py10
-rw-r--r--docs/argument_processing.rst28
-rwxr-xr-xexamples/decorator_example.py (renamed from examples/argparse_example.py)9
-rwxr-xr-xexamples/hello_cmd2.py (renamed from main.py)6
-rwxr-xr-xexamples/tab_autocompletion.py4
-rwxr-xr-xexamples/table_display.py26
-rw-r--r--examples/transcripts/exampleSession.txt2
-rw-r--r--tests/test_acargparse.py17
-rw-r--r--tests/test_autocompletion.py2
-rw-r--r--tests/test_completion.py24
-rw-r--r--tests/test_pyscript.py19
-rw-r--r--tests/test_utils.py12
16 files changed, 311 insertions, 85 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d800fcc..0042b86b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,16 @@
* Fixed bug where **alias** command was dropping quotes around arguments
* Fixed bug where running help on argparse commands didn't work if they didn't support -h
* Fixed transcript testing bug where last command in transcript has no expected output
+ * Fixed bugs with how AutoCompleter and ArgparseFunctor handle argparse
+ arguments with nargs=argparse.REMAINDER. Tab completion now correctly
+ matches how argparse will parse the values. Command strings generated by
+ ArgparseFunctor should now be compliant with how argparse expects
+ REMAINDER arguments to be ordered.
+ * Fixed bugs with how AutoCompleter handles flag prefixes. It is no
+ longer hard-coded to use '-' and will check against the prefix_chars in
+ the argparse object. Also, single-character tokens that happen to be a
+ prefix char are not treated as flags by argparse and AutoCompleter now
+ matches that behavior.
* Enhancements
* Added ``exit_code`` attribute of ``cmd2.Cmd`` class
* Enables applications to return a non-zero exit code when exiting from ``cmdloop``
@@ -24,6 +34,7 @@
* Never - output methods strip all ANSI escape sequences
* Added ``macro`` command to create macros, which are similar to aliases, but can take arguments when called
* All cmd2 command functions have been converted to use argparse.
+ * Renamed argparse_example.py to decorator_example.py to help clarify its intent
* Deprecations
* Deprecated the built-in ``cmd2`` support for colors including ``Cmd.colorize()`` and ``Cmd._colorcodes``
* Deletions (potentially breaking changes)
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index c900a780..168a555f 100755
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -209,6 +209,34 @@ def register_custom_actions(parser: argparse.ArgumentParser) -> None:
parser.register('action', 'append', _AppendRangeAction)
+def token_resembles_flag(token: str, parser: argparse.ArgumentParser) -> bool:
+ """Determine if a token looks like a flag. Based on argparse._parse_optional()."""
+ # if it's an empty string, it was meant to be a positional
+ if not token:
+ return False
+
+ # if it doesn't start with a prefix, it was meant to be positional
+ 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):
+ if not parser._has_negative_number_optionals:
+ return False
+
+ # if it contains a space, it was meant to be a positional
+ if ' ' in token:
+ return False
+
+ # Looks like a flag
+ return True
+
+
class AutoCompleter(object):
"""Automatically command line tab completion based on argparse parameters"""
@@ -318,6 +346,9 @@ class AutoCompleter(object):
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}
+
matched_flags = []
current_is_positional = False
consumed_arg_values = {} # dict(arg_name -> [values, ...])
@@ -331,8 +362,8 @@ class AutoCompleter(object):
def consume_flag_argument() -> None:
"""Consuming token as a flag argument"""
# we're consuming flag arguments
- # if this is not empty and is not another potential flag, count towards flag arguments
- if token and token[0] not in self._parser.prefix_chars and flag_action is not None:
+ # if the token does not look like a new flag, then count towards flag arguments
+ if not token_resembles_flag(token, self._parser) and flag_action is not None:
flag_arg.count += 1
# does this complete a option item for the flag
@@ -355,17 +386,79 @@ class AutoCompleter(object):
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
+
+ # 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
+ # 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.
+ #
+ # We're also trying to save every flag that has been used as well as every value that
+ # has been used for a positional or flag parameter. By saving this information we can exclude
+ # it from the completion results we generate for the last token. For example, single-use flag
+ # 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
is_last_token = False
+
for idx, token in enumerate(tokens):
is_last_token = idx >= len(tokens) - 1
# Only start at the start token index
if idx >= self._token_start_index:
+ # 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:
+ 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:
- # we're not consuming flag arguments, is the current argument a potential flag?
- if len(token) > 0 and token[0] in self._parser.prefix_chars and\
- (is_last_token or (not is_last_token and token != '-')):
+ # 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
+ skip_flag = False
+ 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_flag = True
+
+ # At this point we're no longer consuming flag arguments. Is the current argument a potential flag?
+ if token_resembles_flag(token, self._parser) and not skip_flag:
# reset some tracking values
flag_arg.reset()
# don't reset positional tracking because flags can be interspersed anywhere between positionals
@@ -381,7 +474,7 @@ class AutoCompleter(object):
if flag_action is not None:
# resolve argument counts
- self._process_action_nargs(flag_action, flag_arg)
+ 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)
@@ -418,7 +511,7 @@ class AutoCompleter(object):
return sub_completers[token].complete_command(tokens, text, line,
begidx, endidx)
pos_action = action
- self._process_action_nargs(pos_action, pos_arg)
+ process_action_nargs(pos_action, pos_arg)
consume_positional_argument()
elif not is_last_token and pos_arg.max is not None:
@@ -435,10 +528,13 @@ class AutoCompleter(object):
if not is_last_token and flag_arg.min is not None:
flag_arg.needed = flag_arg.count < flag_arg.min
+ # Here we're done parsing all of the prior arguments. We know what the next argument is.
+
# 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
completion_results = []
- if not flag_arg.needed and len(tokens[-1]) > 0 and tokens[-1][0] in self._parser.prefix_chars:
+ if not flag_arg.needed and len(tokens[-1]) > 0 and tokens[-1][0] in self._parser.prefix_chars and \
+ remainder['arg'] is None:
return AutoCompleter.basic_complete(text, line, begidx, endidx,
[flag for flag in self._flags if flag not in matched_flags])
# we're not at a positional argument, see if we're in a flag argument
@@ -522,32 +618,6 @@ class AutoCompleter(object):
return completers[token].format_help(tokens)
return self._parser.format_help()
- @staticmethod
- def _process_action_nargs(action: argparse.Action, arg_state: _ArgumentState) -> None:
- 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 == '*':
- arg_state.min = 0
- arg_state.max = float('inf')
- arg_state.variable = True
- 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
-
def _complete_for_arg(self, action: argparse.Action,
text: str,
line: str,
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 4502c53a..c000fb80 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -193,7 +193,7 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
instance of argparse.ArgumentParser, but also returning unknown args as a list.
- :param argparser: given instance of ArgumentParser
+ :param argparser: unique instance of ArgumentParser
:param preserve_quotes: if True, then the arguments passed to arparse be maintain their quotes
:return: function that gets passed parsed args and a list of unknown args
"""
@@ -234,7 +234,7 @@ def with_argparser(argparser: argparse.ArgumentParser, preserve_quotes: bool=Fal
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
with the given instance of argparse.ArgumentParser.
- :param argparser: given instance of ArgumentParser
+ :param argparser: unique instance of ArgumentParser
:param preserve_quotes: if True, then the arguments passed to arparse be maintain their quotes
:return: function that gets passed parsed args
"""
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 7c09aab0..11a2cbb3 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -12,8 +12,8 @@ import functools
import sys
from typing import List, Callable, Optional
-from .argparse_completer import _RangeAction
-from .utils import namedtuple_with_defaults, StdSim
+from .argparse_completer import _RangeAction, token_resembles_flag
+from .utils import namedtuple_with_defaults, StdSim, quote_string_if_needed
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
if sys.version_info < (3, 5):
@@ -82,6 +82,10 @@ class ArgparseFunctor:
# Dictionary mapping command argument name to value
self._args = {}
+ # tag the argument that's a remainder type
+ self._remainder_arg = None
+ # separately track flag arguments so they will be printed before positionals
+ self._flag_args = []
# argparse object for the current command layer
self.__current_subcommand_parser = parser
@@ -116,7 +120,6 @@ class ArgparseFunctor:
next_pos_index = 0
has_subcommand = False
- consumed_kw = []
# Iterate through the current sub-command's arguments in order
for action in self.__current_subcommand_parser._actions:
@@ -125,7 +128,7 @@ class ArgparseFunctor:
# this is a flag argument, search for the argument by name in the parameters
if action.dest in kwargs:
self._args[action.dest] = kwargs[action.dest]
- consumed_kw.append(action.dest)
+ self._flag_args.append(action.dest)
else:
# This is a positional argument, search the positional arguments passed in.
if not isinstance(action, argparse._SubParsersAction):
@@ -164,6 +167,10 @@ class ArgparseFunctor:
elif action.nargs == '*':
self._args[action.dest] = args[next_pos_index:next_pos_index + pos_remain]
next_pos_index += pos_remain
+ elif action.nargs == argparse.REMAINDER:
+ self._args[action.dest] = args[next_pos_index:next_pos_index + pos_remain]
+ next_pos_index += pos_remain
+ self._remainder_arg = action.dest
elif action.nargs == '?':
self._args[action.dest] = args[next_pos_index]
next_pos_index += 1
@@ -175,7 +182,7 @@ class ArgparseFunctor:
# Check if there are any extra arguments we don't know how to handle
for kw in kwargs:
- if kw not in self._args: # consumed_kw:
+ if kw not in self._args:
raise TypeError("{}() got an unexpected keyword argument '{}'".format(
self.__current_subcommand_parser.prog, kw))
@@ -194,7 +201,7 @@ class ArgparseFunctor:
# reconstruct the cmd2 command from the python call
cmd_str = ['']
- def process_flag(action, value):
+ def process_argument(action, value):
if isinstance(action, argparse._CountAction):
if isinstance(value, int):
for _ in range(value):
@@ -218,30 +225,61 @@ class ArgparseFunctor:
if isinstance(value, List) or isinstance(value, tuple):
for item in value:
item = str(item).strip()
- if ' ' in item:
- item = '"{}"'.format(item)
+ if token_resembles_flag(item, self._parser):
+ raise ValueError('{} appears to be a flag and should be supplied as a keyword argument '
+ 'to the function.'.format(item))
+ item = quote_string_if_needed(item)
cmd_str[0] += '{} '.format(item)
+
+ # If this is a flag parameter that can accept a variable number of arguments and we have not
+ # reached the max number, add a list completion suffix to tell argparse to move to the next
+ # parameter
+ if action.option_strings and isinstance(action, _RangeAction) and action.nargs_max is not None and \
+ action.nargs_max > len(value):
+ cmd_str[0] += '{0}{0} '.format(self._parser.prefix_chars[0])
+
else:
value = str(value).strip()
- if ' ' in value:
- value = '"{}"'.format(value)
+ if token_resembles_flag(value, self._parser):
+ raise ValueError('{} appears to be a flag and should be supplied as a keyword argument '
+ 'to the function.'.format(value))
+ value = quote_string_if_needed(value)
cmd_str[0] += '{} '.format(value)
+ # If this is a flag parameter that can accept a variable number of arguments and we have not
+ # reached the max number, add a list completion suffix to tell argparse to move to the next
+ # parameter
+ if action.option_strings and isinstance(action, _RangeAction) and action.nargs_max is not None and \
+ action.nargs_max > 1:
+ cmd_str[0] += '{0}{0} '.format(self._parser.prefix_chars[0])
+
+ def process_action(action):
+ if isinstance(action, argparse._SubParsersAction):
+ cmd_str[0] += '{} '.format(self._args[action.dest])
+ traverse_parser(action.choices[self._args[action.dest]])
+ elif isinstance(action, argparse._AppendAction):
+ if isinstance(self._args[action.dest], list) or isinstance(self._args[action.dest], tuple):
+ for values in self._args[action.dest]:
+ process_argument(action, values)
+ else:
+ process_argument(action, self._args[action.dest])
+ else:
+ process_argument(action, self._args[action.dest])
+
def traverse_parser(parser):
+ # first process optional flag arguments
for action in parser._actions:
- # was something provided for the argument
- if action.dest in self._args:
- if isinstance(action, argparse._SubParsersAction):
- cmd_str[0] += '{} '.format(self._args[action.dest])
- traverse_parser(action.choices[self._args[action.dest]])
- elif isinstance(action, argparse._AppendAction):
- if isinstance(self._args[action.dest], list) or isinstance(self._args[action.dest], tuple):
- for values in self._args[action.dest]:
- process_flag(action, values)
- else:
- process_flag(action, self._args[action.dest])
- else:
- process_flag(action, self._args[action.dest])
+ if action.dest in self._args and action.dest in self._flag_args and action.dest != self._remainder_arg:
+ process_action(action)
+ # next process positional arguments
+ for action in parser._actions:
+ if action.dest in self._args and action.dest not in self._flag_args and \
+ action.dest != self._remainder_arg:
+ process_action(action)
+ # Keep remainder argument last
+ for action in parser._actions:
+ if action.dest in self._args and action.dest == self._remainder_arg:
+ process_action(action)
traverse_parser(self._parser)
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 501733a9..d4a3db2f 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -255,10 +255,13 @@ class StdSim(object):
"""
class ByteBuf(object):
"""Inner class which stores an actual bytes buffer and does the actual output if echo is enabled."""
- def __init__(self, inner_stream, echo: bool = False) -> None:
+ def __init__(self, inner_stream, echo: bool = False,
+ encoding: str='utf-8', errors: str='replace') -> None:
self.byte_buf = b''
self.inner_stream = inner_stream
self.echo = echo
+ self.encoding = encoding
+ self.errors = errors
def write(self, b: bytes) -> None:
"""Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
@@ -266,7 +269,10 @@ class StdSim(object):
raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
self.byte_buf += b
if self.echo:
- self.inner_stream.buffer.write(b)
+ if hasattr(self.inner_stream, 'buffer'):
+ self.inner_stream.buffer.write(b)
+ else:
+ self.inner_stream.write(b.decode(encoding=self.encoding, errors=self.errors))
def __init__(self, inner_stream, echo: bool = False,
encoding: str='utf-8', errors: str='replace') -> None:
diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst
index 8aed7498..8931c60b 100644
--- a/docs/argument_processing.rst
+++ b/docs/argument_processing.rst
@@ -15,20 +15,20 @@ Argument Processing
These features are all provided by the ``@with_argparser`` decorator which is importable from ``cmd2``.
-See the either the argprint_ or argparse_ example to learn more about how to use the various ``cmd2`` argument
+See the either the argprint_ or decorator_ example to learn more about how to use the various ``cmd2`` argument
processing decorators in your ``cmd2`` applications.
.. _argprint: https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py
-.. _argparse: https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_example.py
+.. _decorator: https://github.com/python-cmd2/cmd2/blob/master/examples/decorator_example.py
Using the argument parser decorator
===================================
For each command in the ``cmd2`` subclass which requires argument parsing,
-create an instance of ``argparse.ArgumentParser()`` which can parse the
+create a unique instance of ``argparse.ArgumentParser()`` which can parse the
input appropriately for the command. Then decorate the command method with
the ``@with_argparser`` decorator, passing the argument parser as the
-first parameter to the decorator. This changes the second argumen to the command method, which will contain the results
+first parameter to the decorator. This changes the second argument to the command method, which will contain the results
of ``ArgumentParser.parse_args()``.
Here's what it looks like::
@@ -54,6 +54,16 @@ Here's what it looks like::
for i in range(min(repetitions, self.maxrepeats)):
self.poutput(arg)
+.. warning::
+
+ It is important that each command which uses the ``@with_argparser`` decorator be passed a unique instance of a
+ parser. This limitation is due to bugs in CPython prior to Python 3.7 which make it impossible to make a deep copy
+ of an instance of a ``argparse.ArgumentParser``.
+
+ See the table_display_ example for a work-around that demonstrates how to create a function which returns a unique
+ instance of the parser you want.
+
+
.. note::
The ``@with_argparser`` decorator sets the ``prog`` variable in
@@ -61,6 +71,8 @@ Here's what it looks like::
This will override anything you specify in ``prog`` variable when
creating the argument parser.
+.. _table_display: https://github.com/python-cmd2/cmd2/blob/master/examples/table_display.py
+
Help Messages
=============
@@ -159,6 +171,14 @@ Which yields:
This command can not generate tags with no content, like <br/>
+.. warning::
+
+ If a command **foo** is decorated with one of cmd2's argparse decorators, then **help_foo** will not
+ be invoked when ``help foo`` is called. The argparse_ module provides a rich API which can be used to
+ tweak every aspect of the displayed help and we encourage ``cmd2`` developers to utilize that.
+
+.. _argparse: https://docs.python.org/3/library/argparse.html
+
Grouping Commands
=================
diff --git a/examples/argparse_example.py b/examples/decorator_example.py
index 236e2af4..5b8b303b 100755
--- a/examples/argparse_example.py
+++ b/examples/decorator_example.py
@@ -1,14 +1,13 @@
#!/usr/bin/env python
# coding=utf-8
-"""A sample application for cmd2 showing how to use argparse to
+"""A sample application showing how to use cmd2's argparse decorators to
process command line arguments for your application.
Thanks to cmd2's built-in transcript testing capability, it also
-serves as a test suite for argparse_example.py when used with the
-exampleSession.txt transcript.
+serves as a test suite when used with the exampleSession.txt transcript.
-Running `python argparse_example.py -t exampleSession.txt` will run
-all the commands in the transcript against argparse_example.py,
+Running `python decorator_example.py -t exampleSession.txt` will run
+all the commands in the transcript against decorator_example.py,
verifying that the output produced matches the transcript.
"""
import argparse
diff --git a/main.py b/examples/hello_cmd2.py
index 56383f66..397856a6 100755
--- a/main.py
+++ b/examples/hello_cmd2.py
@@ -1,5 +1,8 @@
#!/usr/bin/env python
# coding=utf-8
+"""
+This is intended to be a completely bare-bones cmd2 application suitable for rapid testing and debugging.
+"""
from cmd2 import cmd2
if __name__ == '__main__':
@@ -8,5 +11,6 @@ if __name__ == '__main__':
# Set "use_ipython" to True to include the ipy command if IPython is installed, which supports advanced interactive
# debugging of your application via introspection on self.
app = cmd2.Cmd(use_ipython=True, persistent_history_file='cmd2_history.txt')
- app.locals_in_py = True
+ app.locals_in_py = True # Enable access to "self" within the py command
+ app.debug = True # Show traceback if/when an exception occurs
app.cmdloop()
diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py
index ef283e9e..571b4082 100755
--- a/examples/tab_autocompletion.py
+++ b/examples/tab_autocompletion.py
@@ -298,6 +298,8 @@ class TabCompleteExample(cmd2.Cmd):
.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:
@@ -336,7 +338,7 @@ class TabCompleteExample(cmd2.Cmd):
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='*')
+ 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')
diff --git a/examples/table_display.py b/examples/table_display.py
index 63447377..7541e548 100755
--- a/examples/table_display.py
+++ b/examples/table_display.py
@@ -11,7 +11,6 @@ You can quit out of the pager by typing "q". You can also search for text withi
WARNING: This example requires the tableformatter module: https://github.com/python-tableformatter/tableformatter
- pip install tableformatter
"""
-import argparse
from typing import Tuple
import cmd2
@@ -142,6 +141,21 @@ def high_density_objs(row_obj: CityInfo) -> dict:
return opts
+def make_table_parser() -> cmd2.argparse_completer.ACArgumentParser:
+ """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_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')
+ table_item_group.add_argument('-s', '--sparse', action='store_true', help='Sparse Grid')
+ return table_parser
+
+
class TableDisplay(cmd2.Cmd):
"""Example cmd2 application showing how you can display tabular data."""
@@ -169,18 +183,12 @@ class TableDisplay(cmd2.Cmd):
formatted_table = tf.generate_table(rows=rows, columns=columns, grid_style=grid, row_tagger=row_stylist)
self.ppaged(formatted_table, chop=True)
- table_parser = argparse.ArgumentParser()
- 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')
- table_item_group.add_argument('-s', '--sparse', action='store_true', help='Sparse Grid')
-
- @cmd2.with_argparser(table_parser)
+ @cmd2.with_argparser(make_table_parser())
def do_table(self, args):
"""Display data in iterable form on the Earth's most populated cities in a table."""
self.ptable(EXAMPLE_ITERABLE_DATA, COLUMNS, args, high_density_tuples)
- @cmd2.with_argparser(table_parser)
+ @cmd2.with_argparser(make_table_parser())
def do_object_table(self, args):
"""Display data in object form on the Earth's most populated cities in a table."""
self.ptable(EXAMPLE_OBJECT_DATA, OBJ_COLS, args, high_density_objs)
diff --git a/examples/transcripts/exampleSession.txt b/examples/transcripts/exampleSession.txt
index 38fb0659..8fa7c9bb 100644
--- a/examples/transcripts/exampleSession.txt
+++ b/examples/transcripts/exampleSession.txt
@@ -1,4 +1,4 @@
-# Run this transcript with "python argparse_example.py -t exampleSession.txt"
+# Run this transcript with "python decorator_example.py -t exampleSession.txt"
# The regex for colors is because no color on Windows.
# The regex for editor will match whatever program you use.
# regexes on prompts just make the trailing space obvious
diff --git a/tests/test_acargparse.py b/tests/test_acargparse.py
index be3e8b97..617afd4f 100644
--- a/tests/test_acargparse.py
+++ b/tests/test_acargparse.py
@@ -5,7 +5,7 @@ Copyright 2018 Eric Lin <anselor@gmail.com>
Released under MIT license, see LICENSE file
"""
import pytest
-from cmd2.argparse_completer import ACArgumentParser
+from cmd2.argparse_completer import ACArgumentParser, token_resembles_flag
def test_acarg_narg_empty_tuple():
@@ -51,3 +51,18 @@ def test_acarg_narg_tuple_zero_base():
def test_acarg_narg_tuple_zero_to_one():
parser = ACArgumentParser(prog='test')
parser.add_argument('tuple', nargs=(0, 1))
+
+
+def test_token_resembles_flag():
+ parser = ACArgumentParser()
+
+ # Not valid flags
+ assert not token_resembles_flag('', parser)
+ assert not token_resembles_flag('non-flag', parser)
+ assert not token_resembles_flag('-', parser)
+ assert not token_resembles_flag('--has space', parser)
+ assert not token_resembles_flag('-2', parser)
+
+ # Valid flags
+ assert token_resembles_flag('-flag', parser)
+ assert token_resembles_flag('--flag', parser)
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
index c6c1d1f6..3473ab38 100644
--- a/tests/test_autocompletion.py
+++ b/tests/test_autocompletion.py
@@ -36,7 +36,7 @@ optional arguments:
MEDIA_MOVIES_ADD_HELP = '''Usage: media movies add -d DIRECTOR{1..2}
[-h]
- title {G, PG, PG-13, R, NC-17} [actor [...]]
+ title {G, PG, PG-13, R, NC-17} ...
positional arguments:
title Movie Title
diff --git a/tests/test_completion.py b/tests/test_completion.py
index d722e534..ed36eb01 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -716,6 +716,30 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app):
os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \
cmd2_app.display_matches == expected_display
+def test_argparse_remainder_completion(cmd2_app):
+ # First test a 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 sections
+ assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
+
+ # Now 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 sections
+ assert complete_tester(text, line, begidx, endidx, cmd2_app) is None
@pytest.fixture
def sc_app():
diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py
index 6b72e940..bcb72a3b 100644
--- a/tests/test_pyscript.py
+++ b/tests/test_pyscript.py
@@ -9,7 +9,7 @@ import os
import pytest
from cmd2.cmd2 import Cmd, with_argparser
from cmd2 import argparse_completer
-from .conftest import run_cmd
+from .conftest import run_cmd, normalize
from cmd2.utils import namedtuple_with_defaults, StdSim
@@ -234,3 +234,20 @@ def test_pyscript_custom_name(ps_echo, request):
out = run_cmd(ps_echo, 'pyscript {}'.format(python_script))
assert out
assert message == out[0]
+
+
+def test_pyscript_argparse_checks(ps_app, capsys):
+ # Test command that has nargs.REMAINDER and make sure all tokens are accepted
+ run_cmd(ps_app, 'py app.alias.create("my_alias", "alias_command", "command_arg1", "command_arg2")')
+ out = run_cmd(ps_app, 'alias list my_alias')
+ assert out == normalize('alias create my_alias alias_command command_arg1 command_arg2')
+
+ # Specify flag outside of keyword argument
+ run_cmd(ps_app, 'py app.help("-h")')
+ _, err = capsys.readouterr()
+ assert '-h appears to be a flag' in err
+
+ # Specify list with flag outside of keyword argument
+ run_cmd(ps_app, 'py app.help(["--help"])')
+ _, err = capsys.readouterr()
+ assert '--help appears to be a flag' in err
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 43a05a9a..807bc0fd 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -119,11 +119,23 @@ def stdout_sim():
stdsim = cu.StdSim(sys.stdout)
return stdsim
+@pytest.fixture
+def stringio_sim():
+ import io
+ stdsim = cu.StdSim(io.StringIO(), echo=True)
+ return stdsim
+
+
def test_stdsim_write_str(stdout_sim):
my_str = 'Hello World'
stdout_sim.write(my_str)
assert stdout_sim.getvalue() == my_str
+def test_stdsim_write_str_inner_no_buffer(stringio_sim):
+ my_str = 'Hello World'
+ stringio_sim.write(my_str)
+ assert stringio_sim.getvalue() == my_str
+
def test_stdsim_write_bytes(stdout_sim):
b_str = b'Hello World'
with pytest.raises(TypeError):