summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2019-03-02 12:54:30 -0500
committerTodd Leonhardt <todd.leonhardt@gmail.com>2019-03-02 12:54:30 -0500
commit46df1c127e11ac59706e6656543d06621fd7bc1e (patch)
treeba8e057450e426db76b1398bb288ba0c2cc7c23c
parentfe4b3fd6718963ce9fa2352b013d7f9c912059c3 (diff)
parentde701086ff832bad0f0d97ffb10c2159d56ede7d (diff)
downloadcmd2-git-46df1c127e11ac59706e6656543d06621fd7bc1e.tar.gz
Merged from master and resolved conflicts in cmd2.py
-rw-r--r--CHANGELOG.md7
-rwxr-xr-xREADME.md2
-rw-r--r--cmd2/argparse_completer.py2
-rw-r--r--cmd2/cmd2.py33
-rw-r--r--cmd2/pyscript_bridge.py308
-rw-r--r--cmd2/utils.py6
-rw-r--r--tests/pyscript/bar1.py3
-rw-r--r--tests/pyscript/custom_echo.py3
-rw-r--r--tests/pyscript/foo1.py3
-rw-r--r--tests/pyscript/foo2.py3
-rw-r--r--tests/pyscript/foo3.py3
-rw-r--r--tests/pyscript/foo4.py10
-rw-r--r--tests/pyscript/help.py5
-rw-r--r--tests/pyscript/help_media.py3
-rw-r--r--tests/pyscript/media_movies_add1.py3
-rw-r--r--tests/pyscript/media_movies_add2.py3
-rw-r--r--tests/pyscript/media_movies_list1.py3
-rw-r--r--tests/pyscript/media_movies_list2.py3
-rw-r--r--tests/pyscript/media_movies_list3.py3
-rw-r--r--tests/pyscript/media_movies_list4.py3
-rw-r--r--tests/pyscript/media_movies_list5.py3
-rw-r--r--tests/pyscript/media_movies_list6.py3
-rw-r--r--tests/pyscript/media_movies_list7.py3
-rw-r--r--tests/pyscript/pyscript_dir.py (renamed from tests/pyscript/pyscript_dir1.py)0
-rw-r--r--tests/pyscript/pyscript_dir2.py4
-rw-r--r--tests/test_pyscript.py244
26 files changed, 66 insertions, 600 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 148ccda1..1139853f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,15 +1,16 @@
## 0.9.11 (TBD, 2019)
* Enhancements
* Added ``matches_sort_key`` to override the default way tab completion matches are sorted
-* Deprecations
- * Deprecated support for bash completion since this feature had slow performance. Also it relied on
- ``AutoCompleter`` which has since developed a dependency on ``cmd2`` methods.
* Potentially breaking changes
* Made ``cmd2_app`` a positional and required argument of ``AutoCompleter`` since certain functionality now
requires that it can't be ``None``.
* ``AutoCompleter`` no longer assumes ``CompletionItem`` results are sorted. Therefore you should follow the
``cmd2`` convention of setting ``self.matches_sorted`` to True before returning the results if you have already
sorted the ``CompletionItem`` list. Otherwise it will be sorted using ``self.matches_sort_key``.
+ * Removed support for bash completion since this feature had slow performance. Also it relied on
+ ``AutoCompleter`` which has since developed a dependency on ``cmd2`` methods.
+ * Removed ability to call commands in ``pyscript`` as if they were functions (e.g ``app.help()``) in favor
+ of only supporting one ``pyscript`` interface. This simplifies future maintenance.
## 0.9.10 (February 22, 2019)
* Bug Fixes
diff --git a/README.md b/README.md
index 81adf4c2..6b33ab84 100755
--- a/README.md
+++ b/README.md
@@ -117,7 +117,7 @@ Instructions for implementing each feature follow.
- Syntax for calling `cmd2` commands in a `pyscript` is essentially identical to what they would enter on the command line
- See the [Python](https://cmd2.readthedocs.io/en/latest/freefeatures.html#python) section of the `cmd2` docs for more info
- Also see the [python_scripting.py](https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py)
- example in conjunciton with the [conditional.py](https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/conditional.py) script
+ example in conjunction with the [conditional.py](https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/conditional.py) script
- Parsing commands with `argparse`
- Two decorators provide built-in capability for using `argparse.ArgumentParser` to parse command arguments
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 891622d1..7b466342 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -988,7 +988,7 @@ class ACArgumentParser(argparse.ArgumentParser):
self._custom_error_message = ''
# Begin cmd2 customization
- def set_custom_message(self, custom_message: str='') -> None:
+ 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
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 21fb9d79..c404ee1d 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -177,7 +177,7 @@ def with_category(category: str) -> Callable:
def with_argument_list(func: Callable[[Statement], Optional[bool]],
- preserve_quotes: bool=False) -> Callable[[List], Optional[bool]]:
+ preserve_quotes: bool = False) -> Callable[[List], Optional[bool]]:
"""A decorator to alter the arguments passed to a do_* cmd2 method. Default passes a string of whatever the user
typed. With this decorator, the decorated method will receive a list of arguments parsed from user input using
shlex.split().
@@ -197,7 +197,7 @@ def with_argument_list(func: Callable[[Statement], Optional[bool]],
return cmd_wrapper
-def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve_quotes: bool=False) -> \
+def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve_quotes: bool = False) -> \
Callable[[argparse.Namespace, List], Optional[bool]]:
"""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.
@@ -240,7 +240,7 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve
def with_argparser(argparser: argparse.ArgumentParser,
- preserve_quotes: bool=False) -> Callable[[argparse.Namespace], Optional[bool]]:
+ preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
with the given instance of argparse.ArgumentParser.
@@ -342,9 +342,9 @@ class Cmd(cmd.Cmd):
'quiet': "Don't print nonessential feedback",
'timing': 'Report execution times'}
- def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_history_file: str='',
- persistent_history_length: int=1000, startup_script: Optional[str]=None, use_ipython: bool=False,
- transcript_files: Optional[List[str]]=None) -> None:
+ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent_history_file: str = '',
+ persistent_history_length: int = 1000, startup_script: Optional[str] = None, use_ipython: bool = False,
+ transcript_files: Optional[List[str]] = None) -> None:
"""An easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
:param completekey: (optional) readline name of a completion key, default to Tab
@@ -562,7 +562,7 @@ class Cmd(cmd.Cmd):
msg = utils.strip_ansi(msg)
fileobj.write(msg)
- def poutput(self, msg: Any, end: str='\n', color: str='') -> None:
+ def poutput(self, msg: Any, end: str = '\n', color: str = '') -> None:
"""Smarter self.stdout.write(); color aware and adds newline of not present.
Also handles BrokenPipeError exceptions for when a commands's output has
@@ -590,8 +590,8 @@ class Cmd(cmd.Cmd):
if self.broken_pipe_warning:
sys.stderr.write(self.broken_pipe_warning)
- def perror(self, err: Union[str, Exception], traceback_war: bool=True, err_color: str=Fore.LIGHTRED_EX,
- war_color: str=Fore.LIGHTYELLOW_EX) -> None:
+ def perror(self, err: Union[str, Exception], traceback_war: bool = True, err_color: str = Fore.LIGHTRED_EX,
+ war_color: str = Fore.LIGHTYELLOW_EX) -> None:
""" Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists.
:param err: an Exception or error message to print out
@@ -624,7 +624,7 @@ class Cmd(cmd.Cmd):
else:
self.decolorized_write(sys.stderr, "{}\n".format(msg))
- def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None:
+ def ppaged(self, msg: str, end: str = '\n', chop: bool = False) -> None:
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
Never uses a pager inside of a script (Python or text) or when output is being redirected or piped or when
@@ -906,7 +906,7 @@ class Cmd(cmd.Cmd):
def flag_based_complete(self, text: str, line: str, begidx: int, endidx: int,
flag_dict: Dict[str, Union[Iterable, Callable]],
- all_else: Union[None, Iterable, Callable]=None) -> List[str]:
+ all_else: Union[None, Iterable, Callable] = None) -> List[str]:
"""
Tab completes based on a particular flag preceding the token being completed
:param text: the string prefix we are attempting to match (all returned matches must begin with it)
@@ -1161,7 +1161,7 @@ class Cmd(cmd.Cmd):
return list(exes_set)
def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int,
- complete_blank: bool=False) -> List[str]:
+ complete_blank: bool = False) -> List[str]:
"""Performs completion of executables either in a user's path or a given path
: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
@@ -2587,7 +2587,7 @@ class Cmd(cmd.Cmd):
# No special behavior needed, delegate to cmd base class do_help()
super().do_help(args.command)
- def _help_menu(self, verbose: bool=False) -> None:
+ def _help_menu(self, verbose: bool = False) -> None:
"""Show a list of commands which help can be displayed for.
"""
# Get a sorted list of help topics
@@ -2730,7 +2730,8 @@ class Cmd(cmd.Cmd):
self._should_quit = True
return self._STOP_AND_EXIT
- def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str='Your choice? ') -> str:
+ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]],
+ prompt: str = 'Your choice? ') -> str:
"""Presents a numbered menu to the user. Modeled after
the bash shell's SELECT. Returns the item chosen.
@@ -2786,7 +2787,7 @@ class Cmd(cmd.Cmd):
Output redirection and pipes allowed: {}"""
return read_only_settings.format(str(self.terminators), self.allow_cli_args, self.allow_redirection)
- def show(self, args: argparse.Namespace, parameter: str='') -> None:
+ def show(self, args: argparse.Namespace, parameter: str = '') -> None:
"""Shows current settings of parameters.
:param args: argparse parsed arguments from the set command
@@ -3604,7 +3605,7 @@ class Cmd(cmd.Cmd):
else:
raise RuntimeError("another thread holds terminal_lock")
- def cmdloop(self, intro: Optional[str]=None) -> None:
+ def cmdloop(self, intro: Optional[str] = None) -> None:
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
_cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 6a18fc6a..6c14ff1d 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -7,12 +7,10 @@ Copyright 2018 Eric Lin <anselor@gmail.com>
Released under MIT license, see LICENSE file
"""
-import argparse
import sys
-from typing import List, Optional
+from typing import Optional
-from .argparse_completer import _RangeAction, is_potential_flag
-from .utils import namedtuple_with_defaults, StdSim, quote_string_if_needed
+from .utils import namedtuple_with_defaults, StdSim
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
if sys.version_info < (3, 5):
@@ -44,257 +42,6 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
return not self.stderr
-def _exec_cmd(cmd2_app, command: str, echo: bool) -> CommandResult:
- """
- Helper to encapsulate executing a command and capturing the results
- :param cmd2_app: cmd2 app that will run the command
- :param command: command line being run
- :param echo: if True, output will be echoed to stdout/stderr while the command runs
- :return: result of the command
- """
- copy_stdout = StdSim(sys.stdout, echo)
- copy_stderr = StdSim(sys.stderr, echo)
-
- copy_cmd_stdout = StdSim(cmd2_app.stdout, echo)
-
- cmd2_app._last_result = None
-
- try:
- cmd2_app.stdout = copy_cmd_stdout
- with redirect_stdout(copy_stdout):
- with redirect_stderr(copy_stderr):
- # Include a newline in case it's a multiline command
- cmd2_app.onecmd_plus_hooks(command + '\n')
- finally:
- cmd2_app.stdout = copy_cmd_stdout.inner_stream
-
- # if stderr is empty, set it to None
- stderr = copy_stderr.getvalue() if copy_stderr.getvalue() else None
-
- outbuf = copy_cmd_stdout.getvalue() if copy_cmd_stdout.getvalue() else copy_stdout.getvalue()
- result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result)
- return result
-
-
-class ArgparseFunctor:
- """
- Encapsulates translating Python object traversal
- """
- def __init__(self, echo: bool, cmd2_app, command_name: str, parser: argparse.ArgumentParser):
- self._echo = echo
- self._cmd2_app = cmd2_app
- self._command_name = command_name
- self._parser = parser
-
- # 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
-
- def __dir__(self):
- """Returns a custom list of attribute names to match the sub-commands"""
- commands = []
- for action in self.__current_subcommand_parser._actions:
- if not action.option_strings and isinstance(action, argparse._SubParsersAction):
- commands.extend(action.choices)
- return commands
-
- def __getattr__(self, item: str):
- """Search for a sub-command matching this item and update internal state to track the traversal"""
- # look for sub-command under the current command/sub-command layer
- for action in self.__current_subcommand_parser._actions:
- if not action.option_strings and isinstance(action, argparse._SubParsersAction):
- if item in action.choices:
- # item matches the a sub-command, save our position in argparse,
- # save the sub-command, return self to allow next level of traversal
- self.__current_subcommand_parser = action.choices[item]
- self._args[action.dest] = item
- return self
-
- raise AttributeError(item)
-
- def __call__(self, *args, **kwargs):
- """
- Process the arguments at this layer of the argparse command tree. If there are more sub-commands,
- return self to accept the next sub-command name. If there are no more sub-commands, execute the
- sub-command with the given parameters.
- """
- next_pos_index = 0
-
- has_subcommand = False
-
- # Iterate through the current sub-command's arguments in order
- for action in self.__current_subcommand_parser._actions:
- # is this a flag option?
- if action.option_strings:
- # 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]
- self._flag_args.append(action.dest)
- else:
- # This is a positional argument, search the positional arguments passed in.
- if not isinstance(action, argparse._SubParsersAction):
- if action.dest in kwargs:
- # if this positional argument happens to be passed in as a keyword argument
- # go ahead and consume the matching keyword argument
- self._args[action.dest] = kwargs[action.dest]
- elif next_pos_index < len(args):
- # Make sure we actually have positional arguments to consume
- pos_remain = len(args) - next_pos_index
-
- # Check if this argument consumes a range of values
- if isinstance(action, _RangeAction) and action.nargs_min is not None \
- and action.nargs_max is not None:
- # this is a cmd2 ranged action.
-
- if pos_remain >= action.nargs_min:
- # Do we meet the minimum count?
- if pos_remain > action.nargs_max:
- # Do we exceed the maximum count?
- self._args[action.dest] = args[next_pos_index:next_pos_index + action.nargs_max]
- next_pos_index += action.nargs_max
- else:
- self._args[action.dest] = args[next_pos_index:next_pos_index + pos_remain]
- next_pos_index += pos_remain
- else:
- raise ValueError('Expected at least {} values for {}'.format(action.nargs_min,
- action.dest))
- elif action.nargs is not None:
- if action.nargs == '+':
- if pos_remain > 0:
- self._args[action.dest] = args[next_pos_index:next_pos_index + pos_remain]
- next_pos_index += pos_remain
- else:
- raise ValueError('Expected at least 1 value for {}'.format(action.dest))
- 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
- else:
- self._args[action.dest] = args[next_pos_index]
- next_pos_index += 1
- else:
- has_subcommand = True
-
- # Check if there are any extra arguments we don't know how to handle
- for kw in kwargs:
- if kw not in self._args:
- raise TypeError("{}() got an unexpected keyword argument '{}'".format(
- self.__current_subcommand_parser.prog, kw))
-
- if has_subcommand:
- return self
- else:
- return self._run()
-
- def _run(self):
- # look up command function
- func = self._cmd2_app.cmd_func(self._command_name)
- if func is None:
- raise AttributeError("'{}' object has no command called '{}'".format(self._cmd2_app.__class__.__name__,
- self._command_name))
-
- # reconstruct the cmd2 command from the python call
- command = self._command_name
-
- def process_argument(action, value):
- nonlocal command
- if isinstance(action, argparse._CountAction):
- if isinstance(value, int):
- for _ in range(value):
- command += ' {}'.format(action.option_strings[0])
- return
- else:
- raise TypeError('Expected int for ' + action.dest)
- if isinstance(action, argparse._StoreConstAction) or isinstance(action, argparse._AppendConstAction):
- if value:
- # Nothing else to append to the command string, just the flag is enough.
- command += ' {}'.format(action.option_strings[0])
- return
- else:
- # value is not True so we default to false, which means don't include the flag
- return
-
- # was the argument a flag?
- if action.option_strings:
- command += ' {}'.format(action.option_strings[0])
-
- is_remainder_arg = action.dest == self._remainder_arg
-
- if isinstance(value, List) or isinstance(value, tuple):
- for item in value:
- item = str(item).strip()
- if not is_remainder_arg and is_potential_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)
- command += ' {}'.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 2 prefix chars (ex: --) to tell argparse to stop processing the
- # parameter. This also means the remaining arguments will be treated as positionals by argparse.
- if action.option_strings and isinstance(action, _RangeAction) and action.nargs_max is not None and \
- action.nargs_max > len(value):
- command += ' {0}{0}'.format(self._parser.prefix_chars[0])
-
- else:
- value = str(value).strip()
- if not is_remainder_arg and is_potential_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)
- command += ' {}'.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 2 prefix chars (ex: --) to tell argparse to stop processing the
- # parameter. This also means the remaining arguments will be treated as positionals by argparse.
- if action.option_strings and isinstance(action, _RangeAction) and action.nargs_max is not None and \
- action.nargs_max > 1:
- command += ' {0}{0}'.format(self._parser.prefix_chars[0])
-
- def process_action(action):
- nonlocal command
- if isinstance(action, argparse._SubParsersAction):
- command += ' {}'.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:
- 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)
- return _exec_cmd(self._cmd2_app, command, self._echo)
-
-
class PyscriptBridge(object):
"""Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for
application commands."""
@@ -303,37 +50,13 @@ class PyscriptBridge(object):
self._last_result = None
self.cmd_echo = False
- def __getattr__(self, item: str):
- """
- Provide functionality to call application commands as a method of PyscriptBridge
- ex: app.help()
- """
- func = self._cmd2_app.cmd_func(item)
-
- if func:
- if hasattr(func, 'argparser'):
- # Command uses argparse, return an object that can traverse the argparse subcommands and arguments
- return ArgparseFunctor(self.cmd_echo, self._cmd2_app, item, getattr(func, 'argparser'))
- else:
- # Command doesn't use argparse, we will accept parameters in the form of a command string
- def wrap_func(args=''):
- command = item
- if args:
- command += ' ' + args
- return _exec_cmd(self._cmd2_app, command, self.cmd_echo)
-
- return wrap_func
- else:
- # item does not refer to a command
- raise AttributeError("'{}' object has no attribute '{}'".format(self._cmd2_app.pyscript_name, item))
-
def __dir__(self):
"""Return a custom set of attribute names"""
- attributes = self._cmd2_app.get_all_commands()
+ attributes = []
attributes.insert(0, 'cmd_echo')
return attributes
- def __call__(self, command: str, echo: Optional[bool]=None) -> CommandResult:
+ def __call__(self, command: str, echo: Optional[bool] = None) -> CommandResult:
"""
Provide functionality to call application commands by calling PyscriptBridge
ex: app('help')
@@ -344,4 +67,25 @@ class PyscriptBridge(object):
if echo is None:
echo = self.cmd_echo
- return _exec_cmd(self._cmd2_app, command, echo)
+ copy_stdout = StdSim(sys.stdout, echo)
+ copy_stderr = StdSim(sys.stderr, echo)
+
+ copy_cmd_stdout = StdSim(self._cmd2_app.stdout, echo)
+
+ self._cmd2_app._last_result = None
+
+ try:
+ self._cmd2_app.stdout = copy_cmd_stdout
+ with redirect_stdout(copy_stdout):
+ with redirect_stderr(copy_stderr):
+ # Include a newline in case it's a multiline command
+ self._cmd2_app.onecmd_plus_hooks(command + '\n')
+ finally:
+ self._cmd2_app.stdout = copy_cmd_stdout.inner_stream
+
+ # if stderr is empty, set it to None
+ stderr = copy_stderr.getvalue() if copy_stderr.getvalue() else None
+
+ outbuf = copy_cmd_stdout.getvalue() if copy_cmd_stdout.getvalue() else copy_stdout.getvalue()
+ result = CommandResult(stdout=outbuf, stderr=stderr, data=self._cmd2_app._last_result)
+ return result
diff --git a/cmd2/utils.py b/cmd2/utils.py
index ae172fa4..098ed41d 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -68,7 +68,7 @@ def strip_quotes(arg: str) -> str:
def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]],
- default_values: collections.Iterable=()):
+ default_values: collections.Iterable = ()):
"""
Convenience function for defining a namedtuple with default values
@@ -268,7 +268,7 @@ 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,
- encoding: str='utf-8', errors: str='replace') -> None:
+ encoding: str = 'utf-8', errors: str = 'replace') -> None:
self.byte_buf = b''
self.inner_stream = inner_stream
self.echo = echo
@@ -284,7 +284,7 @@ class StdSim(object):
self.inner_stream.buffer.write(b)
def __init__(self, inner_stream, echo: bool = False,
- encoding: str='utf-8', errors: str='replace') -> None:
+ encoding: str = 'utf-8', errors: str = 'replace') -> None:
"""
Initializer
:param inner_stream: the emulated stream
diff --git a/tests/pyscript/bar1.py b/tests/pyscript/bar1.py
deleted file mode 100644
index 0f2b1e5e..00000000
--- a/tests/pyscript/bar1.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.bar('11', '22')
diff --git a/tests/pyscript/custom_echo.py b/tests/pyscript/custom_echo.py
deleted file mode 100644
index 3a79133a..00000000
--- a/tests/pyscript/custom_echo.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-custom.cmd_echo = True
-custom.echo('blah!')
diff --git a/tests/pyscript/foo1.py b/tests/pyscript/foo1.py
deleted file mode 100644
index 443282a5..00000000
--- a/tests/pyscript/foo1.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.foo('aaa', 'bbb', counter=3, trueval=True, constval=True)
diff --git a/tests/pyscript/foo2.py b/tests/pyscript/foo2.py
deleted file mode 100644
index 9aa37105..00000000
--- a/tests/pyscript/foo2.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.foo('11', '22', '33', '44', counter=3, trueval=True, constval=True)
diff --git a/tests/pyscript/foo3.py b/tests/pyscript/foo3.py
deleted file mode 100644
index e4384076..00000000
--- a/tests/pyscript/foo3.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.foo('11', '22', '33', '44', '55', '66', counter=3, trueval=False, constval=False)
diff --git a/tests/pyscript/foo4.py b/tests/pyscript/foo4.py
deleted file mode 100644
index a601ccd8..00000000
--- a/tests/pyscript/foo4.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-result = app.foo('aaa', 'bbb', counter=3)
-out_text = 'Fail'
-if result:
- data = result.data
- if 'aaa' in data.variable and 'bbb' in data.variable and data.counter == 3:
- out_text = 'Success'
-
-print(out_text)
diff --git a/tests/pyscript/help.py b/tests/pyscript/help.py
index 3f24246d..2e69d79f 100644
--- a/tests/pyscript/help.py
+++ b/tests/pyscript/help.py
@@ -1,3 +1,6 @@
# flake8: noqa F821
app.cmd_echo = True
-app.help()
+app('help')
+
+# Exercise py_quit() in unit test
+quit()
diff --git a/tests/pyscript/help_media.py b/tests/pyscript/help_media.py
deleted file mode 100644
index 38c4a2f8..00000000
--- a/tests/pyscript/help_media.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.help('media')
diff --git a/tests/pyscript/media_movies_add1.py b/tests/pyscript/media_movies_add1.py
deleted file mode 100644
index b5045a39..00000000
--- a/tests/pyscript/media_movies_add1.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.media.movies.add('My Movie', 'PG-13', director=('George Lucas', 'J. J. Abrams'))
diff --git a/tests/pyscript/media_movies_add2.py b/tests/pyscript/media_movies_add2.py
deleted file mode 100644
index 91dbbc6b..00000000
--- a/tests/pyscript/media_movies_add2.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.media.movies.add('My Movie', 'PG-13', actor=('Mark Hamill'), director=('George Lucas', 'J. J. Abrams'))
diff --git a/tests/pyscript/media_movies_list1.py b/tests/pyscript/media_movies_list1.py
deleted file mode 100644
index 505d1f91..00000000
--- a/tests/pyscript/media_movies_list1.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.media.movies.list()
diff --git a/tests/pyscript/media_movies_list2.py b/tests/pyscript/media_movies_list2.py
deleted file mode 100644
index 69e0d3c5..00000000
--- a/tests/pyscript/media_movies_list2.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.media().movies().list()
diff --git a/tests/pyscript/media_movies_list3.py b/tests/pyscript/media_movies_list3.py
deleted file mode 100644
index c4f0cc1e..00000000
--- a/tests/pyscript/media_movies_list3.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app('media movies list')
diff --git a/tests/pyscript/media_movies_list4.py b/tests/pyscript/media_movies_list4.py
deleted file mode 100644
index 29e98fe7..00000000
--- a/tests/pyscript/media_movies_list4.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.media.movies.list(actor='Mark Hamill')
diff --git a/tests/pyscript/media_movies_list5.py b/tests/pyscript/media_movies_list5.py
deleted file mode 100644
index 1c249ebf..00000000
--- a/tests/pyscript/media_movies_list5.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.media.movies.list(actor=('Mark Hamill', 'Carrie Fisher'))
diff --git a/tests/pyscript/media_movies_list6.py b/tests/pyscript/media_movies_list6.py
deleted file mode 100644
index c16ae6c5..00000000
--- a/tests/pyscript/media_movies_list6.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.media.movies.list(rating='PG')
diff --git a/tests/pyscript/media_movies_list7.py b/tests/pyscript/media_movies_list7.py
deleted file mode 100644
index d4ca7dca..00000000
--- a/tests/pyscript/media_movies_list7.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# flake8: noqa F821
-app.cmd_echo = True
-app.media.movies.list(rating=('PG', 'PG-13'))
diff --git a/tests/pyscript/pyscript_dir1.py b/tests/pyscript/pyscript_dir.py
index 81814d70..81814d70 100644
--- a/tests/pyscript/pyscript_dir1.py
+++ b/tests/pyscript/pyscript_dir.py
diff --git a/tests/pyscript/pyscript_dir2.py b/tests/pyscript/pyscript_dir2.py
deleted file mode 100644
index ebbbf712..00000000
--- a/tests/pyscript/pyscript_dir2.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# flake8: noqa F821
-out = dir(app.media)
-out.sort()
-print(out)
diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py
index 692a498b..78500185 100644
--- a/tests/test_pyscript.py
+++ b/tests/test_pyscript.py
@@ -1,255 +1,31 @@
# coding=utf-8
# flake8: noqa E302
"""
-Unit/functional testing for argparse completer in cmd2
-
-Copyright 2018 Eric Lin <anselor@gmail.com>
-Released under MIT license, see LICENSE file
+Unit/functional testing for pytest in cmd2
"""
import os
-import pytest
-from cmd2.cmd2 import Cmd, with_argparser
-from cmd2 import argparse_completer
-from .conftest import run_cmd, normalize
-from cmd2.utils import namedtuple_with_defaults, StdSim
-
-
-class PyscriptExample(Cmd):
- ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17']
-
- def _do_media_movies(self, args) -> None:
- if not args.command:
- self.do_help('media movies')
- else:
- self.poutput('media movies ' + str(args.__dict__))
-
- def _do_media_shows(self, args) -> None:
- if not args.command:
- self.do_help('media shows')
-
- if not args.command:
- self.do_help('media shows')
- else:
- self.poutput('media shows ' + str(args.__dict__))
-
- 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='*')
-
- movies_delete_parser = movies_commands_subparsers.add_parser('delete')
-
- 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')
-
- @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')
-
- foo_parser = argparse_completer.ACArgumentParser(prog='foo')
- foo_parser.add_argument('-c', dest='counter', action='count')
- foo_parser.add_argument('-t', dest='trueval', action='store_true')
- foo_parser.add_argument('-n', dest='constval', action='store_const', const=42)
- foo_parser.add_argument('variable', nargs=(2, 3))
- foo_parser.add_argument('optional', nargs='?')
- foo_parser.add_argument('zeroormore', nargs='*')
-
- @with_argparser(foo_parser)
- def do_foo(self, args):
- self.poutput('foo ' + str(sorted(args.__dict__)))
- if self._in_py:
- FooResult = namedtuple_with_defaults('FooResult',
- ['counter', 'trueval', 'constval',
- 'variable', 'optional', 'zeroormore'])
- self._last_result = FooResult(**{'counter': args.counter,
- 'trueval': args.trueval,
- 'constval': args.constval,
- 'variable': args.variable,
- 'optional': args.optional,
- 'zeroormore': args.zeroormore})
-
- bar_parser = argparse_completer.ACArgumentParser(prog='bar')
- bar_parser.add_argument('first')
- bar_parser.add_argument('oneormore', nargs='+')
- bar_parser.add_argument('-a', dest='aaa')
-
- @with_argparser(bar_parser)
- def do_bar(self, args):
- out = 'bar '
- arg_dict = args.__dict__
- keys = list(arg_dict.keys())
- keys.sort()
- out += '{'
- for key in keys:
- out += "'{}':'{}'".format(key, arg_dict[key])
- self.poutput(out)
-
-
-@pytest.fixture
-def ps_app():
- c = PyscriptExample()
- c.stdout = StdSim(c.stdout)
- return c
-
-
-class PyscriptCustomNameExample(Cmd):
- def __init__(self):
- super().__init__()
- self.pyscript_name = 'custom'
- def do_echo(self, out):
- self.poutput(out)
+from .conftest import run_cmd
-@pytest.fixture
-def ps_echo():
- c = PyscriptCustomNameExample()
- c.stdout = StdSim(c.stdout)
- return c
-
-
-@pytest.mark.parametrize('command, pyscript_file', [
- ('help', 'help.py'),
- ('help media', 'help_media.py'),
-])
-def test_pyscript_help(ps_app, request, command, pyscript_file):
+def test_pyscript_help(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
- python_script = os.path.join(test_dir, 'pyscript', pyscript_file)
- expected = run_cmd(ps_app, command)
+ python_script = os.path.join(test_dir, 'pyscript', 'help.py')
+ expected = run_cmd(base_app, 'help')
assert len(expected) > 0
assert len(expected[0]) > 0
- out = run_cmd(ps_app, 'pyscript {}'.format(python_script))
+ out = run_cmd(base_app, 'pyscript {}'.format(python_script))
assert len(out) > 0
assert out == expected
-@pytest.mark.parametrize('command, pyscript_file', [
- ('media movies list', 'media_movies_list1.py'),
- ('media movies list', 'media_movies_list2.py'),
- ('media movies list', 'media_movies_list3.py'),
- ('media movies list -a "Mark Hamill"', 'media_movies_list4.py'),
- ('media movies list -a "Mark Hamill" -a "Carrie Fisher"', 'media_movies_list5.py'),
- ('media movies list -r PG', 'media_movies_list6.py'),
- ('media movies list -r PG PG-13', 'media_movies_list7.py'),
- ('media movies add "My Movie" PG-13 --director "George Lucas" "J. J. Abrams"',
- 'media_movies_add1.py'),
- ('media movies add "My Movie" PG-13 --director "George Lucas" "J. J. Abrams" "Mark Hamill"',
- 'media_movies_add2.py'),
- ('foo aaa bbb -ccc -t -n', 'foo1.py'),
- ('foo 11 22 33 44 -ccc -t -n', 'foo2.py'),
- ('foo 11 22 33 44 55 66 -ccc', 'foo3.py'),
- ('bar 11 22', 'bar1.py'),
-])
-def test_pyscript_out(ps_app, request, command, pyscript_file):
+def test_pyscript_dir(base_app, capsys, request):
test_dir = os.path.dirname(request.module.__file__)
- python_script = os.path.join(test_dir, 'pyscript', pyscript_file)
- expected = run_cmd(ps_app, command)
- assert expected
-
- out = run_cmd(ps_app, 'pyscript {}'.format(python_script))
- assert out
- assert out == expected
-
-
-@pytest.mark.parametrize('command, error', [
- ('app.noncommand', 'AttributeError'),
- ('app.media.noncommand', 'AttributeError'),
- ('app.media.movies.list(artist="Invalid Keyword")', 'TypeError'),
- ('app.foo(counter="a")', 'TypeError'),
- ('app.foo("aaa")', 'ValueError'),
-])
-def test_pyscript_errors(ps_app, capsys, command, error):
- run_cmd(ps_app, 'py {}'.format(command))
- _, err = capsys.readouterr()
-
- assert len(err) > 0
- assert 'Traceback' in err
- assert error in err
-
-
-@pytest.mark.parametrize('pyscript_file, exp_out', [
- ('foo4.py', 'Success'),
-])
-def test_pyscript_results(ps_app, capsys, request, pyscript_file, exp_out):
- test_dir = os.path.dirname(request.module.__file__)
- python_script = os.path.join(test_dir, 'pyscript', pyscript_file)
-
- run_cmd(ps_app, 'pyscript {}'.format(python_script))
- expected, _ = capsys.readouterr()
- assert len(expected) > 0
- assert exp_out in expected
-
+ python_script = os.path.join(test_dir, 'pyscript', 'pyscript_dir.py')
-@pytest.mark.parametrize('expected, pyscript_file', [
- ("['_relative_load', 'alias', 'bar', 'cmd_echo', 'edit', 'eof', 'eos', 'foo', 'help', 'history', 'load', 'macro', 'media', 'py', 'pyscript', 'quit', 'set', 'shell', 'shortcuts']",
- 'pyscript_dir1.py'),
- ("['movies', 'shows']", 'pyscript_dir2.py')
-])
-def test_pyscript_dir(ps_app, capsys, request, expected, pyscript_file):
- test_dir = os.path.dirname(request.module.__file__)
- python_script = os.path.join(test_dir, 'pyscript', pyscript_file)
-
- run_cmd(ps_app, 'pyscript {}'.format(python_script))
+ run_cmd(base_app, 'pyscript {}'.format(python_script))
out, _ = capsys.readouterr()
out = out.strip()
assert len(out) > 0
- assert out == expected
-
-
-def test_pyscript_custom_name(ps_echo, request):
- message = 'blah!'
-
- test_dir = os.path.dirname(request.module.__file__)
- python_script = os.path.join(test_dir, 'pyscript', 'custom_echo.py')
-
- 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
- # Include a flag in the REMAINDER section to show that they are processed as literals in that section
- run_cmd(ps_app, 'py app.alias.create("my_alias", "alias_command", "command_arg1", "-h")')
- out = run_cmd(ps_app, 'alias list my_alias')
- assert out == normalize('alias create my_alias alias_command command_arg1 -h')
-
- # 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
+ assert out == "['cmd_echo']"