diff --git a/docs/quickstart.rst b/docs/quickstart.rst index efd6d348630..560048d6999 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -11,7 +11,7 @@ Install a package from `PyPI`_: [...] Successfully installed SomePackage -Install a package already downloaded from `PyPI`_ or got elsewhere. +Install a package already downloaded from `PyPI`_ or obtained via other means. This is useful if the target machine does not have a network connection: :: @@ -39,11 +39,11 @@ List what packages are outdated: $ pip list --outdated SomePackage (Current: 1.0 Latest: 2.0) -Upgrade a package: +To upgrade an existing package, simply run the install command again: :: - $ pip install --upgrade SomePackage + $ pip install SomePackage [...] Found existing installation: SomePackage 1.0 Uninstalling SomePackage: diff --git a/docs/reference/pip_install.rst b/docs/reference/pip_install.rst index 6dac31e8c02..e5de9a53979 100644 --- a/docs/reference/pip_install.rst +++ b/docs/reference/pip_install.rst @@ -758,13 +758,6 @@ Examples $ pip install -r requirements.txt -#. Upgrade an already installed `SomePackage` to the latest from PyPI. - - :: - - $ pip install --upgrade SomePackage - - #. Install a local project in "editable" mode. See the section on :ref:`Editable Installs `. :: diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 6bffdc22a4b..22363b68734 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -472,32 +472,29 @@ $ 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. +``pip install``, now, defaults to performs a "only if needed" recursive +upgrade, i.e. it upgrades dependencies only when they no longer satisfy the +new parent requirements. This was not always the case. -E.g. supposing: +Before pip 9.0, the upgrade behaviour defaulted to a different "eager" +upgrade, i.e. it used to upgrade all dependencies regardless of whether they +already satisfied the new parent requirements. This behaviour was seen as +problematic and was replaced with the current behaviour in pip 9.0. This +section contained instructions on how to work around the problematic +behaviour. The recommended fix for that behaviour, now, is to upgrade to +pip 9.0 or greater. -* `SomePackage-1.0` requires `AnotherPackage>=1.0` -* `SomePackage-2.0` requires `AnotherPackage>=1.0` and `OneMorePackage==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. +See :issue:`59` and :issue:`3786` for the discussion on this change in +behaviour. -Running ``pip install --upgrade SomePackage`` would upgrade `SomePackage` *and* -`AnotherPackage` despite `AnotherPackage` already being satisfied. -pip doesn't currently have an option to do an "only if needed" recursive -upgrade, but you can achieve it using these 2 steps:: +As an historic note, the fix for the behaviour was:: - pip install --upgrade --no-deps SomePackage - pip install SomePackage + 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. +A proposal for an ``upgrade-all`` command is being considered as a safer +alternative to the earlier behaviour of eager upgrading. User Installs @@ -565,10 +562,6 @@ From within a real python, where ``SomePackage`` *is* installed globally, but is $ pip install --user SomePackage [...] - Requirement already satisfied (use --upgrade to upgrade) - - $ pip install --user --upgrade SomePackage - [...] Successfully installed SomePackage @@ -576,10 +569,6 @@ From within a real python, where ``SomePackage`` *is* installed globally, and is $ pip install --user SomePackage [...] - Requirement already satisfied (use --upgrade to upgrade) - - $ pip install --user --upgrade SomePackage - [...] Requirement already up-to-date: SomePackage # force the install @@ -657,7 +646,7 @@ You can then install from the archive like this:: $ tempdir=$(mktemp -d /tmp/wheelhouse-XXXXX) $ (cd $tempdir; tar -xvf /path/to/bundled.tar.bz2) - $ pip install --force-reinstall --ignore-installed --upgrade --no-index --no-deps $tempdir/* + $ pip install --force-reinstall --ignore-installed --no-index --no-deps $tempdir/* Note that compiled packages are typically OS- and architecture-specific, so these archives are not necessarily portable across machines. diff --git a/pip/commands/install.py b/pip/commands/install.py index 28d30c59f7b..db2926799bc 100644 --- a/pip/commands/install.py +++ b/pip/commands/install.py @@ -67,9 +67,7 @@ def __init__(self, *args, **kw): metavar='dir', default=None, help='Install packages into . ' - 'By default this will not replace existing files/folders in ' - '. Use --upgrade to replace existing packages in ' - 'with new versions.' + 'This will not replace existing files/folders in .' ) cmd_opts.add_option( @@ -87,17 +85,14 @@ def __init__(self, *args, **kw): '-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.' + help='No-op option. Kept for backwards compatibility.' ) cmd_opts.add_option( '--force-reinstall', dest='force_reinstall', action='store_true', - help='When upgrading, reinstall all packages even if they are ' - 'already up-to-date.') + help='Reinstall all packages even if they are already up-to-date.') cmd_opts.add_option( '-I', '--ignore-installed', @@ -268,7 +263,6 @@ def run(self, options, args): build_dir=build_dir, src_dir=options.src_dir, download_dir=options.download_dir, - upgrade=options.upgrade, as_egg=options.as_egg, ignore_installed=options.ignore_installed, ignore_dependencies=options.ignore_dependencies, @@ -368,26 +362,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: - logger.warning( - 'Target directory %s already exists. Specify ' - '--upgrade to force replacement.', - target_item_dir - ) - continue - if os.path.islink(target_item_dir): - logger.warning( - 'Target directory %s already exists and is ' - 'a link. Pip will not automatically replace ' - 'links, please remove if replacement is ' - 'desired.', - target_item_dir - ) - continue - if os.path.isdir(target_item_dir): - shutil.rmtree(target_item_dir) - else: - os.remove(target_item_dir) + logger.warning( + 'Skipping target directory %s as it already ' + 'exists.', + target_item_dir + ) + continue shutil.move( os.path.join(lib_dir, item), diff --git a/pip/commands/wheel.py b/pip/commands/wheel.py index 53f85e31b27..a8096196de1 100644 --- a/pip/commands/wheel.py +++ b/pip/commands/wheel.py @@ -107,18 +107,18 @@ def check_required_packages(self): "'pip wheel' requires the 'wheel' package. To fix this, run: " "pip install wheel" ) + + need_setuptools_message = ( + "'pip wheel' requires setuptools >= 0.8 for dist-info support. " + "To fix this, run: pip install setuptools>=0.8" + ) pkg_resources = import_or_raise( 'pkg_resources', CommandError, - "'pip wheel' requires setuptools >= 0.8 for dist-info support." - " To fix this, run: pip install --upgrade setuptools" + need_setuptools_message ) if not hasattr(pkg_resources, 'DistInfoDistribution'): - raise CommandError( - "'pip wheel' requires setuptools >= 0.8 for dist-info " - "support. To fix this, run: pip install --upgrade " - "setuptools" - ) + raise CommandError(need_setuptools_message) def run(self, options, args): self.check_required_packages() diff --git a/pip/req/req_set.py b/pip/req/req_set.py index a4e6b0e161b..7f0e70a4aee 100644 --- a/pip/req/req_set.py +++ b/pip/req/req_set.py @@ -139,7 +139,7 @@ def prep_for_dist(self): class RequirementSet(object): - def __init__(self, build_dir, src_dir, download_dir, upgrade=False, + def __init__(self, build_dir, src_dir, download_dir, ignore_installed=False, as_egg=False, target_dir=None, ignore_dependencies=False, force_reinstall=False, use_user_site=False, session=None, pycompile=True, @@ -169,7 +169,6 @@ def __init__(self, build_dir, src_dir, download_dir, upgrade=False, # be combined if we're willing to have non-wheel archives present in # the wheelhouse output by 'pip wheel'. self.download_dir = download_dir - self.upgrade = upgrade self.ignore_installed = ignore_installed self.force_reinstall = force_reinstall self.requirements = Requirements() @@ -241,6 +240,8 @@ 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.is_direct = (parent_req_name is None) + if not name: # url or path requirement w/o an egg fragment self.unnamed_requirements.append(install_req) @@ -396,17 +397,20 @@ def _check_skip_installed(self, req_to_install, finder): # Check whether to upgrade/reinstall this req or not. req_to_install.check_if_exists() if req_to_install.satisfied_by: - skip_reason = 'satisfied (use --upgrade to upgrade)' - if self.upgrade: - best_installed = False + upgrade_allowed = req_to_install.is_direct + + # Is the best version is installed. + best_installed = False + + if upgrade_allowed: # 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_allowed) except BestVersionAlreadyInstalled: - skip_reason = 'up-to-date' best_installed = True except DistributionNotFound: # No distribution found, so we squash the @@ -423,6 +427,17 @@ def _check_skip_installed(self, req_to_install, finder): req_to_install.conflicts_with = \ req_to_install.satisfied_by req_to_install.satisfied_by = None + + # Figure out a nice message to say why we're skipping this. + if best_installed: + skip_reason = 'already up-to-date' + elif not upgrade_allowed: + # NOTE: Change this message if someday the upgrade strategy + # changes. + skip_reason = 'not upgraded as not directly required' + else: + skip_reason = 'already satisfied' + return skip_reason else: return None @@ -443,6 +458,7 @@ def _prepare_file(self, return [] req_to_install.prepared = True + upgrade_allowed = req_to_install.is_direct # ###################### # # # print log messages # # @@ -463,7 +479,7 @@ def _prepare_file(self, 'req_to_install.satisfied_by is set to %r' % (req_to_install.satisfied_by,)) logger.info( - 'Requirement already %s: %s', skip_reason, + 'Requirement %s: %s', skip_reason, req_to_install) else: if (req_to_install.link and @@ -519,7 +535,7 @@ def _prepare_file(self, % (req_to_install, req_to_install.source_dir) ) req_to_install.populate_link( - finder, self.upgrade, require_hashes) + finder, upgrade_allowed, require_hashes) # 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 @@ -609,7 +625,7 @@ def _prepare_file(self, 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_allowed 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 @@ -620,8 +636,7 @@ def _prepare_file(self, req_to_install.satisfied_by = None else: logger.info( - 'Requirement already satisfied (use ' - '--upgrade to upgrade): %s', + 'Requirement already satisfied: %s', req_to_install, ) diff --git a/pip/utils/outdated.py b/pip/utils/outdated.py index 2164cc3cc2f..2bc65a56d8a 100644 --- a/pip/utils/outdated.py +++ b/pip/utils/outdated.py @@ -151,7 +151,7 @@ def pip_version_check(session): logger.warning( "You are using pip version %s, however version %s is " "available.\nYou should consider upgrading via the " - "'%s install --upgrade pip' command.", + "'%s install pip' command.", pip_version, pypi_version, pip_cmd ) diff --git a/tests/data/packages/README.txt b/tests/data/packages/README.txt index c36d77f1b0e..b2f61b5138c 100644 --- a/tests/data/packages/README.txt +++ b/tests/data/packages/README.txt @@ -108,3 +108,8 @@ requires_wheelbroken_upper -------------------------- Requires wheelbroken and upper - used for testing implicit wheel building during install. + +require_simple-1.0.tar.gz +------------------------ +contains "require_simple" package which requires simple>=2.0 - used for testing +if dependencies are handled correctly. diff --git a/tests/data/packages/require_simple-1.0.tar.gz b/tests/data/packages/require_simple-1.0.tar.gz new file mode 100644 index 00000000000..c4eca04a783 Binary files /dev/null and b/tests/data/packages/require_simple-1.0.tar.gz differ diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 1440f3335f9..8b088618b0e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -527,33 +527,17 @@ def test_install_package_with_target(script): str(result) ) - # Test repeated call without --upgrade, no files should have changed + # Test repeated call, no files should have changed result = script.pip_install_local( '-t', target_dir, "simple==1.0", expect_stderr=True, ) 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") - assert Path('scratch') / 'target' / 'simple' in result.files_updated, ( - str(result) - ) - egg_folder = ( - Path('scratch') / 'target' / 'simple-2.0-py%s.egg-info' % pyversion) - assert egg_folder in result.files_created, ( - str(result) - ) - - # Test install and upgrade of single-module package + # Test install of single-module package result = script.pip_install_local('-t', target_dir, 'singlemodule==0.0.0') 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') - assert singlemodule_py in result.files_updated, str(result) - def test_install_package_with_root(script, data): """ @@ -815,7 +799,7 @@ def test_install_upgrade_editable_depending_on_other_editable(script): version='0.1', install_requires=['pkga']) """)) - script.pip('install', '--upgrade', '--editable', pkgb_path, '--no-index') + script.pip('install', '--editable', pkgb_path, '--no-index') result = script.pip('list', '--format=freeze') assert "pkgb==0.1" in result.stdout @@ -1026,3 +1010,89 @@ def test_double_install_fail(script, data): msg = ("Double requirement given: pip==7.1.2 (already in pip==*, " "name='pip')") assert msg in result.stderr + + +@pytest.mark.network +def test_install_when_older_version_explicitly_passed_as_url(script): + """ + When a file URL is passed, forcefully reinstall the package regardless + of whether it has a newer version already installed. + + """ + result = script.pip('install', 'INITools==0.3', expect_error=True) + assert script.site_packages / 'initools' in result.files_created, ( + sorted(result.files_created.keys()) + ) + result2 = script.pip( + 'install', + 'https://pypi.python.org/packages/source/I/INITools/INITools-' + '0.2.tar.gz', + expect_error=True, + ) + assert result2.files_updated, ( + 'INITools==0.2 was not installed over INITools==0.3 when explicitly ' + 'passed with a URL' + ) + + +def test_install_when_older_version_explicitly_passed_as_path(script, data): + """ + When a file path is passed, forcefully reinstall the package regardless + of whether it has a newer version already installed. + + """ + result = script.pip_install_local('simple==3.0', expect_error=True) + assert script.site_packages / 'simple' in result.files_created, ( + sorted(result.files_created.keys()) + ) + result2 = script.pip_install_local( + data.packages.join("simple-2.0.tar.gz"), + expect_error=True, + ) + assert result2.files_updated, ( + 'simple==0.2 was not installed over simple==0.3 when explicitly ' + 'passed with a path' + ) + + +@pytest.mark.network +def test_install_when_same_version_explicitly_passed_as_url(script): + """ + When a file URL is passed, forcefully reinstall the package regardless + of whether it has a newer version already installed. + + """ + result = script.pip('install', 'INITools==0.3', expect_error=True) + assert script.site_packages / 'initools' in result.files_created, ( + sorted(result.files_created.keys()) + ) + result2 = script.pip( + 'install', + 'https://pypi.python.org/packages/source/I/INITools/INITools-' + '0.3.tar.gz', + expect_error=True, + ) + assert result2.files_updated, ( + 'INITools==0.3 was not installed over INITools==0.3 when explicitly ' + 'passed with a URL' + ) + + +def test_install_when_same_version_explicitly_passed_as_path(script, data): + """ + When a file path is passed, forcefully reinstall the package regardless + of whether it has a newer version already installed. + + """ + result = script.pip_install_local('simple==3.0', expect_error=True) + assert script.site_packages / 'simple' in result.files_created, ( + sorted(result.files_created.keys()) + ) + result2 = script.pip_install_local( + data.packages.join("simple-3.0.tar.gz"), + expect_error=True, + ) + assert result2.files_updated, ( + 'simple==0.3 was not installed over simple==0.3 when explicitly ' + 'passed with a path' + ) diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index f4a78f0ca7c..1ba25ce4a5d 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -11,17 +11,76 @@ from tests.lib.local_repos import local_checkout -def test_no_upgrade_unless_requested(script): +def test_upgrade_by_default(script): """ - No upgrade if not specifically requested. + It does upgrade if not specifically requested. """ script.pip('install', 'INITools==0.1', expect_error=True) result = script.pip('install', 'INITools', expect_error=True) - assert not result.files_created, ( - 'pip install INITools upgraded when it should not have' + assert result.files_created, ( + 'pip install INITools did not upgrade when it should have' + ) + + +def test_does_not_upgrade_dependecies_if_existing_version_satisfies(script): + """ + It doesn't upgrade a dependency if it already satisfies the requirements. + + """ + script.pip_install_local('simple==2.0', expect_error=True) + result = script.pip_install_local('require_simple', expect_error=True) + + assert ( + (script.site_packages / 'require_simple-1.0-py%s.egg-info' % pyversion) + not in result.files_deleted + ), "should have installed require_simple==1.0" + assert ( + (script.site_packages / 'simple-2.0-py%s.egg-info' % pyversion) + not in result.files_deleted + ), "should not have uninstalled simple==2.0" + + +def test_upgrade_dependecies_if_existing_version_does_not_satisfy(script): + """ + It does upgrade a dependency if it already satisfies the requirements. + + """ + script.pip_install_local('simple==1.0', expect_error=True) + result = script.pip_install_local('require_simple', expect_error=True) + + assert ( + (script.site_packages / 'require_simple-1.0-py%s.egg-info' % pyversion) + not in result.files_deleted + ), "should have installed require_simple==1.0" + assert ( + script.site_packages / 'simple-3.0-py%s.egg-info' % + pyversion in result.files_created + ), "should have installed simple==3.0" + assert ( + script.site_packages / 'simple-1.0-py%s.egg-info' % + pyversion in result.files_deleted + ), "should have uninstalled simple==1.0" + + +def test_upgrade_option_does_not_affect_behaviour(script): + """ + It does not change behaviour on passing --upgrade. + """ + script.pip_install_local('simple==2.0', expect_error=True) + result = script.pip_install_local( + 'require_simple', '--upgrade', expect_error=True ) + assert ( + (script.site_packages / 'require_simple-1.0-py%s.egg-info' % pyversion) + not in result.files_deleted + ), "should have installed require_simple==1.0" + assert ( + (script.site_packages / 'simple-2.0-py%s.egg-info' % pyversion) + not in result.files_deleted + ), "should not have uninstalled simple==2.0" + @pytest.mark.network def test_upgrade_to_specific_version(script): @@ -51,8 +110,8 @@ def test_upgrade_if_requested(script): """ script.pip('install', 'INITools==0.1', expect_error=True) - result = script.pip('install', '--upgrade', 'INITools', expect_error=True) - assert result.files_created, 'pip install --upgrade did not upgrade' + result = script.pip('install', 'INITools', expect_error=True) + assert result.files_created, 'pip install did not upgrade' assert ( script.site_packages / 'INITools-0.1-py%s.egg-info' % pyversion not in result.files_created @@ -66,7 +125,7 @@ def test_upgrade_with_newest_already_installed(script, data): """ script.pip('install', '-f', data.find_links, '--no-index', 'simple') result = script.pip( - 'install', '--upgrade', '-f', data.find_links, '--no-index', 'simple' + 'install', '-f', data.find_links, '--no-index', 'simple' ) assert not result.files_created, 'simple upgraded when it should not have' assert 'already up-to-date' in result.stdout, result.stdout @@ -83,7 +142,7 @@ def test_upgrade_force_reinstall_newest(script): sorted(result.files_created.keys()) ) result2 = script.pip( - 'install', '--upgrade', '--force-reinstall', 'INITools' + 'install', '--force-reinstall', 'INITools' ) assert result2.files_updated, 'upgrade to INITools 0.3 failed' result3 = script.pip('uninstall', 'initools', '-y', expect_error=True) @@ -130,8 +189,8 @@ def test_uninstall_before_upgrade_from_url(script): @pytest.mark.network def test_upgrade_to_same_version_from_url(script): """ - When installing from a URL the same version that is already installed, no - need to uninstall and reinstall if --upgrade is not specified. + When installing from a URL the same version that is already installed, + ensure an uninstall and reinstall take place. """ result = script.pip('install', 'INITools==0.3', expect_error=True) @@ -144,7 +203,7 @@ def test_upgrade_to_same_version_from_url(script): '0.3.tar.gz', expect_error=True, ) - assert not result2.files_updated, 'INITools 0.3 reinstalled same version' + assert result2.files_updated, 'INITools 0.3 did not reinstall same version' result3 = script.pip('uninstall', 'initools', '-y', expect_error=True) assert_all_changes(result, result3, [script.venv / 'build', 'cache']) @@ -169,7 +228,7 @@ def test_upgrade_from_reqs_file(script): INITools """)) script.pip( - 'install', '--upgrade', '-r', script.scratch_path / 'test-req.txt' + 'install', '-r', script.scratch_path / 'test-req.txt' ) uninstall_result = script.pip( 'uninstall', '-r', script.scratch_path / 'test-req.txt', '-y' @@ -277,7 +336,7 @@ def test_upgrade_vcs_req_with_no_dists_found(script, tmpdir): tmpdir.join("cache"), ) script.pip("install", req) - result = script.pip("install", "-U", req) + result = script.pip("install", req) assert not result.returncode @@ -294,7 +353,7 @@ def test_upgrade_vcs_req_with_dist_found(script): ) ) script.pip("install", req, expect_stderr=True) - result = script.pip("install", "-U", req, expect_stderr=True) + result = script.pip("install", req, expect_stderr=True) assert "pypi.python.org" not in result.stdout, result.stdout @@ -304,13 +363,18 @@ class TestUpgradeDistributeToSetuptools(object): allow distribute to conflict with setuptools, so that the following would work to upgrade distribute: - ``pip install -U setuptools`` + ``pip install -U setuptools`` In pip7, the hacks were removed. This test remains to at least confirm pip can upgrade distribute to setuptools using: ``pip install -U distribute`` + In pip9, install started upgrading by default, thus removing the need to + pass -U: + + ``pip install distribute`` + The reason this works is that a final version of distribute (v0.7.3) was released that is simple wrapper with: @@ -354,7 +418,7 @@ def test_from_distribute_6_to_setuptools_7( ) result = self.script.run( self.ve_bin / 'pip', 'install', '--no-index', - '--find-links=%s' % data.find_links, '-U', 'distribute', + '--find-links=%s' % data.find_links, 'distribute', expect_stderr=True if sys.version_info[:2] == (2, 6) else False, ) assert (