Skip to content

Implement a "pip upgrade" command #3194

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

Closed
wants to merge 13 commits into from
4 changes: 3 additions & 1 deletion pip/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pip.commands.list import ListCommand
from pip.commands.search import SearchCommand
from pip.commands.show import ShowCommand
from pip.commands.install import InstallCommand
from pip.commands.install import InstallCommand, UpgradeCommand
from pip.commands.uninstall import UninstallCommand
from pip.commands.wheel import WheelCommand

Expand All @@ -22,6 +22,7 @@
SearchCommand.name: SearchCommand,
ShowCommand.name: ShowCommand,
InstallCommand.name: InstallCommand,
UpgradeCommand.name: UpgradeCommand,
UninstallCommand.name: UninstallCommand,
DownloadCommand.name: DownloadCommand,
ListCommand.name: ListCommand,
Expand All @@ -31,6 +32,7 @@

commands_order = [
InstallCommand,
UpgradeCommand,
DownloadCommand,
UninstallCommand,
FreezeCommand,
Expand Down
67 changes: 41 additions & 26 deletions pip/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,16 @@
logger = logging.getLogger(__name__)


class InstallCommand(RequirementCommand):
"""
Install packages from:

- PyPI (and other indexes) using requirement specifiers.
- VCS project urls.
- Local project directories.
- Local or remote source archives.

pip also supports installing from "requirements files", which provide
an easy way to specify a whole environment to be installed.
"""
name = 'install'

class InstallBase(RequirementCommand):
usage = """
%prog [options] <requirement specifier> [package-index-options] ...
%prog [options] -r <requirements file> [package-index-options] ...
%prog [options] [-e] <vcs project url> ...
%prog [options] [-e] <local project path> ...
%prog [options] <archive url/path> ..."""

summary = 'Install packages.'

def __init__(self, *args, **kw):
super(InstallCommand, self).__init__(*args, **kw)
super(InstallBase, self).__init__(*args, **kw)

cmd_opts = self.cmd_opts

Expand Down Expand Up @@ -83,14 +68,15 @@ def __init__(self, *args, **kw):

cmd_opts.add_option(cmdoptions.src())

cmd_opts.add_option(
'-U', '--upgrade',
dest='upgrade',
action='store_true',
help='Upgrade all specified packages to the newest available '
'version. This process is recursive regardless of whether '
'a dependency is already satisfied.'
)
if self.upgrade_option:
cmd_opts.add_option(
'-U', '--upgrade',
dest='upgrade',
action='store_true',
help='Upgrade all specified packages to the newest available '
'version. This process is recursive regardless of '
'whether a dependency is already satisfied.'
)

cmd_opts.add_option(
'--force-reinstall',
Expand Down Expand Up @@ -255,7 +241,8 @@ def run(self, options, args):
build_dir=build_dir,
src_dir=options.src_dir,
download_dir=options.download_dir,
upgrade=options.upgrade,
upgrade=self.upgrade_option and options.upgrade,
upgrade_direct=self.upgrade_direct,
as_egg=options.as_egg,
Copy link
Contributor

Choose a reason for hiding this comment

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

again, maybe bikeshedding, here, but I want the RequirementSet properties to be meaningful for the long term

what we're doing here is keeping the general word "upgrade" to refer to the old style "recursive upgrade", and then tacking on a new property "upgrade_direct" for the new non-recursive type.

maybe that allows for a smaller PR diff, but I care more about readability.

to me the most readable properties for the Set would be:

upgrade: whether it's an upgrade of any kind
upgrade_recursive: is it recursive or not

ignore_installed=options.ignore_installed,
ignore_dependencies=options.ignore_dependencies,
Expand Down Expand Up @@ -369,3 +356,31 @@ def run(self, options, args):
)
shutil.rmtree(temp_target_dir)
return requirement_set


class InstallCommand(InstallBase):
"""
Install packages from:

- PyPI (and other indexes) using requirement specifiers.
- VCS project urls.
- Local project directories.
- Local or remote source archives.

pip also supports installing from "requirements files", which provide
an easy way to specify a whole environment to be installed.
"""
name = 'install'
summary = 'Install packages.'
upgrade_option = True
upgrade_direct = False
Copy link
Contributor

Choose a reason for hiding this comment

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

this may be bikeshedding, but this "upgrade_direct" phrase is pretty meaningless to me.

I'd prefer "upgrade_recursive", which will be True for the InstallCommand, and False for the UpgradeCommand



class UpgradeCommand(InstallBase):
"""
Upgrade packages, with minimal upgrades of dependencies.
"""
name = 'upgrade'
summary = 'Upgrade packages.'
upgrade_option = False
upgrade_direct = True
15 changes: 11 additions & 4 deletions pip/req/req_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def prep_for_dist(self):
class RequirementSet(object):

def __init__(self, build_dir, src_dir, download_dir, upgrade=False,
upgrade_direct=False,
ignore_installed=False, as_egg=False, target_dir=None,
ignore_dependencies=False, force_reinstall=False,
use_user_site=False, session=None, pycompile=True,
Expand Down Expand Up @@ -167,6 +168,7 @@ def __init__(self, build_dir, src_dir, download_dir, upgrade=False,
# the wheelhouse output by 'pip wheel'.
self.download_dir = download_dir
self.upgrade = upgrade
self.upgrade_direct = upgrade_direct
self.ignore_installed = ignore_installed
self.force_reinstall = force_reinstall
self.requirements = Requirements()
Expand Down Expand Up @@ -227,6 +229,7 @@ def add_requirement(self, install_req, parent_req_name=None):
install_req.use_user_site = self.use_user_site
install_req.target_dir = self.target_dir
install_req.pycompile = self.pycompile
install_req.direct = (parent_req_name is None)
if not name:
# url or path requirement w/o an egg fragment
self.unnamed_requirements.append(install_req)
Expand Down Expand Up @@ -366,14 +369,16 @@ def _check_skip_installed(self, req_to_install, finder):
req_to_install.check_if_exists()
if req_to_install.satisfied_by:
skip_reason = 'satisfied (use --upgrade to upgrade)'

Choose a reason for hiding this comment

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

This should say use "pip upgrade" to upgrade.

if self.upgrade:
upgrade = self.upgrade or (
self.upgrade_direct and req_to_install.direct)
if upgrade:
best_installed = False
# For link based requirements we have to pull the
# tree down and inspect to assess the version #, so
# its handled way down.
if not (self.force_reinstall or req_to_install.link):
try:
finder.find_requirement(req_to_install, self.upgrade)
finder.find_requirement(req_to_install, upgrade)
except BestVersionAlreadyInstalled:
skip_reason = 'up-to-date'
best_installed = True
Expand Down Expand Up @@ -408,6 +413,8 @@ def _prepare_file(self, finder, req_to_install):
return []

req_to_install.prepared = True
upgrade = self.upgrade or (
self.upgrade_direct and req_to_install.direct)

if req_to_install.editable:
logger.info('Obtaining %s', req_to_install)
Expand Down Expand Up @@ -469,7 +476,7 @@ def _prepare_file(self, finder, req_to_install):
"can delete this. Please delete it and try again."
% (req_to_install, req_to_install.source_dir)
)
req_to_install.populate_link(finder, self.upgrade)
req_to_install.populate_link(finder, upgrade)
# We can't hit this spot and have populate_link return None.
# req_to_install.satisfied_by is None here (because we're
# guarded) and upgrade has no impact except when satisfied_by
Expand Down Expand Up @@ -524,7 +531,7 @@ def _prepare_file(self, finder, req_to_install):
if not self.ignore_installed:
req_to_install.check_if_exists()
if req_to_install.satisfied_by:
if self.upgrade or self.ignore_installed:
if upgrade or self.ignore_installed:
# don't uninstall conflict if user install and
# conflict is not user install
if not (self.use_user_site and not
Expand Down
Binary file added tests/data/packages/application-1.9.tar.gz
Binary file not shown.
Binary file added tests/data/packages/application-2.0.tar.gz
Binary file not shown.
Binary file added tests/data/packages/application-2.1.tar.gz
Binary file not shown.
Binary file added tests/data/packages/requirement-1.5.tar.gz
Binary file not shown.
Binary file added tests/data/packages/requirement-1.6.tar.gz
Binary file not shown.
Binary file added tests/data/packages/requirement-1.7.tar.gz
Binary file not shown.
86 changes: 86 additions & 0 deletions tests/functional/test_upgrade.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import os
import pytest
from tests.lib import pyversion


# The package "requirement" has versions 1.5, 1.6 and 1.7 available.
# The package "application" has versions 1.9, 2.0 and 2.1 available,
# and 2.0 depends on requirement>=1.5 and 2.1 on requirement>=1.6.
# This is intended to resemble the situation with numpy, where
# several current packages depend on 1.6 or newer, leading
# "pip install -U package" to attempt to install some newer version.

@pytest.mark.network
def test_upgrade_not_recursive(script, data):
"""
If requirement-1.6 is already installed, upgrading application
should not upgrade requirement.
"""
script.pip('install', '--no-index', '-f', data.find_links,
'requirement==1.6', 'application==2.0')
result = script.pip('upgrade', '--no-index', '-f', data.find_links,
'application')
assert (
script.site_packages / 'application-2.0-py%s.egg-info' % pyversion
in result.files_deleted
)
assert (
script.site_packages / 'application-2.1-py%s.egg-info' % pyversion
in result.files_created
)
assert (
'requirement-1.6-py%s.egg-info' % pyversion
in os.listdir(script.site_packages_path)
)


@pytest.mark.network
def test_upgrade_recursive_when_needed(script, data):
"""
If only requirement-1.5 is installed, upgrading application
should upgrade requirement to the newest available version.
"""
script.pip('install', '--no-index', '-f', data.find_links,
'requirement==1.5', 'application==2.0')
result = script.pip('upgrade', '--no-index', '-f', data.find_links,
'application')
assert (
script.site_packages / 'application-2.0-py%s.egg-info' % pyversion
in result.files_deleted
)
assert (
script.site_packages / 'application-2.1-py%s.egg-info' % pyversion
in result.files_created
)
assert (
script.site_packages / 'requirement-1.5-py%s.egg-info' % pyversion
in result.files_deleted
)
assert (
script.site_packages / 'requirement-1.7-py%s.egg-info' % pyversion
in result.files_created
)


@pytest.mark.network
def test_upgrade_installs_when_needed(script, data):
"""
If requirement is not installed, upgrading application should
install the newest available version.
"""
script.pip('install', '--no-index', '-f', data.find_links,
'application==1.9')
result = script.pip('upgrade', '--no-index', '-f', data.find_links,
'application')
assert (
script.site_packages / 'application-1.9-py%s.egg-info' % pyversion
in result.files_deleted
)
assert (
script.site_packages / 'application-2.1-py%s.egg-info' % pyversion
in result.files_created
)
assert (
script.site_packages / 'requirement-1.7-py%s.egg-info' % pyversion
in result.files_created
)