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
9 changed files with 199 additions and 6 deletions
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]
|
||||
# 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]',
|
||||
|
|
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
0
tests/data/completion_paths/resources/images/icon.png
Normal file
0
tests/data/completion_paths/resources/images/icon.png
Normal 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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue