Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: include sub-actions in tab completion #13140

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/0741cad6-3007-47f8-9c53-984e9116c7ff.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix: include sub-actions in tab completion
4 changes: 4 additions & 0 deletions src/pip/_internal/cli/autocompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ def autocomplete() -> None:
if option[1] and option[0][:2] == "--":
opt_label += "="
print(opt_label)

for handler_name in subcommand.handler_map():
if handler_name.startswith(current):
print(handler_name)
else:
# show main parser options only when necessary

Expand Down
8 changes: 7 additions & 1 deletion src/pip/_internal/cli/base_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sys
import traceback
from optparse import Values
from typing import List, Optional, Tuple
from typing import Callable, Dict, List, Optional, Tuple

from pip._vendor.rich import reconfigure
from pip._vendor.rich import traceback as rich_traceback
Expand Down Expand Up @@ -229,3 +229,9 @@ def _main(self, args: List[str]) -> int:
options.cache_dir = None

return self._run_wrapper(level_number, options, args)

def handler_map(self) -> Dict[str, Callable[[Values, List[str]], None]]:
"""
map of names to handler actions for commands with sub-actions
"""
return {}
25 changes: 14 additions & 11 deletions src/pip/_internal/commands/cache.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import textwrap
from optparse import Values
from typing import Any, List
from typing import Callable, Dict, List

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

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

def run(self, options: Values, args: List[str]) -> int:
handlers = {
def handler_map(self) -> Dict[str, Callable[[Values, List[str]], None]]:
return {
"dir": self.get_cache_dir,
"info": self.get_cache_info,
"list": self.list_cache_items,
"remove": self.remove_cache_items,
"purge": self.purge_cache,
}

def run(self, options: Values, args: List[str]) -> int:
handler_map = self.handler_map()

if not options.cache_dir:
logger.error("pip cache commands can not function since cache is disabled.")
return ERROR

# Determine action
if not args or args[0] not in handlers:
if not args or args[0] not in handler_map:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handlers)),
", ".join(sorted(handler_map)),
)
return ERROR

action = args[0]

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

return SUCCESS

def get_cache_dir(self, options: Values, args: List[Any]) -> None:
def get_cache_dir(self, options: Values, args: List[str]) -> None:
if args:
raise CommandError("Too many arguments")

logger.info(options.cache_dir)

def get_cache_info(self, options: Values, args: List[Any]) -> None:
def get_cache_info(self, options: Values, args: List[str]) -> None:
if args:
raise CommandError("Too many arguments")

Expand Down Expand Up @@ -129,7 +132,7 @@ def get_cache_info(self, options: Values, args: List[Any]) -> None:

logger.info(message)

def list_cache_items(self, options: Values, args: List[Any]) -> None:
def list_cache_items(self, options: Values, args: List[str]) -> None:
if len(args) > 1:
raise CommandError("Too many arguments")

Expand Down Expand Up @@ -161,7 +164,7 @@ def format_for_abspath(self, files: List[str]) -> None:
if files:
logger.info("\n".join(sorted(files)))

def remove_cache_items(self, options: Values, args: List[Any]) -> None:
def remove_cache_items(self, options: Values, args: List[str]) -> None:
if len(args) > 1:
raise CommandError("Too many arguments")

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

def purge_cache(self, options: Values, args: List[Any]) -> None:
def purge_cache(self, options: Values, args: List[str]) -> None:
if args:
raise CommandError("Too many arguments")

Expand Down
15 changes: 9 additions & 6 deletions src/pip/_internal/commands/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import subprocess
from optparse import Values
from typing import Any, List, Optional
from typing import Any, Callable, Dict, List, Optional

from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
Expand Down Expand Up @@ -93,8 +93,8 @@ def add_options(self) -> None:

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

def run(self, options: Values, args: List[str]) -> int:
handlers = {
def handler_map(self) -> Dict[str, Callable[[Values, List[str]], None]]:
return {
"list": self.list_values,
"edit": self.open_in_editor,
"get": self.get_name,
Expand All @@ -103,11 +103,14 @@ def run(self, options: Values, args: List[str]) -> int:
"debug": self.list_config_values,
}

def run(self, options: Values, args: List[str]) -> int:
handler_map = self.handler_map()

# Determine action
if not args or args[0] not in handlers:
if not args or args[0] not in handler_map:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handlers)),
", ".join(sorted(handler_map)),
)
return ERROR

Expand All @@ -131,7 +134,7 @@ def run(self, options: Values, args: List[str]) -> int:

# Error handling happens here, not in the action-handlers.
try:
handlers[action](options, args[1:])
handler_map[action](options, args[1:])
except PipError as e:
logger.error(e.args[0])
return ERROR
Expand Down
15 changes: 9 additions & 6 deletions src/pip/_internal/commands/index.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from optparse import Values
from typing import Any, Iterable, List, Optional
from typing import Any, Callable, Dict, Iterable, List, Optional

from pip._vendor.packaging.version import Version

Expand Down Expand Up @@ -45,30 +45,33 @@ def add_options(self) -> None:
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)

def run(self, options: Values, args: List[str]) -> int:
handlers = {
def handler_map(self) -> Dict[str, Callable[[Values, List[str]], None]]:
return {
"versions": self.get_available_package_versions,
}

def run(self, options: Values, args: List[str]) -> int:
handler_map = self.handler_map()

logger.warning(
"pip index is currently an experimental command. "
"It may be removed/changed in a future release "
"without prior warning."
)

# Determine action
if not args or args[0] not in handlers:
if not args or args[0] not in handler_map:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handlers)),
", ".join(sorted(handler_map)),
)
return ERROR

action = args[0]

# Error handling happens here, not in the action-handlers.
try:
handlers[action](options, args[1:])
handler_map[action](options, args[1:])
except PipError as e:
logger.error(e.args[0])
return ERROR
Expand Down
25 changes: 25 additions & 0 deletions tests/functional/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,28 @@ def test_completion_uses_same_executable_name(
expect_stderr=deprecated_python,
)
assert executable_name in result.stdout


@pytest.mark.parametrize(
"subcommand, handler_prefix, expected",
[
("cache", "d", "dir"),
("cache", "in", "info"),
("cache", "l", "list"),
("cache", "re", "remove"),
("cache", "pu", "purge"),
("config", "li", "list"),
("config", "e", "edit"),
("config", "ge", "get"),
("config", "se", "set"),
("config", "unse", "unset"),
("config", "d", "debug"),
("index", "ve", "versions"),
Comment on lines +429 to +440
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this list could probably be auto-generated from all commands with a non-empty handler_map but I just hard coded it for simplicity (read: laziness 🙃)

],
)
def test_completion_for_action_handler(
subcommand: str, handler_prefix: str, expected: str, autocomplete: DoAutocomplete
) -> None:
res, _ = autocomplete(f"pip {subcommand} {handler_prefix}", cword="2")

assert [expected] == res.stdout.split()
Loading