mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
Improve autocompletion function on file name completion (#5125)
This commit is contained in:
parent
bcd9db92bc
commit
14fe337bcf
2
news/4842.feature
Normal file
2
news/4842.feature
Normal 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
2
news/5125.feature
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Improve autocompletion function on file name completion after options
|
||||||
|
which have ``<file>``, ``<dir>`` or ``<path>`` as metavar.
|
|
@ -116,6 +116,15 @@ def autocomplete():
|
||||||
options = [(x, v) for (x, v) in options if x not in prev_opts]
|
options = [(x, v) for (x, v) in options if x not in prev_opts]
|
||||||
# filter options by current input
|
# filter options by current input
|
||||||
options = [(k, v) for k, v in options if k.startswith(current)]
|
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:
|
for option in options:
|
||||||
opt_label = option[0]
|
opt_label = option[0]
|
||||||
# append '=' to options which require args
|
# append '=' to options which require args
|
||||||
|
@ -124,19 +133,74 @@ def autocomplete():
|
||||||
print(opt_label)
|
print(opt_label)
|
||||||
else:
|
else:
|
||||||
# show main parser options only when necessary
|
# 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:
|
for opt in opts:
|
||||||
if opt.help != optparse.SUPPRESS_HELP:
|
if opt.help != optparse.SUPPRESS_HELP:
|
||||||
subcommands += opt._long_opts + opt._short_opts
|
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)]))
|
print(' '.join([x for x in subcommands if x.startswith(current)]))
|
||||||
sys.exit(1)
|
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():
|
def create_main_parser():
|
||||||
parser_kw = {
|
parser_kw = {
|
||||||
'usage': '\n%prog <command> [options]',
|
'usage': '\n%prog <command> [options]',
|
||||||
|
|
0
tests/data/completion_paths/README.txt
Normal file
0
tests/data/completion_paths/README.txt
Normal file
0
tests/data/completion_paths/REPLAY/video.mpeg
Normal file
0
tests/data/completion_paths/REPLAY/video.mpeg
Normal file
0
tests/data/completion_paths/requirements.txt
Normal file
0
tests/data/completion_paths/requirements.txt
Normal file
|
@ -77,7 +77,7 @@ def test_completion_alone(script):
|
||||||
'completion alone failed -- ' + result.stderr
|
'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 = os.environ.copy()
|
||||||
script.environ['PIP_AUTO_COMPLETE'] = '1'
|
script.environ['PIP_AUTO_COMPLETE'] = '1'
|
||||||
script.environ['COMP_WORDS'] = words
|
script.environ['COMP_WORDS'] = words
|
||||||
|
@ -87,6 +87,7 @@ def setup_completion(script, words, cword):
|
||||||
result = script.run(
|
result = script.run(
|
||||||
'python', '-c', 'import pip._internal;pip._internal.autocomplete()',
|
'python', '-c', 'import pip._internal;pip._internal.autocomplete()',
|
||||||
expect_error=True,
|
expect_error=True,
|
||||||
|
cwd=cwd,
|
||||||
)
|
)
|
||||||
|
|
||||||
return result, script
|
return result, script
|
||||||
|
@ -113,7 +114,7 @@ def test_completion_for_default_parameters(script):
|
||||||
|
|
||||||
def test_completion_option_for_command(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')
|
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 ``-``"
|
"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'])
|
@pytest.mark.parametrize('flag', ['--bash', '--zsh', '--fish'])
|
||||||
def test_completion_uses_same_executable_name(script, flag):
|
def test_completion_uses_same_executable_name(script, flag):
|
||||||
expect_stderr = sys.version_info[:2] == (3, 3)
|
expect_stderr = sys.version_info[:2] == (3, 3)
|
||||||
|
|
|
@ -116,6 +116,10 @@ class TestData(object):
|
||||||
def reqfiles(self):
|
def reqfiles(self):
|
||||||
return self.root.join("reqfiles")
|
return self.root.join("reqfiles")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def completion_paths(self):
|
||||||
|
return self.root.join("completion_paths")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def find_links(self):
|
def find_links(self):
|
||||||
return path_to_url(self.packages)
|
return path_to_url(self.packages)
|
||||||
|
|
Loading…
Reference in a new issue