Skip to content

Commit 449eb1c

Browse files
authored
Merge pull request #13140 from matthewhughes934/tab-completion-for-sub-commands
2 parents 3c5a189 + 55da574 commit 449eb1c

File tree

7 files changed

+79
-23
lines changed

7 files changed

+79
-23
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Include sub-commands in tab completion.

src/pip/_internal/cli/autocompletion.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ def autocomplete() -> None:
103103
if option[1] and option[0][:2] == "--":
104104
opt_label += "="
105105
print(opt_label)
106+
107+
# Complete sub-commands (unless one is already given).
108+
if not any(name in cwords for name in subcommand.handler_map()):
109+
for handler_name in subcommand.handler_map():
110+
if handler_name.startswith(current):
111+
print(handler_name)
106112
else:
107113
# show main parser options only when necessary
108114

src/pip/_internal/cli/base_command.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import traceback
1111
from optparse import Values
12+
from typing import Callable
1213

1314
from pip._vendor.rich import reconfigure
1415
from pip._vendor.rich import traceback as rich_traceback
@@ -232,3 +233,9 @@ def _main(self, args: list[str]) -> int:
232233
options.cache_dir = None
233234

234235
return self._run_wrapper(level_number, options, args)
236+
237+
def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
238+
"""
239+
map of names to handler actions for commands with sub-actions
240+
"""
241+
return {}

src/pip/_internal/commands/cache.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
import textwrap
33
from optparse import Values
4-
from typing import Any
4+
from typing import Callable
55

66
from pip._internal.cli.base_command import Command
77
from pip._internal.cli.status_codes import ERROR, SUCCESS
@@ -49,45 +49,48 @@ def add_options(self) -> None:
4949

5050
self.parser.insert_option_group(0, self.cmd_opts)
5151

52-
def run(self, options: Values, args: list[str]) -> int:
53-
handlers = {
52+
def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
53+
return {
5454
"dir": self.get_cache_dir,
5555
"info": self.get_cache_info,
5656
"list": self.list_cache_items,
5757
"remove": self.remove_cache_items,
5858
"purge": self.purge_cache,
5959
}
6060

61+
def run(self, options: Values, args: list[str]) -> int:
62+
handler_map = self.handler_map()
63+
6164
if not options.cache_dir:
6265
logger.error("pip cache commands can not function since cache is disabled.")
6366
return ERROR
6467

6568
# Determine action
66-
if not args or args[0] not in handlers:
69+
if not args or args[0] not in handler_map:
6770
logger.error(
6871
"Need an action (%s) to perform.",
69-
", ".join(sorted(handlers)),
72+
", ".join(sorted(handler_map)),
7073
)
7174
return ERROR
7275

7376
action = args[0]
7477

7578
# Error handling happens here, not in the action-handlers.
7679
try:
77-
handlers[action](options, args[1:])
80+
handler_map[action](options, args[1:])
7881
except PipError as e:
7982
logger.error(e.args[0])
8083
return ERROR
8184

8285
return SUCCESS
8386

84-
def get_cache_dir(self, options: Values, args: list[Any]) -> None:
87+
def get_cache_dir(self, options: Values, args: list[str]) -> None:
8588
if args:
8689
raise CommandError("Too many arguments")
8790

8891
logger.info(options.cache_dir)
8992

90-
def get_cache_info(self, options: Values, args: list[Any]) -> None:
93+
def get_cache_info(self, options: Values, args: list[str]) -> None:
9194
if args:
9295
raise CommandError("Too many arguments")
9396

@@ -129,7 +132,7 @@ def get_cache_info(self, options: Values, args: list[Any]) -> None:
129132

130133
logger.info(message)
131134

132-
def list_cache_items(self, options: Values, args: list[Any]) -> None:
135+
def list_cache_items(self, options: Values, args: list[str]) -> None:
133136
if len(args) > 1:
134137
raise CommandError("Too many arguments")
135138

@@ -161,7 +164,7 @@ def format_for_abspath(self, files: list[str]) -> None:
161164
if files:
162165
logger.info("\n".join(sorted(files)))
163166

164-
def remove_cache_items(self, options: Values, args: list[Any]) -> None:
167+
def remove_cache_items(self, options: Values, args: list[str]) -> None:
165168
if len(args) > 1:
166169
raise CommandError("Too many arguments")
167170

@@ -188,7 +191,7 @@ def remove_cache_items(self, options: Values, args: list[Any]) -> None:
188191
logger.verbose("Removed %s", filename)
189192
logger.info("Files removed: %s (%s)", len(files), format_size(bytes_removed))
190193

191-
def purge_cache(self, options: Values, args: list[Any]) -> None:
194+
def purge_cache(self, options: Values, args: list[str]) -> None:
192195
if args:
193196
raise CommandError("Too many arguments")
194197

src/pip/_internal/commands/configuration.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import subprocess
66
from optparse import Values
7-
from typing import Any
7+
from typing import Any, Callable
88

99
from pip._internal.cli.base_command import Command
1010
from pip._internal.cli.status_codes import ERROR, SUCCESS
@@ -95,8 +95,8 @@ def add_options(self) -> None:
9595

9696
self.parser.insert_option_group(0, self.cmd_opts)
9797

98-
def run(self, options: Values, args: list[str]) -> int:
99-
handlers = {
98+
def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
99+
return {
100100
"list": self.list_values,
101101
"edit": self.open_in_editor,
102102
"get": self.get_name,
@@ -105,11 +105,14 @@ def run(self, options: Values, args: list[str]) -> int:
105105
"debug": self.list_config_values,
106106
}
107107

108+
def run(self, options: Values, args: list[str]) -> int:
109+
handler_map = self.handler_map()
110+
108111
# Determine action
109-
if not args or args[0] not in handlers:
112+
if not args or args[0] not in handler_map:
110113
logger.error(
111114
"Need an action (%s) to perform.",
112-
", ".join(sorted(handlers)),
115+
", ".join(sorted(handler_map)),
113116
)
114117
return ERROR
115118

@@ -133,7 +136,7 @@ def run(self, options: Values, args: list[str]) -> int:
133136

134137
# Error handling happens here, not in the action-handlers.
135138
try:
136-
handlers[action](options, args[1:])
139+
handler_map[action](options, args[1:])
137140
except PipError as e:
138141
logger.error(e.args[0])
139142
return ERROR

src/pip/_internal/commands/index.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
from collections.abc import Iterable
66
from optparse import Values
7-
from typing import Any
7+
from typing import Any, Callable
88

99
from pip._vendor.packaging.version import Version
1010

@@ -53,24 +53,27 @@ def add_options(self) -> None:
5353
self.parser.insert_option_group(0, index_opts)
5454
self.parser.insert_option_group(0, self.cmd_opts)
5555

56-
def run(self, options: Values, args: list[str]) -> int:
57-
handlers = {
56+
def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
57+
return {
5858
"versions": self.get_available_package_versions,
5959
}
6060

61+
def run(self, options: Values, args: list[str]) -> int:
62+
handler_map = self.handler_map()
63+
6164
# Determine action
62-
if not args or args[0] not in handlers:
65+
if not args or args[0] not in handler_map:
6366
logger.error(
6467
"Need an action (%s) to perform.",
65-
", ".join(sorted(handlers)),
68+
", ".join(sorted(handler_map)),
6669
)
6770
return ERROR
6871

6972
action = args[0]
7073

7174
# Error handling happens here, not in the action-handlers.
7275
try:
73-
handlers[action](options, args[1:])
76+
handler_map[action](options, args[1:])
7477
except PipError as e:
7578
logger.error(e.args[0])
7679
return ERROR

tests/functional/test_completion.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,36 @@ def test_completion_uses_same_executable_name(
429429
expect_stderr=deprecated_python,
430430
)
431431
assert executable_name in result.stdout
432+
433+
434+
@pytest.mark.parametrize(
435+
"subcommand, handler_prefix, expected",
436+
[
437+
("cache", "d", "dir"),
438+
("cache", "in", "info"),
439+
("cache", "l", "list"),
440+
("cache", "re", "remove"),
441+
("cache", "pu", "purge"),
442+
("config", "li", "list"),
443+
("config", "e", "edit"),
444+
("config", "ge", "get"),
445+
("config", "se", "set"),
446+
("config", "unse", "unset"),
447+
("config", "d", "debug"),
448+
("index", "ve", "versions"),
449+
],
450+
)
451+
def test_completion_for_action_handler(
452+
subcommand: str, handler_prefix: str, expected: str, autocomplete: DoAutocomplete
453+
) -> None:
454+
res, _ = autocomplete(f"pip {subcommand} {handler_prefix}", cword="2")
455+
456+
assert [expected] == res.stdout.split()
457+
458+
459+
def test_completion_for_action_handler_handler_not_repeated(
460+
autocomplete: DoAutocomplete,
461+
) -> None:
462+
res, _ = autocomplete("pip cache remove re", cword="3")
463+
464+
assert [] == res.stdout.split()

0 commit comments

Comments
 (0)