diff --git a/news/4551.feature.rst b/news/4551.feature.rst new file mode 100644 index 00000000000..77cd3578dab --- /dev/null +++ b/news/4551.feature.rst @@ -0,0 +1 @@ +Added an ``upgrade-all`` command. This command will update all packages that can be updated. diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index e1fb8788428..8e0d2f59b1d 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -47,6 +47,10 @@ 'pip._internal.commands.check', 'CheckCommand', 'Verify installed packages have compatible dependencies.', )), + ('upgrade-all', CommandInfo( + 'pip._internal.commands.upgrade_all', 'UpgradeAllCommand', + 'Upgrade all packages to latest version', + )), ('config', CommandInfo( 'pip._internal.commands.configuration', 'ConfigurationCommand', 'Manage local and global configuration.', diff --git a/src/pip/_internal/commands/upgrade_all.py b/src/pip/_internal/commands/upgrade_all.py new file mode 100644 index 00000000000..c99e32df0f3 --- /dev/null +++ b/src/pip/_internal/commands/upgrade_all.py @@ -0,0 +1,105 @@ +import logging +from optparse import Values +from typing import List + +from pip._internal.commands.install import InstallCommand +from pip._internal.commands.list import ListCommand +from pip._internal.utils.compat import stdlib_pkgs +from pip._internal.utils.misc import get_installed_distributions + +logger = logging.getLogger(__name__) + + +class UpgradeAllCommand(InstallCommand, ListCommand): + """ + Upgrades all out of date packages, exactly like this old oneliner used to do: + pip list --format freeze | \ + grep --invert-match "pkg-resources" | \ + cut --delimiter "=" --fields 1 | \ + xargs pip install --upgrade + """ + usage = """ + %prog [options] + %prog [options] [-e] ... + %prog [options] [-e] ... + %prog [options] ...""" + + def add_options(self): + # type: () -> None + # install all options from installcommand + InstallCommand.add_options(self) + # we don't upgrade in editable mode AND listcommand also has an editable option + self.cmd_opts.remove_option('--editable') + # redefine user later + self.cmd_opts.remove_option('--user') + # upgrade all always upgrade, so the help text for target makes no sense + self.cmd_opts.remove_option('--target') + # we always upgrade + self.cmd_opts.remove_option('--upgrade') + # pre is defined in installcommand and listcommand, so remove it once + self.cmd_opts.remove_option('--pre') + self.cmd_opts.remove_option('--index-url') + self.cmd_opts.remove_option('--extra-index-url') + self.cmd_opts.remove_option('--no-index') + self.cmd_opts.remove_option('--find-links') + + # install command will have added the cmd_opts to self.parser already, + # so pop them here because well' add them again later + del self.parser.option_groups[0] + # same for package index options + del self.parser.option_groups[0] + + # install all options from listcommand + ListCommand.add_options(self) + + # also remove options listcommand have added so we can add our real + # options later + del self.parser.option_groups[0] + + # redefine user later + self.cmd_opts.remove_option('--user') + self.cmd_opts.add_option( + '--user', + dest='user', + action='store_true', + default=False, + help='Only upgrade packages installed in user-site.') + self.cmd_opts.add_option( + '-t', '--target', + dest='target_dir', + metavar='dir', + default=None, + help='Install packages into . ' + 'This will replace existing files/folders in ' + ' with new versions.' + ) + + self.parser.insert_option_group(0, self.cmd_opts) + + def run(self, options, args): + # type: (Values, List[str]) -> int + skip = set(stdlib_pkgs) + if options.excludes: + skip.update(options.excludes) + + packages = get_installed_distributions( + local_only=options.local, + user_only=options.user, + editables_only=options.editable, + include_editables=options.include_editable, + paths=options.path, + skip=skip, + ) + if options.not_required: + packages = self.get_not_required(packages, options) + + if options.outdated: + packages = self.get_outdated(packages, options) + packages = [dist.project_name for dist in packages] + + logging.info("upgrading %s", packages) + + options.upgrade = True + # we don't upgrade in editable mode + options.editable = False + return InstallCommand.run(self, options, packages) diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index cb144c5f6da..2d1ff99af41 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -11,8 +11,7 @@ # These are the expected names of the commands whose classes inherit from # IndexGroupCommand. -EXPECTED_INDEX_GROUP_COMMANDS = [ - 'download', 'index', 'install', 'list', 'wheel'] +EXPECTED_INDEX_GROUP_COMMANDS = ['download', 'index', 'install', 'list', 'upgrade-all', 'wheel'] def check_commands(pred, expected): @@ -50,9 +49,8 @@ def test_session_commands(): def is_session_command(command): return isinstance(command, SessionCommandMixin) - expected = [ - 'download', 'index', 'install', 'list', 'search', 'uninstall', 'wheel' - ] + expected = ['download', 'index' 'install', 'list', 'search', 'uninstall', + 'upgrade-all', 'wheel'] check_commands(is_session_command, expected) @@ -113,5 +111,5 @@ def test_requirement_commands(): """ def is_requirement_command(command): return isinstance(command, RequirementCommand) - - check_commands(is_requirement_command, ['download', 'install', 'wheel']) + check_commands(is_requirement_command, ['download', 'install', 'upgrade-all', + 'wheel'])