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
51 changes: 21 additions & 30 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -466,36 +466,27 @@ Then, to install from local only, you'll be using :ref:`--find-links

$ pip install --no-index --find-links=DIR -r requirements.txt


"Only if needed" Recursive Upgrade
**********************************

``pip install --upgrade`` is currently written to perform an eager recursive
upgrade, i.e. it upgrades all dependencies regardless of whether they still
satisfy the new parent requirements.

E.g. supposing:

* `SomePackage-1.0` requires `AnotherPackage>=1.0`
* `SomePackage-2.0` requires `AnotherPackage>=1.0` and `OneMorePoject==1.0`
* `SomePackage-1.0` and `AnotherPackage-1.0` are currently installed
* `SomePackage-2.0` and `AnotherPackage-2.0` are the latest versions available on PyPI.

Running ``pip install --upgrade SomePackage`` would upgrade `SomePackage` *and*
`AnotherPackage` despite `AnotherPackage` already being satisifed.

pip doesn't currently have an option to do an "only if needed" recursive
upgrade, but you can achieve it using these 2 steps::

pip install --upgrade --no-deps SomePackage
pip install SomePackage

The first line will upgrade `SomePackage`, but not dependencies like
`AnotherPackage`. The 2nd line will fill in new dependencies like
`OneMorePackage`.

See :issue:`59` for a plan of making "only if needed" recursive the default
behavior for a new ``pip upgrade`` command.
.. _only-if-needed-recursive-upgrade:

Different Kinds of Recursive Upgrades
*************************************

The new command ``pip upgrade`` upgrades the package or packages specified,
and any dependencies that need updating to satisfy constraints. The command
``pip install --upgrade`` has been deprecated and will be removed in the future.
The latter command performs an eager recursive upgrade, i.e., it upgrades all
dependencies regardless of whether they still satisfy the new parent requirements.
To achieve this with the new ``pip upgrade`` command, use the ``--recursive``
option.

The ``pip upgrade`` command uses a naive dependency resolution algorithm,
which can cause suboptimal results in the presence of conflicting dependency
constraints. It can be guided using the ``--constraint`` option.
See :issue:`988` for plans on adding a more advanced resolution algorithm.

A use case that is still not satisfied is upgrading everything that has been
installed in a virtualenv. See :issue:`59` for a plan of adding ``pip upgrade --all``
to achieve this.


User Installs
Expand Down
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
98 changes: 69 additions & 29 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,24 @@ 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.'
)

if self.recursive_option:
cmd_opts.add_option(
'-R', '--recursive',
dest='recursive',
action='store_true',
help='Upgrade all dependencies recursively, regardless '
'of whether they are already satisfied.',
)

cmd_opts.add_option(
'--force-reinstall',
Expand Down Expand Up @@ -204,6 +199,15 @@ def run(self, options, args):
)
options.ignore_installed = True

if self.upgrade_option and options.upgrade:
warnings.warn(
"pip install --upgrade has been deprecated and will be "
"removed in the future. The command pip upgrade should be "
"used instead, with --recursive if the exact behavior of "
"--upgrade is required.",
RemovedInPip10Warning,
)

if options.build_dir:
options.build_dir = os.path.abspath(options.build_dir)

Expand Down Expand Up @@ -255,7 +259,11 @@ 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_command or (
self.upgrade_option and options.upgrade),
upgrade_recursive=(
(self.upgrade_option and options.upgrade) or
(self.recursive_option and options.recursive)),
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 @@ -342,10 +350,12 @@ def run(self, options, args):
for item in os.listdir(lib_dir):
target_item_dir = os.path.join(options.target_dir, item)
if os.path.exists(target_item_dir):
if not options.upgrade:
if not (self.upgrade_command or
(self.upgrade_option and options.upgrade)):
logger.warning(
'Target directory %s already exists. Specify '
'--upgrade to force replacement.',
'Target directory %s already exists. Use '
'pip upgrade or pip install --upgrade '
'to force replacement.',
target_item_dir
)
continue
Expand All @@ -369,3 +379,33 @@ 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_command = False
upgrade_option = True
recursive_option = False


class UpgradeCommand(InstallBase):
"""
Upgrade packages, with minimal upgrades of dependencies.
"""
name = 'upgrade'
summary = 'Upgrade packages.'
upgrade_command = True
upgrade_option = False
recursive_option = True
20 changes: 16 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_recursive=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 All @@ -153,6 +154,11 @@ def __init__(self, build_dir, src_dir, download_dir, upgrade=False,
unpacking.
:param wheel_cache: The pip wheel cache, for passing to
InstallRequirement.
:param upgrade: Whether the direct requirements (and possibly their
dependencies) should be upgraded.
:param upgrade_recursive: Whether all dependencies should be upgraded
even if not needed to satisfy new lower bounds of directly
upgraded packages.
"""
if session is None:
raise TypeError(
Expand All @@ -167,6 +173,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_recursive = upgrade_recursive
self.ignore_installed = ignore_installed
self.force_reinstall = force_reinstall
self.requirements = Requirements()
Expand Down Expand Up @@ -227,6 +234,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 +374,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_this = self.upgrade and (
self.upgrade_recursive or req_to_install.direct)
if upgrade_this:
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_this)
except BestVersionAlreadyInstalled:
skip_reason = 'up-to-date'
best_installed = True
Expand Down Expand Up @@ -408,6 +418,8 @@ def _prepare_file(self, finder, req_to_install):
return []

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

if req_to_install.editable:
logger.info('Obtaining %s', req_to_install)
Expand Down Expand Up @@ -469,7 +481,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_this)
# 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 +536,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_this 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.
9 changes: 4 additions & 5 deletions tests/functional/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,8 @@ def test_install_package_with_target(script):
assert not Path('scratch') / 'target' / 'simple' in result.files_updated

# Test upgrade call, check that new version is installed
result = script.pip_install_local('--upgrade', '-t',
target_dir, "simple==2.0")
result = script.pip_upgrade_local(
'--recursive', '-t', target_dir, "simple==2.0")
assert Path('scratch') / 'target' / 'simple' in result.files_updated, (
str(result)
)
Expand All @@ -458,8 +458,7 @@ def test_install_package_with_target(script):
singlemodule_py = Path('scratch') / 'target' / 'singlemodule.py'
assert singlemodule_py in result.files_created, str(result)

result = script.pip_install_local('-t', target_dir, 'singlemodule==0.0.1',
'--upgrade')
result = script.pip_upgrade_local('-t', target_dir, 'singlemodule==0.0.1')
assert singlemodule_py in result.files_updated, str(result)


Expand Down Expand Up @@ -660,7 +659,7 @@ def test_install_upgrade_editable_depending_on_other_editable(script):
version='0.1',
install_requires=['pkga'])
"""))
script.pip('install', '--upgrade', '--editable', pkgb_path)
script.pip('upgrade', '--recursive', '--editable', pkgb_path)
result = script.pip('list')
assert "pkgb" in result.stdout

Expand Down
Loading