diff options
-rw-r--r-- | CHANGELOG.md | 13 | ||||
-rw-r--r-- | CODEOWNERS | 1 | ||||
-rw-r--r-- | cmd2/argcomplete_bridge.py | 265 | ||||
-rw-r--r-- | cmd2/argparse_completer.py | 54 | ||||
-rw-r--r-- | cmd2/cmd2.py | 6 | ||||
-rwxr-xr-x | examples/bash_completion.py | 98 | ||||
-rwxr-xr-x | examples/tab_autocompletion.py | 5 | ||||
-rw-r--r-- | tests/test_bashcompletion.py | 257 |
8 files changed, 38 insertions, 661 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd78a51..7207f7f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.9.11 (TBD, 2019) +* Enhancements + * Simplified examples that illustrate ``argparse`` tab completion via ``AutoCompleter`` +* 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 return the results if you have already + sorted the ``CompletionItem`` list. Otherwise ``cmd2`` will just sort them alphabetically. + ## 0.9.10 (February 22, 2019) * Bug Fixes * Fixed unit test that hangs on Windows @@ -41,7 +41,6 @@ tests/conftest.py @kotfu @tleonhardt tests/test_acar*.py @anselor tests/test_argp*.py @kotfu tests/test_auto*.py @anselor -tests/test_bash*.py @anselor @tleonhardt tests/test_comp*.py @kmvanbrunt tests/test_pars*.py @kotfu tests/test_pysc*.py @anselor diff --git a/cmd2/argcomplete_bridge.py b/cmd2/argcomplete_bridge.py deleted file mode 100644 index 885cea31..00000000 --- a/cmd2/argcomplete_bridge.py +++ /dev/null @@ -1,265 +0,0 @@ -# coding=utf-8 -"""Hijack the ArgComplete's bash completion handler to return AutoCompleter results""" - -try: - # check if argcomplete is installed - import argcomplete -except ImportError: # pragma: no cover - # not installed, skip the rest of the file - DEFAULT_COMPLETER = None -else: - # argcomplete is installed - - # Newer versions of argcomplete have FilesCompleter at top level, older versions only have it under completers - try: - DEFAULT_COMPLETER = argcomplete.FilesCompleter() - except AttributeError: - DEFAULT_COMPLETER = argcomplete.completers.FilesCompleter() - - from cmd2.argparse_completer import ACTION_ARG_CHOICES, ACTION_SUPPRESS_HINT - from contextlib import redirect_stdout - import copy - from io import StringIO - import os - import shlex - import sys - from typing import List, Tuple, Union - - from . import constants - from . import utils - - def tokens_for_completion(line: str, endidx: int) -> Union[Tuple[List[str], List[str], int, int], - Tuple[None, None, None, None]]: - """ - Used by tab completion functions to get all tokens through the one being completed - :param line: the current input line with leading whitespace removed - :param endidx: the ending index of the prefix text - :return: A 4 item tuple where the items are - On Success - tokens: list of unquoted tokens - this is generally the list needed for tab completion functions - raw_tokens: list of tokens with any quotes preserved - this can be used to know if a token was quoted or is missing a closing quote - begidx: beginning of last token - endidx: cursor position - - Both lists are guaranteed to have at least 1 item - The last item in both lists is the token being tab completed - - On Failure - All 4 items are None - """ - unclosed_quote = '' - quotes_to_try = copy.copy(constants.QUOTES) - - tmp_line = line[:endidx] - tmp_endidx = endidx - - # Parse the line into tokens - while True: - try: - # Use non-POSIX parsing to keep the quotes around the tokens - initial_tokens = shlex.split(tmp_line[:tmp_endidx], posix=False) - - # calculate begidx - if unclosed_quote: - begidx = tmp_line[:tmp_endidx].rfind(initial_tokens[-1]) + 1 - else: - if tmp_endidx > 0 and tmp_line[tmp_endidx - 1] == ' ': - begidx = endidx - else: - begidx = tmp_line[:tmp_endidx].rfind(initial_tokens[-1]) - - # If the cursor is at an empty token outside of a quoted string, - # then that is the token being completed. Add it to the list. - if not unclosed_quote and begidx == tmp_endidx: - initial_tokens.append('') - break - except ValueError: - # ValueError can be caused by missing closing quote - if not quotes_to_try: # pragma: no cover - # Since we have no more quotes to try, something else - # is causing the parsing error. Return None since - # this means the line is malformed. - return None, None, None, None - - # Add a closing quote and try to parse again - unclosed_quote = quotes_to_try[0] - quotes_to_try = quotes_to_try[1:] - - tmp_line = line[:endidx] - tmp_line += unclosed_quote - tmp_endidx = endidx + 1 - - raw_tokens = initial_tokens - - # Save the unquoted tokens - tokens = [utils.strip_quotes(cur_token) for cur_token in raw_tokens] - - # If the token being completed had an unclosed quote, we need - # to remove the closing quote that was added in order for it - # to match what was on the command line. - if unclosed_quote: - raw_tokens[-1] = raw_tokens[-1][:-1] - - return tokens, raw_tokens, begidx, endidx - - class CompletionFinder(argcomplete.CompletionFinder): - """Hijack the functor from argcomplete to call AutoCompleter""" - - def __call__(self, argument_parser, completer=None, always_complete_options=True, exit_method=os._exit, - output_stream=None, exclude=None, validator=None, print_suppressed=False, append_space=None, - default_completer=DEFAULT_COMPLETER): - """ - :param argument_parser: The argument parser to autocomplete on - :type argument_parser: :class:`argparse.ArgumentParser` - :param always_complete_options: - Controls the autocompletion of option strings if an option string opening character (normally ``-``) has - not been entered. If ``True`` (default), both short (``-x``) and long (``--x``) option strings will be - suggested. If ``False``, no option strings will be suggested. If ``long``, long options and short - options with no long variant will be suggested. If ``short``, short options and long options with no - short variant will be suggested. - :type always_complete_options: boolean or string - :param exit_method: - Method used to stop the program after printing completions. Defaults to :meth:`os._exit`. If you want to - perform a normal exit that calls exit handlers, use :meth:`sys.exit`. - :type exit_method: callable - :param exclude: List of strings representing options to be omitted from autocompletion - :type exclude: iterable - :param validator: - Function to filter all completions through before returning (called with two string arguments, - completion and prefix; return value is evaluated as a boolean) - :type validator: callable - :param print_suppressed: - Whether or not to autocomplete options that have the ``help=argparse.SUPPRESS`` keyword argument set. - :type print_suppressed: boolean - :param append_space: - Whether to append a space to unique matches. The default is ``True``. - :type append_space: boolean - - .. note:: - If you are not subclassing CompletionFinder to override its behaviors, - use ``argcomplete.autocomplete()`` directly. It has the same signature as this method. - - Produces tab completions for ``argument_parser``. See module docs for more info. - - Argcomplete only executes actions if their class is known not to have side effects. Custom action classes - can be added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer - argument, or their execution is otherwise desirable. - """ - # Older versions of argcomplete have fewer keyword arguments - if sys.version_info >= (3, 5): - self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude, - validator=validator, print_suppressed=print_suppressed, append_space=append_space, - default_completer=default_completer) - else: - self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude, - validator=validator, print_suppressed=print_suppressed) - - if "_ARGCOMPLETE" not in os.environ: - # not an argument completion invocation - return - - try: - argcomplete.debug_stream = os.fdopen(9, "w") - except IOError: - argcomplete.debug_stream = sys.stderr - - if output_stream is None: - try: - output_stream = os.fdopen(8, "wb") - except IOError: - argcomplete.debug("Unable to open fd 8 for writing, quitting") - exit_method(1) - - ifs = os.environ.get("_ARGCOMPLETE_IFS", "\013") - if len(ifs) != 1: - argcomplete.debug("Invalid value for IFS, quitting [{v}]".format(v=ifs)) - exit_method(1) - - comp_line = os.environ["COMP_LINE"] - comp_point = int(os.environ["COMP_POINT"]) - - comp_line = argcomplete.ensure_str(comp_line) - - ############################## - # SWAPPED FOR AUTOCOMPLETER - # - # Replaced with our own tokenizer function - ############################## - tokens, _, begidx, endidx = tokens_for_completion(comp_line, comp_point) - - # _ARGCOMPLETE is set by the shell script to tell us where comp_words - # should start, based on what we're completing. - # 1: <script> [args] - # 2: python <script> [args] - # 3: python -m <module> [args] - start = int(os.environ["_ARGCOMPLETE"]) - 1 - ############################## - # SWAPPED FOR AUTOCOMPLETER - # - # Applying the same token dropping to our tokens - ############################## - # comp_words = comp_words[start:] - tokens = tokens[start:] - - # debug("\nLINE: {!r}".format(comp_line), - # "\nPOINT: {!r}".format(comp_point), - # "\nPREQUOTE: {!r}".format(cword_prequote), - # "\nPREFIX: {!r}".format(cword_prefix), - # "\nSUFFIX: {!r}".format(cword_suffix), - # "\nWORDS:", comp_words) - - ############################## - # SWAPPED FOR AUTOCOMPLETER - # - # Replaced with our own completion function and customizing the returned values - ############################## - # completions = self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos) - - # capture stdout from the autocompleter - result = StringIO() - with redirect_stdout(result): - completions = completer.complete_command(tokens, tokens[-1], comp_line, begidx, endidx) - outstr = result.getvalue() - - if completions: - # If any completion has a space in it, then quote all completions - # this improves the user experience so they don't nede to go back and add a quote - if ' ' in ''.join(completions): - completions = ['"{}"'.format(entry) for entry in completions] - - argcomplete.debug("\nReturning completions:", completions) - - output_stream.write(ifs.join(completions).encode(argcomplete.sys_encoding)) - elif outstr: - # if there are no completions, but we got something from stdout, try to print help - # trick the bash completion into thinking there are 2 completions that are unlikely - # to ever match. - - comp_type = int(os.environ["COMP_TYPE"]) - if comp_type == 63: # type is 63 for second tab press - print(outstr.rstrip(), file=argcomplete.debug_stream, end='') - - if completions is not None: - output_stream.write(ifs.join([ifs, ' ']).encode(argcomplete.sys_encoding)) - else: - output_stream.write(ifs.join([]).encode(argcomplete.sys_encoding)) - else: - # if completions is None we assume we don't know how to handle it so let bash - # go forward with normal filesystem completion - output_stream.write(ifs.join([]).encode(argcomplete.sys_encoding)) - output_stream.flush() - argcomplete.debug_stream.flush() - exit_method(0) - - def bash_complete(action, show_hint: bool = True): - """Helper function to configure an argparse action to fall back to bash completion. - - This function tags a parameter for bash completion, bypassing the autocompleter (for file input). - """ - def complete_none(*args, **kwargs): - return None - - setattr(action, ACTION_SUPPRESS_HINT, not show_hint) - setattr(action, ACTION_ARG_CHOICES, (complete_none,)) diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 18549e9e..c9293b4e 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -17,7 +17,7 @@ How to supply completion options for each argument: parser = argparse.ArgumentParser() parser.add_argument('-o', '--options', dest='options') choices = {} - mycompleter = AutoCompleter(parser, completer, 1, choices) + mycompleter = AutoCompleter(parser, cmd2_app, completer, 1, choices) - static list - provide a static list for each argument name ex: @@ -261,21 +261,21 @@ class AutoCompleter(object): def __init__(self, parser: argparse.ArgumentParser, + cmd2_app, token_start_index: int = 1, arg_choices: Dict[str, Union[List, Tuple, Callable]] = None, subcmd_args_lookup: dict = None, - tab_for_arg_help: bool = True, - cmd2_app=None) -> None: + tab_for_arg_help: bool = True) -> None: """ Create an AutoCompleter :param parser: ArgumentParser instance + :param cmd2_app: reference to the Cmd2 application. Enables argparse argument completion with class methods :param token_start_index: index of the token to start parsing at :param arg_choices: dictionary mapping from argparse argument 'dest' name to list of choices :param subcmd_args_lookup: mapping a sub-command group name to a tuple to fill the child\ AutoCompleter's arg_choices and subcmd_args_lookup parameters :param tab_for_arg_help: Enable of disable argument help when there's no completion result - :param cmd2_app: reference to the Cmd2 application. Enables argparse argument completion with class methods """ if not subcmd_args_lookup: subcmd_args_lookup = {} @@ -283,10 +283,10 @@ class AutoCompleter(object): else: forward_arg_choices = False self._parser = parser + self._cmd2_app = cmd2_app self._arg_choices = arg_choices.copy() if arg_choices is not None else {} self._token_start_index = token_start_index self._tab_for_arg_help = tab_for_arg_help - self._cmd2_app = cmd2_app self._flags = [] # all flags in this command self._flags_without_args = [] # all flags that don't take arguments @@ -328,11 +328,12 @@ class AutoCompleter(object): subcmd in args_for_action else \ (arg_choices, subcmd_args_lookup) if forward_arg_choices else ({}, {}) subcmd_start = token_start_index + len(self._positional_actions) - sub_completers[subcmd] = AutoCompleter(action.choices[subcmd], subcmd_start, + sub_completers[subcmd] = AutoCompleter(action.choices[subcmd], + cmd2_app, + token_start_index=subcmd_start, arg_choices=subcmd_args, subcmd_args_lookup=subcmd_lookup, - tab_for_arg_help=tab_for_arg_help, - cmd2_app=cmd2_app) + tab_for_arg_help=tab_for_arg_help) sub_commands.append(subcmd) self._positional_completers[action.dest] = sub_completers self._arg_choices[action.dest] = sub_commands @@ -558,8 +559,8 @@ class AutoCompleter(object): # a flag prefix then we'll complete the list of flag options if not flag_arg.needed and len(tokens[-1]) > 0 and tokens[-1][0] in self._parser.prefix_chars and \ not skip_remaining_flags: - return AutoCompleter.basic_complete(text, line, begidx, endidx, - [flag for flag in self._flags if flag not in matched_flags]) + return self._cmd2_app.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 elif not current_is_positional: if flag_action is not None: @@ -610,7 +611,6 @@ class AutoCompleter(object): self._cmd2_app.completion_header = header self._cmd2_app.display_matches = completions_with_desc - self._cmd2_app.matches_sorted = True return completions @@ -625,7 +625,7 @@ class AutoCompleter(object): if token in completers: return completers[token].complete_command_help(tokens, text, line, begidx, endidx) else: - return self.basic_complete(text, line, begidx, endidx, completers.keys()) + return self._cmd2_app.basic_complete(text, line, begidx, endidx, completers.keys()) return [] def format_help(self, tokens: List[str]) -> str: @@ -687,8 +687,8 @@ class AutoCompleter(object): # assume this is due to an incorrect function signature, return nothing. return [] else: - return AutoCompleter.basic_complete(text, line, begidx, endidx, - self._resolve_choices_for_arg(action, used_values)) + return self._cmd2_app.basic_complete(text, line, begidx, endidx, + self._resolve_choices_for_arg(action, used_values)) return [] @@ -708,12 +708,9 @@ class AutoCompleter(object): # is the provided argument a callable. If so, call it if callable(args): try: - if self._cmd2_app is not None: - try: - args = args(self._cmd2_app) - except TypeError: - args = args() - else: + try: + args = args(self._cmd2_app) + except TypeError: args = args() except TypeError: return [] @@ -781,21 +778,6 @@ class AutoCompleter(object): # Redraw prompt and input line rl_force_redisplay() - # noinspection PyUnusedLocal - @staticmethod - def basic_complete(text: str, line: str, begidx: int, endidx: int, match_against: List[str]) -> List[str]: - """ - Performs tab completion against a list - - :param text: the string prefix we are attempting to match (all returned matches must begin with it) - :param line: the current input line with leading whitespace removed - :param begidx: the beginning index of the prefix text - :param endidx: the ending index of the prefix text - :param match_against: the list being matched against - :return: a list of possible tab completions - """ - return [cur_match for cur_match in match_against if cur_match.startswith(text)] - ############################################################################### # Unless otherwise noted, everything below this point are copied from Python's @@ -1142,7 +1124,7 @@ class ACArgumentParser(argparse.ArgumentParser): # all args after -- are non-options if arg_string == '--': arg_string_pattern_parts.append('-') - for arg_string in arg_strings_iter: + for cur_string in arg_strings_iter: arg_string_pattern_parts.append('A') # otherwise, add the arg to the arg strings diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 03fc719e..1f981417 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1574,7 +1574,7 @@ class Cmd(cmd.Cmd): def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int, argparser: argparse.ArgumentParser) -> List[str]: """Default completion function for argparse commands.""" - completer = AutoCompleter(argparser, cmd2_app=self) + completer = AutoCompleter(argparser, self) tokens, _ = self.tokens_for_completion(line, begidx, endidx) if not tokens: @@ -2564,7 +2564,7 @@ class Cmd(cmd.Cmd): # Check if this is a command with an argparse function func = self.cmd_func(command) if func and hasattr(func, 'argparser'): - completer = AutoCompleter(getattr(func, 'argparser'), cmd2_app=self) + completer = AutoCompleter(getattr(func, 'argparser'), self) matches = completer.complete_command_help(tokens[cmd_index:], text, line, begidx, endidx) return matches @@ -2593,7 +2593,7 @@ class Cmd(cmd.Cmd): # Getting help for a specific command func = self.cmd_func(args.command) if func and hasattr(func, 'argparser'): - completer = AutoCompleter(getattr(func, 'argparser'), cmd2_app=self) + completer = AutoCompleter(getattr(func, 'argparser'), self) tokens = [args.command] + args.subcommand self.poutput(completer.format_help(tokens)) else: diff --git a/examples/bash_completion.py b/examples/bash_completion.py deleted file mode 100755 index b70761e2..00000000 --- a/examples/bash_completion.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -# coding=utf-8 -# PYTHON_ARGCOMPLETE_OK - This is required at the beginning of the file to enable argcomplete support -"""A simple example demonstrating integration with argcomplete. - -This example demonstrates how to achieve automatic auto-completion of argparse arguments for a command-line utility -(CLU) in the Bash shell. - -Realistically it will probably only work on Linux and then only in a Bash shell. With some effort you can probably get -it to work on macOS or Windows Subsystem for Linux (WSL); but then again, specifically within a Bash shell. This -automatic Bash completion integration with the argcomplete module is included within cmd2 in order to assist developers -with providing a the best possible out-of-the-box experience with their cmd2 applications, which in many cases will -accept argparse arguments on the command-line when executed. But from an architectural point of view, the -"argcomplete_bridge" functionality within cmd2 doesn't really depend on the rest of cmd2 and could be used in your own -CLU which doesn't use cmd2. - -WARNING: For this example to work correctly you need the argcomplete module installed and activated: - pip install argcomplete - activate-global-python-argcomplete -Please see https://github.com/kislyuk/argcomplete for more information on argcomplete. -""" -import argparse - -optional_strs = ['Apple', 'Banana', 'Cranberry', 'Durian', 'Elderberry'] - -bash_parser = argparse.ArgumentParser(prog='base') - -bash_parser.add_argument('option', choices=['load', 'export', 'reload']) - -bash_parser.add_argument('-u', '--user', help='User name') -bash_parser.add_argument('-p', '--passwd', help='Password') - -input_file = bash_parser.add_argument('-f', '--file', type=str, help='Input File') - -if __name__ == '__main__': - from cmd2.argcomplete_bridge import bash_complete - # bash_complete flags this argument telling AutoCompleter to yield to bash to perform - # tab completion of a file path - bash_complete(input_file) - -flag_opt = bash_parser.add_argument('-o', '--optional', help='Optional flag with choices') -setattr(flag_opt, 'arg_choices', optional_strs) - -# Handle bash completion if it's installed -# This early check allows the script to bail out early to provide tab-completion results -# to the argcomplete library. Putting this at the end of the file would cause the full application -# to load fulfill every tab-completion request coming from bash. This can cause a notable delay -# on the bash prompt. -try: - # only move forward if we can import CompletionFinder and AutoCompleter - from cmd2.argcomplete_bridge import CompletionFinder - from cmd2.argparse_completer import AutoCompleter - import sys - if __name__ == '__main__': - completer = CompletionFinder() - - # completer will return results to argcomplete and exit the script - completer(bash_parser, AutoCompleter(bash_parser)) -except ImportError: - pass - -# Intentionally below the bash completion code to reduce tab completion lag -import cmd2 # noqa: E402 - - -class DummyApp(cmd2.Cmd): - """ - Dummy cmd2 app - """ - - def __init__(self): - super().__init__() - - -if __name__ == '__main__': - args = bash_parser.parse_args() - - # demonstrates some handling of the command line parameters - - if args.user is None: - user = input('Username: ') - else: - user = args.user - - if args.passwd is None: - import getpass - password = getpass.getpass() - else: - password = args.passwd - - if args.file is not None: - print('Loading file: {}'.format(args.file)) - - # Clear the argumentns so cmd2 doesn't try to parse them - sys.argv = sys.argv[:1] - - app = DummyApp() - app.cmdloop() diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py index 6dc9844d..156c2d45 100755 --- a/examples/tab_autocompletion.py +++ b/examples/tab_autocompletion.py @@ -378,7 +378,9 @@ class TabCompleteExample(cmd2.Cmd): 'director': TabCompleteExample.static_list_directors, # static list 'movie_file': (self.path_complete,) } - completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser, arg_choices=choices, cmd2_app=self) + completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser, + self, + arg_choices=choices) tokens, _ = self.tokens_for_completion(line, begidx, endidx) results = completer.complete_command(tokens, text, line, begidx, endidx) @@ -525,6 +527,7 @@ class TabCompleteExample(cmd2.Cmd): library_subcommand_groups = {'type': library_type_params} completer = argparse_completer.AutoCompleter(TabCompleteExample.library_parser, + self, subcmd_args_lookup=library_subcommand_groups) tokens, _ = self.tokens_for_completion(line, begidx, endidx) diff --git a/tests/test_bashcompletion.py b/tests/test_bashcompletion.py deleted file mode 100644 index b99c3fea..00000000 --- a/tests/test_bashcompletion.py +++ /dev/null @@ -1,257 +0,0 @@ -# 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 -""" -import os -import pytest -import shlex -import sys -from typing import List - -from cmd2.argparse_completer import ACArgumentParser, AutoCompleter - - -try: - from cmd2.argcomplete_bridge import CompletionFinder, tokens_for_completion - skip_no_argcomplete = False - skip_reason = '' -except ImportError: - # Don't test if argcomplete isn't present (likely on Windows) - skip_no_argcomplete = True - skip_reason = "argcomplete isn't installed\n" - -skip_travis = "TRAVIS" in os.environ and os.environ["TRAVIS"] == "true" -if skip_travis: - skip_reason += 'These tests cannot run on TRAVIS\n' - -skip_windows = sys.platform.startswith('win') -if skip_windows: - skip_reason = 'argcomplete doesn\'t support Windows' - -skip = skip_no_argcomplete or skip_travis or skip_windows - -skip_mac = sys.platform.startswith('dar') - - -actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', - 'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac', - 'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', - 'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee'] - - -def query_actors() -> List[str]: - """Simulating a function that queries and returns a completion values""" - return actors - - -@pytest.fixture -def parser1(): - """creates a argparse object to test completion against""" - 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: - print('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: - print('media shows ' + str(args.__dict__)) - - media_parser = 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_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_commands_subparsers.add_parser('list') - - return media_parser - - -# noinspection PyShadowingNames -@pytest.mark.skipif(skip_no_argcomplete or skip_windows, reason=skip_reason) -def test_bash_nocomplete(parser1): - completer = CompletionFinder() - result = completer(parser1, AutoCompleter(parser1)) - assert result is None - - -# save the real os.fdopen -os_fdopen = os.fdopen - - -def my_fdopen(fd, mode, *args): - """mock fdopen that redirects 8 and 9 from argcomplete to stdin/stdout for testing""" - if fd > 7: - return os_fdopen(fd - 7, mode, *args) - return os_fdopen(fd, mode) - - -# noinspection PyShadowingNames -@pytest.mark.skipif(skip_no_argcomplete or skip_windows, reason=skip_reason) -def test_invalid_ifs(parser1, mocker): - completer = CompletionFinder() - - mocker.patch.dict(os.environ, {'_ARGCOMPLETE': '1', - '_ARGCOMPLETE_IFS': '\013\013'}) - - mocker.patch.object(os, 'fdopen', my_fdopen) - - with pytest.raises(SystemExit): - completer(parser1, AutoCompleter(parser1), exit_method=sys.exit) - - -# noinspection PyShadowingNames -@pytest.mark.skipif(skip or skip_mac, reason=skip_reason) -@pytest.mark.parametrize('comp_line, exp_out, exp_err', [ - ('media ', 'movies\013shows', ''), - ('media mo', 'movies', ''), - ('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''), - ('media movies list ', '', ''), - ('media movies add ', '\013\013 ', ''' -Hint: - TITLE Movie Title'''), -]) -def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err): - mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1', - '_ARGCOMPLETE_IFS': '\013', - 'COMP_TYPE': '63', - 'COMP_LINE': comp_line, - 'COMP_POINT': str(len(comp_line))}) - - mock.patch.object(os, 'fdopen', my_fdopen) - - with pytest.raises(SystemExit): - completer = CompletionFinder() - - choices = {'actor': query_actors, # function - } - autocompleter = AutoCompleter(parser1, arg_choices=choices) - completer(parser1, autocompleter, exit_method=sys.exit) - - out, err = capfd.readouterr() - assert out == exp_out - assert err == exp_err - - -def fdopen_fail_8(fd, mode, *args): - """mock fdopen that forces failure if fd == 8""" - if fd == 8: - raise IOError() - return my_fdopen(fd, mode, *args) - - -# noinspection PyShadowingNames -@pytest.mark.skipif(skip_no_argcomplete or skip_windows, reason=skip_reason) -def test_fail_alt_stdout(parser1, mocker): - completer = CompletionFinder() - - comp_line = 'media movies list ' - mocker.patch.dict(os.environ, {'_ARGCOMPLETE': '1', - '_ARGCOMPLETE_IFS': '\013', - 'COMP_TYPE': '63', - 'COMP_LINE': comp_line, - 'COMP_POINT': str(len(comp_line))}) - mocker.patch.object(os, 'fdopen', fdopen_fail_8) - - try: - choices = {'actor': query_actors, # function - } - autocompleter = AutoCompleter(parser1, arg_choices=choices) - completer(parser1, autocompleter, exit_method=sys.exit) - except SystemExit as err: - assert err.code == 1 - - -def fdopen_fail_9(fd, mode, *args): - """mock fdopen that forces failure if fd == 9""" - if fd == 9: - raise IOError() - return my_fdopen(fd, mode, *args) - - -# noinspection PyShadowingNames -@pytest.mark.skipif(skip or skip_mac, reason=skip_reason) -def test_fail_alt_stderr(parser1, capfd, mock): - completer = CompletionFinder() - - comp_line = 'media movies add ' - exp_out = '\013\013 ' - exp_err = ''' -Hint: - TITLE Movie Title''' - - mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1', - '_ARGCOMPLETE_IFS': '\013', - 'COMP_TYPE': '63', - 'COMP_LINE': comp_line, - 'COMP_POINT': str(len(comp_line))}) - mock.patch.object(os, 'fdopen', fdopen_fail_9) - - with pytest.raises(SystemExit): - choices = {'actor': query_actors, # function - } - autocompleter = AutoCompleter(parser1, arg_choices=choices) - completer(parser1, autocompleter, exit_method=sys.exit) - - out, err = capfd.readouterr() - assert out == exp_out - assert err == exp_err - -@pytest.mark.skipif(skip_no_argcomplete, reason=skip_reason) -def test_argcomplete_tokens_for_completion_simple(): - line = 'this is "a test"' - endidx = len(line) - - tokens, raw_tokens, begin_idx, end_idx = tokens_for_completion(line, endidx) - assert tokens == shlex.split(line) - assert raw_tokens == ['this', 'is', '"a test"'] - assert begin_idx == line.rfind("is ") + len("is ") - assert end_idx == end_idx - -@pytest.mark.skipif(skip_no_argcomplete, reason=skip_reason) -def test_argcomplete_tokens_for_completion_unclosed_quotee_exception(): - line = 'this is "a test' - endidx = len(line) - - tokens, raw_tokens, begin_idx, end_idx = tokens_for_completion(line, endidx) - - assert tokens == ['this', 'is', 'a test'] - assert raw_tokens == ['this', 'is', '"a test'] - assert begin_idx == line.rfind("is ") + len("is ") + 1 - assert end_idx == end_idx |