diff --git a/news/8130.feature b/news/8130.feature new file mode 100644 index 00000000000..9b23aacf868 --- /dev/null +++ b/news/8130.feature @@ -0,0 +1,2 @@ +Add aliases 'add' and 'remove' aliases for 'install' and 'uninstall', +respectively. diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 329de602513..4fdd4ef8e20 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -7,7 +7,7 @@ from itertools import chain from pip._internal.cli.main_parser import create_main_parser -from pip._internal.commands import commands_dict, create_command +from pip._internal.commands import create_command, subcommands_set from pip._internal.utils.misc import get_installed_distributions from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -30,7 +30,7 @@ def autocomplete(): current = '' parser = create_main_parser() - subcommands = list(commands_dict) + subcommands = list(subcommands_set) options = [] # subcommand diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 08c82c1f711..ea07dbe6bbd 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -9,8 +9,11 @@ ConfigOptionParser, UpdatingDefaultsHelpFormatter, ) -from pip._internal.commands import commands_dict, get_similar_commands -from pip._internal.exceptions import CommandError +from pip._internal.commands import ( + aliases_of_commands, + check_subcommand, + commands_dict, +) from pip._internal.utils.misc import get_pip_version, get_prog from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -47,10 +50,10 @@ def create_main_parser(): parser.main = True # type: ignore # create command listing for description - description = [''] + [ - '{name:27} {command_info.summary}'.format(**locals()) - for name, command_info in commands_dict.items() - ] + description = [''] + for name, info in commands_dict.items(): + names = ', '.join(aliases_of_commands[name]) + description.append('{:27} {.summary}'.format(names, info)) parser.description = '\n'.join(description) return parser @@ -82,16 +85,7 @@ def parse_command(args): # the subcommand name cmd_name = args_else[0] - - if cmd_name not in commands_dict: - guess = get_similar_commands(cmd_name) - - msg = ['unknown command "{}"'.format(cmd_name)] - if guess: - msg.append('maybe you meant "{}"'.format(guess)) - - raise CommandError(' - '.join(msg)) - + check_subcommand(cmd_name) # all the args without the subcommand cmd_args = args[:] cmd_args.remove(cmd_name) diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 6825fa6e2d4..f8a3c29dd43 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -14,10 +14,11 @@ import importlib from collections import OrderedDict, namedtuple +from pip._internal.exceptions import CommandError from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any + from typing import Any, Dict, List, Set from pip._internal.cli.base_command import Command @@ -94,13 +95,29 @@ )), ]) # type: OrderedDict[str, CommandInfo] +aliases_dict = { + 'add': 'install', + 'remove': 'uninstall', +} # type: Dict[str, str] + +aliases_of_commands = { + name: [name] for name in commands_dict} # type: Dict[str, List[str]] +for alias, name in aliases_dict.items(): + aliases_of_commands[name].append(alias) + +subcommands_set = {cmd for aliases in aliases_of_commands.values() + for cmd in aliases} # type: Set[str] + def create_command(name, **kwargs): # type: (str, **Any) -> Command """ Create an instance of the Command class with the given name. """ - module_path, class_name, summary = commands_dict[name] + try: + module_path, class_name, summary = commands_dict[name] + except KeyError: + module_path, class_name, summary = commands_dict[aliases_dict[name]] module = importlib.import_module(module_path) command_class = getattr(module, class_name) command = command_class(name=name, summary=summary, **kwargs) @@ -114,9 +131,20 @@ def get_similar_commands(name): name = name.lower() - close_commands = get_close_matches(name, commands_dict.keys()) + close_commands = get_close_matches(name, subcommands_set) if close_commands: return close_commands[0] else: return False + + +def check_subcommand(name): + # type: (str) -> None + """Raise CommandError if the given subcommand not found.""" + if name not in aliases_dict and name not in commands_dict: + guess = get_similar_commands(name) + msg = 'unknown command "{}"'.format(name) + if guess: + msg += ' - maybe you meant "{}"'.format(guess) + raise CommandError(msg) diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index a2edc29897f..1e29235dad3 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -2,7 +2,6 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS -from pip._internal.exceptions import CommandError from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -19,9 +18,7 @@ class HelpCommand(Command): def run(self, options, args): # type: (Values, List[str]) -> int - from pip._internal.commands import ( - commands_dict, create_command, get_similar_commands, - ) + from pip._internal.commands import check_subcommand, create_command try: # 'pip help' with no args is handled by pip.__init__.parseopt() @@ -29,15 +26,7 @@ def run(self, options, args): except IndexError: return SUCCESS - if cmd_name not in commands_dict: - guess = get_similar_commands(cmd_name) - - msg = ['unknown command "{}"'.format(cmd_name)] - if guess: - msg.append('maybe you meant "{}"'.format(guess)) - - raise CommandError(' - '.join(msg)) - + check_subcommand(cmd_name) command = create_command(cmd_name) command.parser.print_help() diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py index 00a395006b7..d1e5e8dfd9f 100644 --- a/tests/functional/test_help.py +++ b/tests/functional/test_help.py @@ -2,7 +2,7 @@ from mock import Mock from pip._internal.cli.status_codes import ERROR, SUCCESS -from pip._internal.commands import commands_dict, create_command +from pip._internal.commands import create_command, subcommands_set from pip._internal.exceptions import CommandError @@ -79,7 +79,7 @@ def test_help_commands_equally_functional(in_memory_pip): assert sum(ret) == 0, 'exit codes of: ' + msg assert all(len(o) > 0 for o in out) - for name in commands_dict: + for name in subcommands_set: assert ( in_memory_pip.pip('help', name).stdout == in_memory_pip.pip(name, '--help').stdout != "" diff --git a/tests/lib/options_helpers.py b/tests/lib/options_helpers.py index 2354a818df8..e13a8697caa 100644 --- a/tests/lib/options_helpers.py +++ b/tests/lib/options_helpers.py @@ -3,7 +3,12 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command -from pip._internal.commands import CommandInfo, commands_dict +from pip._internal.commands import ( + CommandInfo, + aliases_of_commands, + commands_dict, + subcommands_set, +) class FakeCommand(Command): @@ -23,6 +28,10 @@ def setup(self): commands_dict['fake'] = CommandInfo( 'tests.lib.options_helpers', 'FakeCommand', 'fake summary', ) + aliases_of_commands['fake'] = ['fake'] + subcommands_set.add('fake') def teardown(self): commands_dict.pop('fake') + aliases_of_commands.pop('fake') + subcommands_set.remove('fake') diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 7fae427c697..e196bc920c8 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -6,7 +6,12 @@ RequirementCommand, SessionCommandMixin, ) -from pip._internal.commands import commands_dict, create_command +from pip._internal.commands import ( + aliases_dict, + commands_dict, + create_command, + subcommands_set, +) # These are the expected names of the commands whose classes inherit from # IndexGroupCommand. @@ -33,12 +38,16 @@ def test_commands_dict__order(): assert names[-1] == 'help' -@pytest.mark.parametrize('name', list(commands_dict)) +@pytest.mark.parametrize('name', subcommands_set) def test_create_command(name): """Test creating an instance of each available command.""" command = create_command(name) assert command.name == name - assert command.summary == commands_dict[name].summary + try: + summary = commands_dict[name].summary + except KeyError: + summary = commands_dict[aliases_dict[name]].summary + assert command.summary == summary def test_session_commands():