From 14fe337bcfb8dd9cc12196a38a4c9454466d5723 Mon Sep 17 00:00:00 2001 From: Kexuan Sun Date: Fri, 22 Jun 2018 01:29:31 +0800 Subject: [PATCH] Improve autocompletion function on file name completion (#5125) --- news/4842.feature | 2 + news/5125.feature | 2 + src/pip/_internal/__init__.py | 72 +++++++++- tests/data/completion_paths/README.txt | 0 tests/data/completion_paths/REPLAY/video.mpeg | 0 tests/data/completion_paths/requirements.txt | 0 .../resources/images/icon.png | 0 tests/functional/test_completion.py | 125 +++++++++++++++++- tests/lib/__init__.py | 4 + 9 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 news/4842.feature create mode 100644 news/5125.feature create mode 100644 tests/data/completion_paths/README.txt create mode 100644 tests/data/completion_paths/REPLAY/video.mpeg create mode 100644 tests/data/completion_paths/requirements.txt create mode 100644 tests/data/completion_paths/resources/images/icon.png diff --git a/news/4842.feature b/news/4842.feature new file mode 100644 index 000000000..f96a668ec --- /dev/null +++ b/news/4842.feature @@ -0,0 +1,2 @@ +Improve autocompletion function on file name completion after options +which have ````, ```` or ```` as metavar. diff --git a/news/5125.feature b/news/5125.feature new file mode 100644 index 000000000..f96a668ec --- /dev/null +++ b/news/5125.feature @@ -0,0 +1,2 @@ +Improve autocompletion function on file name completion after options +which have ````, ```` or ```` as metavar. diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index 865d9ec3c..250ed1d90 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -116,6 +116,15 @@ def autocomplete(): options = [(x, v) for (x, v) in options if x not in prev_opts] # filter options by current input options = [(k, v) for k, v in options if k.startswith(current)] + # get completion type given cwords and available subcommand options + completion_type = get_path_completion_type( + cwords, cword, subcommand.parser.option_list_all, + ) + # get completion files and directories if ``completion_type`` is + # ````, ```` or ```` + if completion_type: + options = auto_complete_paths(current, completion_type) + options = ((opt, 0) for opt in options) for option in options: opt_label = option[0] # append '=' to options which require args @@ -124,19 +133,74 @@ def autocomplete(): print(opt_label) else: # show main parser options only when necessary - if current.startswith('-') or current.startswith('--'): - opts = [i.option_list for i in parser.option_groups] - opts.append(parser.option_list) - opts = (o for it in opts for o in it) + opts = [i.option_list for i in parser.option_groups] + opts.append(parser.option_list) + opts = (o for it in opts for o in it) + if current.startswith('-'): for opt in opts: if opt.help != optparse.SUPPRESS_HELP: subcommands += opt._long_opts + opt._short_opts + else: + # get completion type given cwords and all available options + completion_type = get_path_completion_type(cwords, cword, opts) + if completion_type: + subcommands = auto_complete_paths(current, completion_type) print(' '.join([x for x in subcommands if x.startswith(current)])) sys.exit(1) +def get_path_completion_type(cwords, cword, opts): + """Get the type of path completion (``file``, ``dir``, ``path`` or None) + + :param cwords: same as the environmental variable ``COMP_WORDS`` + :param cword: same as the environmental variable ``COMP_CWORD`` + :param opts: The available options to check + :return: path completion type (``file``, ``dir``, ``path`` or None) + """ + if cword < 2 or not cwords[cword - 2].startswith('-'): + return + for opt in opts: + if opt.help == optparse.SUPPRESS_HELP: + continue + for o in str(opt).split('/'): + if cwords[cword - 2].split('=')[0] == o: + if any(x in ('path', 'file', 'dir') + for x in opt.metavar.split('/')): + return opt.metavar + + +def auto_complete_paths(current, completion_type): + """If ``completion_type`` is ``file`` or ``path``, list all regular files + and directories starting with ``current``; otherwise only list directories + starting with ``current``. + + :param current: The word to be completed + :param completion_type: path completion type(`file`, `path` or `dir`)i + :return: A generator of regular files and/or directories + """ + directory, filename = os.path.split(current) + current_path = os.path.abspath(directory) + # Don't complete paths if they can't be accessed + if not os.access(current_path, os.R_OK): + return + filename = os.path.normcase(filename) + # list all files that start with ``filename`` + file_list = (x for x in os.listdir(current_path) + if os.path.normcase(x).startswith(filename)) + for f in file_list: + opt = os.path.join(current_path, f) + comp_file = os.path.normcase(os.path.join(directory, f)) + # complete regular files when there is not ```` after option + # complete directories when there is ````, ```` or + # ````after option + if completion_type != 'dir' and os.path.isfile(opt): + yield comp_file + elif os.path.isdir(opt): + yield os.path.join(comp_file, '') + + def create_main_parser(): parser_kw = { 'usage': '\n%prog [options]', diff --git a/tests/data/completion_paths/README.txt b/tests/data/completion_paths/README.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/completion_paths/REPLAY/video.mpeg b/tests/data/completion_paths/REPLAY/video.mpeg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/completion_paths/requirements.txt b/tests/data/completion_paths/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/completion_paths/resources/images/icon.png b/tests/data/completion_paths/resources/images/icon.png new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index 996dcc63d..bd7a5afc4 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -77,7 +77,7 @@ def test_completion_alone(script): 'completion alone failed -- ' + result.stderr -def setup_completion(script, words, cword): +def setup_completion(script, words, cword, cwd=None): script.environ = os.environ.copy() script.environ['PIP_AUTO_COMPLETE'] = '1' script.environ['COMP_WORDS'] = words @@ -87,6 +87,7 @@ def setup_completion(script, words, cword): result = script.run( 'python', '-c', 'import pip._internal;pip._internal.autocomplete()', expect_error=True, + cwd=cwd, ) return result, script @@ -113,7 +114,7 @@ def test_completion_for_default_parameters(script): def test_completion_option_for_command(script): """ - Test getting completion for ``--`` in command (eg. pip search --) + Test getting completion for ``--`` in command (e.g. ``pip search --``) """ res, env = setup_completion(script, 'pip search --', '2') @@ -144,6 +145,126 @@ def test_completion_short_option_for_command(script): "autocomplete function could not complete short options after ``-``" +def test_completion_files_after_option(script, data): + """ + Test getting completion for or after options in command + (e.g. ``pip install -r``) + """ + res, env = setup_completion( + script=script, + words=('pip install -r r'), + cword='3', + cwd=data.completion_paths, + ) + assert 'requirements.txt' in res.stdout, ( + "autocomplete function could not complete " + "after options in command" + ) + assert os.path.join('resources', '') in res.stdout, ( + "autocomplete function could not complete " + "after options in command" + ) + assert not any(out in res.stdout for out in + (os.path.join('REPLAY', ''), 'README.txt')), ( + "autocomplete function completed or that " + "should not be completed" + ) + if sys.platform != 'win32': + return + assert 'readme.txt' in res.stdout, ( + "autocomplete function could not complete " + "after options in command" + ) + assert os.path.join('replay', '') in res.stdout, ( + "autocomplete function could not complete " + "after options in command" + ) + + +def test_completion_not_files_after_option(script, data): + """ + Test not getting completion files after options which not applicable + (e.g. ``pip install``) + """ + res, env = setup_completion( + script=script, + words=('pip install r'), + cword='2', + cwd=data.completion_paths, + ) + assert not any(out in res.stdout for out in + ('requirements.txt', 'readme.txt',)), ( + "autocomplete function completed when " + "it should not complete" + ) + assert not any(os.path.join(out, '') in res.stdout + for out in ('replay', 'resources')), ( + "autocomplete function completed when " + "it should not complete" + ) + + +def test_completion_directories_after_option(script, data): + """ + Test getting completion after options in command + (e.g. ``pip --cache-dir``) + """ + res, env = setup_completion( + script=script, + words=('pip --cache-dir r'), + cword='2', + cwd=data.completion_paths, + ) + assert os.path.join('resources', '') in res.stdout, ( + "autocomplete function could not complete after options" + ) + assert not any(out in res.stdout for out in ( + 'requirements.txt', 'README.txt', os.path.join('REPLAY', ''))), ( + "autocomplete function completed when " + "it should not complete" + ) + if sys.platform == 'win32': + assert os.path.join('replay', '') in res.stdout, ( + "autocomplete function could not complete after options" + ) + + +def test_completion_subdirectories_after_option(script, data): + """ + Test getting completion after options in command + given path of a directory + """ + res, env = setup_completion( + script=script, + words=('pip --cache-dir ' + os.path.join('resources', '')), + cword='2', + cwd=data.completion_paths, + ) + assert os.path.join('resources', + os.path.join('images', '')) in res.stdout, ( + "autocomplete function could not complete " + "given path of a directory after options" + ) + + +def test_completion_path_after_option(script, data): + """ + Test getting completion after options in command + given absolute path + """ + res, env = setup_completion( + script=script, + words=('pip install -e ' + os.path.join(data.completion_paths, 'R')), + cword='3', + ) + assert all(os.path.normcase(os.path.join(data.completion_paths, out)) + in res.stdout for out in ( + 'README.txt', os.path.join('REPLAY', ''))), ( + "autocomplete function could not complete " + "after options in command given absolute path" + ) + + @pytest.mark.parametrize('flag', ['--bash', '--zsh', '--fish']) def test_completion_uses_same_executable_name(script, flag): expect_stderr = sys.version_info[:2] == (3, 3) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 7f1dd5d0c..b445e89e8 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -116,6 +116,10 @@ class TestData(object): def reqfiles(self): return self.root.join("reqfiles") + @property + def completion_paths(self): + return self.root.join("completion_paths") + @property def find_links(self): return path_to_url(self.packages)