diff options
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 @@ -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']" |