Improve autocompletion function on file name completion (#5125)

This commit is contained in:
Kexuan Sun 2018-06-22 01:29:31 +08:00 committed by Pradyun Gedam
parent bcd9db92bc
commit 14fe337bcf
9 changed files with 199 additions and 6 deletions

2
news/4842.feature Normal file
View File

@ -0,0 +1,2 @@
Improve autocompletion function on file name completion after options
which have ``<file>``, ``<dir>`` or ``<path>`` as metavar.

2
news/5125.feature Normal file
View File

@ -0,0 +1,2 @@
Improve autocompletion function on file name completion after options
which have ``<file>``, ``<dir>`` or ``<path>`` as metavar.

View File

@ -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
# ``<file>``, ``<dir>`` or ``<path>``
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 ``<dir>`` after option
# complete directories when there is ``<file>``, ``<path>`` or
# ``<dir>``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 <command> [options]',

View File

View File

@ -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 <file> or <dir> 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 <file> "
"after options in command"
)
assert os.path.join('resources', '') in res.stdout, (
"autocomplete function could not complete <dir> "
"after options in command"
)
assert not any(out in res.stdout for out in
(os.path.join('REPLAY', ''), 'README.txt')), (
"autocomplete function completed <file> or <dir> that "
"should not be completed"
)
if sys.platform != 'win32':
return
assert 'readme.txt' in res.stdout, (
"autocomplete function could not complete <file> "
"after options in command"
)
assert os.path.join('replay', '') in res.stdout, (
"autocomplete function could not complete <dir> "
"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 <file> when "
"it should not complete"
)
assert not any(os.path.join(out, '') in res.stdout
for out in ('replay', 'resources')), (
"autocomplete function completed <dir> when "
"it should not complete"
)
def test_completion_directories_after_option(script, data):
"""
Test getting completion <dir> 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 <dir> after options"
)
assert not any(out in res.stdout for out in (
'requirements.txt', 'README.txt', os.path.join('REPLAY', ''))), (
"autocomplete function completed <dir> when "
"it should not complete"
)
if sys.platform == 'win32':
assert os.path.join('replay', '') in res.stdout, (
"autocomplete function could not complete <dir> after options"
)
def test_completion_subdirectories_after_option(script, data):
"""
Test getting completion <dir> 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 <dir> "
"given path of a directory after options"
)
def test_completion_path_after_option(script, data):
"""
Test getting completion <path> 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 <path> "
"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)

View File

@ -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)