Skip to content

Commit e0f4c2b

Browse files
committed
Improve autocompletion function on file name completion.
1 parent 4e2c752 commit e0f4c2b

File tree

7 files changed

+158
-7
lines changed

7 files changed

+158
-7
lines changed

news/4842.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve autocompletion function on file name completion after options
2+
which have ``<file>``, ``<dir>`` or ``<path>`` as metavar.

src/pip/_internal/__init__.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ def autocomplete():
114114
options = [(x, v) for (x, v) in options if x not in prev_opts]
115115
# filter options by current input
116116
options = [(k, v) for k, v in options if k.startswith(current)]
117+
# get completion type given cwords and available subcommand options
118+
completion_type = get_path_completion_type(
119+
cwords, cword, subcommand.parser.option_list_all)
120+
# get completion files and directories if ``completion_type`` is
121+
# ``<file>``, ``<dir>`` or ``<path>``
122+
if completion_type:
123+
options = auto_complete_paths(current, completion_type)
124+
options = ((opt, 0) for opt in options)
117125
for option in options:
118126
opt_label = option[0]
119127
# append '=' to options which require args
@@ -122,18 +130,71 @@ def autocomplete():
122130
print(opt_label)
123131
else:
124132
# show main parser options only when necessary
125-
if current.startswith('-') or current.startswith('--'):
126-
opts = [i.option_list for i in parser.option_groups]
127-
opts.append(parser.option_list)
128-
opts = (o for it in opts for o in it)
129-
133+
opts = [i.option_list for i in parser.option_groups]
134+
opts.append(parser.option_list)
135+
opts = (o for it in opts for o in it)
136+
if current.startswith('-'):
130137
subcommands += [i.get_opt_string() for i in opts
131138
if i.help != optparse.SUPPRESS_HELP]
139+
else:
140+
# get completion type given cwords and all available options
141+
completion_type = get_path_completion_type(cwords, cword, opts)
142+
if completion_type:
143+
subcommands = auto_complete_paths(current, completion_type)
132144

133145
print(' '.join([x for x in subcommands if x.startswith(current)]))
134146
sys.exit(1)
135147

136148

149+
def get_path_completion_type(cwords, cword, opts):
150+
"""Get the type of path completion(``file``, ``dir``, ``path`` or None)
151+
152+
:param cwords: same as the environmental variable ``COMP_WORDS``
153+
:param cword: same as the environmental variable ``COMP_CWORD``
154+
:param opts: The available options to check
155+
:return: path completion type(``file``, ``dir``, ``path`` or None)
156+
"""
157+
if cword >= 2 and cwords[cword - 2].startswith('-'):
158+
for opt in opts:
159+
if opt.help == optparse.SUPPRESS_HELP:
160+
continue
161+
for o in str(opt).split('/'):
162+
if cwords[cword - 2].split('=')[0] == o:
163+
if any(x in ('path', 'file', 'dir')
164+
for x in opt.metavar.split('/')):
165+
return opt.metavar
166+
167+
168+
def auto_complete_paths(current, completion_type):
169+
"""If ``completion_type`` is ``file`` or ``path``, list all regular files
170+
and directories starting with ``current``; otherwise only list directories
171+
starting with ``current``.
172+
173+
:param current: The word to be completed
174+
:param completion_type: path completion type(`file`, `path` or `dir`)i
175+
:return: A generator of regular files and/or directories
176+
"""
177+
# split ``current`` into two parts(directory and name)
178+
directory, filename = os.path.split(current)
179+
# change directory to ``directory``
180+
current_path = os.path.abspath(directory)
181+
# check whether ``current_path`` is accessible
182+
if os.access(current_path, os.R_OK):
183+
# list all files that start with ``filename``
184+
file_list = (x for x in os.listdir(current_path)
185+
if x.startswith(filename))
186+
for f in file_list:
187+
opt = os.path.join(current_path, f)
188+
comp_file = os.path.join(directory, f)
189+
# complete regular files when there is not ``<dir>`` after option
190+
# complete directories when there is ``<file>``, ``<path>`` or
191+
# ``<dir>``after option
192+
if completion_type != 'dir' and os.path.isfile(opt):
193+
yield comp_file
194+
elif os.path.isdir(opt):
195+
yield os.path.join(comp_file, '')
196+
197+
137198
def create_main_parser():
138199
parser_kw = {
139200
'usage': '\n%prog <command> [options]',

tests/data/completepaths/README.txt

Whitespace-only changes.

tests/data/completepaths/requirements.txt

Whitespace-only changes.

tests/data/completepaths/resources/images/icon.png

Loading

tests/functional/test_completion.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_completion_alone(script):
7777
'completion alone failed -- ' + result.stderr
7878

7979

80-
def setup_completion(script, words, cword):
80+
def setup_completion(script, words, cword, cwd=None):
8181
script.environ = os.environ.copy()
8282
script.environ['PIP_AUTO_COMPLETE'] = '1'
8383
script.environ['COMP_WORDS'] = words
@@ -87,6 +87,7 @@ def setup_completion(script, words, cword):
8787
result = script.run(
8888
'python', '-c', 'import pip._internal;pip._internal.autocomplete()',
8989
expect_error=True,
90+
cwd=cwd,
9091
)
9192

9293
return result, script
@@ -113,14 +114,97 @@ def test_completion_for_default_parameters(script):
113114

114115
def test_completion_option_for_command(script):
115116
"""
116-
Test getting completion for ``--`` in command (eg. pip search --)
117+
Test getting completion for ``--`` in command (e.g. ``pip search --``)
117118
"""
118119

119120
res, env = setup_completion(script, 'pip search --', '2')
120121
assert '--help' in res.stdout,\
121122
"autocomplete function could not complete ``--``"
122123

123124

125+
def test_completion_files_after_option(script, data):
126+
"""
127+
Test getting completion for <file> or <dir> after options in command
128+
(e.g. ``pip install -r``)
129+
"""
130+
res, env = setup_completion(
131+
script, ('pip install -r r'),
132+
'3',
133+
data.complete_paths
134+
)
135+
assert 'requirements.txt' in res.stdout,\
136+
"autocomplete function could not complete <file>"\
137+
"after options in command"
138+
assert 'README.txt' not in res.stdout,\
139+
"autocomplete function completed <file> that"\
140+
"should not be completed"
141+
assert 'resources' in res.stdout,\
142+
"autocomplete function could not complete <dir>"\
143+
"after options in command"
144+
145+
146+
def test_completion_not_files_after_option(script, data):
147+
"""
148+
Test not getting completion files after options which not applicable
149+
(e.g. ``pip install``)
150+
"""
151+
res, env = setup_completion(
152+
script, ('pip install r'),
153+
'2',
154+
data.complete_paths
155+
)
156+
assert 'requirements.txt' not in res.stdout,\
157+
"autocomplete function completed <file> when"\
158+
"it should not complete"
159+
160+
161+
def test_completion_directories_after_option(script, data):
162+
"""
163+
Test getting completion <dir> after options in command
164+
(e.g. ``pip --cache-dir``)
165+
"""
166+
res, env = setup_completion(
167+
script,
168+
('pip --cache-dir resources'),
169+
'2',
170+
data.complete_paths
171+
)
172+
assert os.path.join('resources', '') in res.stdout,\
173+
"autocomplete function could not complete <dir> after options"
174+
175+
176+
def test_completion_subdirectories_after_option(script, data):
177+
"""
178+
Test getting completion <dir> after options in command
179+
given path of a directory
180+
"""
181+
res, env = setup_completion(
182+
script,
183+
('pip --cache-dir ' + os.path.join('resources', '')),
184+
'2',
185+
data.complete_paths
186+
)
187+
assert os.path.join('resources',
188+
os.path.join('images', '')) in res.stdout,\
189+
"autocomplete function could not complete <dir>"\
190+
"given path of a directory after options"
191+
192+
193+
def test_completion_path_after_option(script, data):
194+
"""
195+
Test getting completion <path> after options in command
196+
given absolute path
197+
"""
198+
res, env = setup_completion(
199+
script,
200+
('pip install -e ' + os.path.join(data.complete_paths, 'R')),
201+
'3'
202+
)
203+
assert os.path.join(data.complete_paths, 'README.txt') in res.stdout,\
204+
"autocomplete function could not complete <path>"\
205+
"after options in command given absolute path"
206+
207+
124208
@pytest.mark.parametrize('flag', ['--bash', '--zsh', '--fish'])
125209
def test_completion_uses_same_executable_name(script, flag):
126210
expect_stderr = sys.version_info[:2] == (3, 3)

tests/lib/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ def indexes(self):
112112
def reqfiles(self):
113113
return self.root.join("reqfiles")
114114

115+
@property
116+
def complete_paths(self):
117+
return self.root.join("completepaths")
118+
115119
@property
116120
def find_links(self):
117121
return path_to_url(self.packages)

0 commit comments

Comments
 (0)