diff --git a/AUTHORS.txt b/AUTHORS.txt index d98ed9c4499d3e4a36095f87e901e533fb2940db..61ca48e202ed985b8eca6e8b5b3e875e8d041b86 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -46,6 +46,7 @@ Craig Kerstiens <craig.kerstiens@gmail.com> Cristian Sorinel <cristian.sorinel@gmail.com> Dan Savilonis <djs@n-cube.org> Dan Sully <daniel-github@electricrain.com> +daniel <mcdonaldd@unimelb.edu.au> Daniel Collins <accounts@dac.io> Daniel Hahler <git@thequod.de> Daniel Holth <dholth@fastmail.fm> @@ -67,6 +68,7 @@ Donald Stufft <donald@stufft.io> Dongweiming <dongweiming@admaster.com.cn> Douglas Thor <dougthor42@users.noreply.github.com> Dwayne Bailey <dwayne@translate.org.za> +Emil Styrke <emil.styrke@gmail.com> Endoh Takanao <djmchl@gmail.com> enoch <lanxenet@gmail.com> Eric Gillingham <Gillingham@bikezen.net> @@ -155,6 +157,7 @@ Matthew Iversen <teh.ivo@gmail.com> Matthew Trumbell <matthew@thirdstonepartners.com> Matthias Bussonnier <bussonniermatthias@gmail.com> Maxime Rouyrre <rouyrre+git@gmail.com> +Michael <michael-k@users.noreply.github.com> Michael E. Karpeles <michael.karpeles@gmail.com> Michael Klich <michal@michalklich.com> Michael Williamson <mike@zwobble.org> @@ -240,6 +243,7 @@ Victor Stinner <victor.stinner@gmail.com> Ville Skyttä <ville.skytta@iki.fi> Vinay Sajip <vinay_sajip@yahoo.co.uk> Vitaly Babiy <vbabiy86@gmail.com> +Vladimir Rutsky <rutsky@users.noreply.github.com> W. Trevor King <wking@drexel.edu> Wil Tan <wil@dready.org> William ML Leslie <william.leslie.ttg@gmail.com> diff --git a/CHANGES.txt b/CHANGES.txt index f2522aecd674950763798f4e462a180e89fea171..e0583f758453386926048b049c8128076630d7fb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,24 @@ +**8.0.1 (2016-01-21)** + +* Detect CAPaths in addition to CAFiles on platforms that provide them. + +* Installing argparse or wsgiref will no longer warn or error - pip will allow + the installation even though it may be useless (since the installed thing + will be shadowed by the standard library). + +* Upgrading a distutils installed item that is installed outside of a virtual + environment, while inside of a virtual environment will no longer warn or + error. + +* Fix a bug where pre-releases were showing up in ``pip list --outdated`` + without the ``--pre`` flag. + +* Switch the SOABI emulation from using RuntimeWarnings to debug logging. + +* Rollback the removal of the ability to uninstall distutils installed items + until a future date. + + **8.0.0 (2016-01-19)** * **BACKWARD INCOMPATIBLE** Drop support for Python 3.2. diff --git a/docs/reference/pip_install.rst b/docs/reference/pip_install.rst index 20963375a514c4660fa1bc5049c681556094e1e5..27757637d34f24e4268d372fb7041dc3aaf031a2 100644 --- a/docs/reference/pip_install.rst +++ b/docs/reference/pip_install.rst @@ -235,7 +235,7 @@ Since version 6.0, pip also supports specifers containing `environment markers :: SomeProject ==5.4 ; python_version < '2.7' - SomeProject; sys.platform == 'win32' + SomeProject; sys_platform == 'win32' Environment markers are supported in the command line and in requirements files. diff --git a/pip/__init__.py b/pip/__init__.py index fd76fbb68fe84c77832b119dd6c48624af3fe658..09a21593f42e5371cc2b6288eb999f93d5970fe7 100755 --- a/pip/__init__.py +++ b/pip/__init__.py @@ -30,7 +30,7 @@ import pip.cmdoptions cmdoptions = pip.cmdoptions # The version as used in the setup.py and the docs conf.py -__version__ = "8.0.0" +__version__ = "8.0.1" logger = logging.getLogger(__name__) diff --git a/pip/commands/list.py b/pip/commands/list.py index 546965ec1320e3364b55c9a9e7d9be6034b3f96f..534648820bc8d6d700c995c0b0904219a3d60238 100644 --- a/pip/commands/list.py +++ b/pip/commands/list.py @@ -159,6 +159,11 @@ class ListCommand(Command): for dist in installed_packages: typ = 'unknown' all_candidates = finder.find_all_candidates(dist.key) + if not options.pre: + # Remove prereleases + all_candidates = [candidate for candidate in all_candidates + if not candidate.version.is_prerelease] + if not all_candidates: continue best_candidate = max(all_candidates, diff --git a/pip/compat/__init__.py b/pip/compat/__init__.py index 8ce5bdc23aa38cb78a2a01616164498fd492f6a0..3b01763b3efa3267e477b4c75ceee7853fd5e289 100644 --- a/pip/compat/__init__.py +++ b/pip/compat/__init__.py @@ -23,6 +23,26 @@ except ImportError: ipaddress.ip_network = ipaddress.IPNetwork +try: + import sysconfig + + def get_stdlib(): + paths = [ + sysconfig.get_path("stdlib"), + sysconfig.get_path("platstdlib"), + ] + return set(filter(bool, paths)) +except ImportError: + from distutils import sysconfig + + def get_stdlib(): + paths = [ + sysconfig.get_python_lib(standard_lib=True), + sysconfig.get_python_lib(standard_lib=True, plat_specific=True), + ] + return set(filter(bool, paths)) + + __all__ = [ "logging_dictConfig", "ipaddress", "uses_pycache", "console_to_str", "native_str", "get_path_uid", "stdlib_pkgs", "WINDOWS", "samefile" diff --git a/pip/locations.py b/pip/locations.py index 64f745aaa7b12d5a1c35a827ad2d906c9b32d778..3c35f0a127f88228df88765cf44c42cb6bfcbce5 100644 --- a/pip/locations.py +++ b/pip/locations.py @@ -19,9 +19,44 @@ from pip.utils import appdirs # doesn't exist or we cannot resolve the path to an existing file, then we will # simply set this to None. Setting this to None will have requests fall back # and use it's default CA Bundle logic. +# Ever since requests 2.9.0, requests has supported a CAPath in addition to a +# CAFile, because some systems (such as Debian) have a broken CAFile currently +# we'll go ahead and support both, prefering CAPath over CAfile. if getattr(ssl, "get_default_verify_paths", None): - CA_BUNDLE_PATH = ssl.get_default_verify_paths().cafile + _ssl_paths = ssl.get_default_verify_paths() + + # Ok, this is a little hairy because system trust stores are randomly + # broken in different and exciting ways and this should help fix that. + # Ideally we'd just not trust the system store, however that leads end + # users to be confused on systems like Debian that patch python-pip, even + # inside of a virtual environment, to support the system store. This would + # lead to pip trusting the system store when creating the virtual + # environment, but then switching to not doing that when upgraded to a + # version from PyPI. However, we can't *only* rely on the system store + # because not all systems actually have one, so at the end of the day we + # still need to fall back to trusting the bundled copy. + # + # Resolution Method: + # + # 1. We prefer a CAPath, however we will *only* prefer a CAPath if the + # directory exists and it is not empty. This works around systems like + # Homebrew which have an empty CAPath but a populated CAFile. + # 2. Failing that, we prefer a CAFile, however again we will *only* prefer + # it if it exists on disk and if it is not empty. This will work around + # systems that have an empty CAFile sitting around for no good reason. + # 3. Finally, we'll just fall back to letting requests use it's bundled + # CAFile, which can of course be overriden by the end user installing + # certifi. + if _ssl_paths.capath is not None and os.listdir(_ssl_paths.capath): + CA_BUNDLE_PATH = _ssl_paths.capath + elif _ssl_paths.cafile is not None and os.path.getsize(_ssl_paths.cafile): + CA_BUNDLE_PATH = _ssl_paths.cafile + else: + CA_BUNDLE_PATH = None else: + # If we aren't running on a copy of Python that is new enough to be able + # to query OpenSSL for it's default locations, then we'll only support + # using the built in CA Bundle by default. CA_BUNDLE_PATH = None diff --git a/pip/pep425tags.py b/pip/pep425tags.py index 0f67921ad717c294a8fbb53651036338f87cfc67..2d91ccc6d5fd022fd13cc7b11c8b1a95516b00d7 100644 --- a/pip/pep425tags.py +++ b/pip/pep425tags.py @@ -5,6 +5,7 @@ import re import sys import warnings import platform +import logging try: import sysconfig @@ -13,6 +14,10 @@ except ImportError: # pragma nocover import distutils.sysconfig as sysconfig import distutils.util + +logger = logging.getLogger(__name__) + + _osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') @@ -69,8 +74,8 @@ def get_flag(var, fallback, expected=True, warn=True): val = get_config_var(var) if val is None: if warn: - warnings.warn("Config variable '{0}' is unset, Python ABI tag may " - "be incorrect".format(var), RuntimeWarning, 2) + logger.debug("Config variable '%s' is unset, Python ABI tag may " + "be incorrect", var) return fallback() return val == expected @@ -116,8 +121,8 @@ def get_platform(): # of MACOSX_DEPLOYMENT_TARGET on which Python was built, which may # be signficantly older than the user's current machine. release, _, machine = platform.mac_ver() - major, minor, micro = release.split('.') - return 'macosx_{0}_{1}_{2}'.format(major, minor, machine) + split_ver = release.split('.') + return 'macosx_{0}_{1}_{2}'.format(split_ver[0], split_ver[1], machine) # XXX remove distutils dependency return distutils.util.get_platform().replace('.', '_').replace('-', '_') diff --git a/pip/req/req_install.py b/pip/req/req_install.py index 6f497cbc065770a447ec22d8c87116c790fabda4..3b48431cd6402fc8099f70757e92a082719352d3 100644 --- a/pip/req/req_install.py +++ b/pip/req/req_install.py @@ -7,10 +7,11 @@ import shutil import sys import tempfile import traceback +import warnings import zipfile -from distutils.util import change_root from distutils import sysconfig +from distutils.util import change_root from email.parser import FeedParser from pip._vendor import pkg_resources, six @@ -20,7 +21,7 @@ from pip._vendor.six.moves import configparser import pip.wheel -from pip.compat import native_str, WINDOWS +from pip.compat import native_str, get_stdlib, WINDOWS from pip.download import is_url, url_to_path, path_to_url, is_archive_file from pip.exceptions import ( InstallationError, UninstallationError, UnsupportedWheel, @@ -32,9 +33,11 @@ from pip.utils import ( display_path, rmtree, ask_path_exists, backup_dir, is_installable_dir, dist_in_usersite, dist_in_site_packages, egg_link_path, call_subprocess, read_text_file, FakeFile, _make_build_dir, ensure_dir, - get_installed_version, canonicalize_name + get_installed_version, canonicalize_name, normalize_path, dist_is_local, ) + from pip.utils.hashes import Hashes +from pip.utils.deprecation import RemovedInPip10Warning from pip.utils.logging import indent_log from pip.utils.setuptools_build import SETUPTOOLS_SHIM from pip.utils.ui import open_spinner @@ -114,6 +117,9 @@ class InstallRequirement(object): self.install_succeeded = None # UninstallPathSet of uninstalled distribution (for possible rollback) self.uninstalled = None + # Set True if a legitimate do-nothing-on-uninstall has happened - e.g. + # system site packages, stdlib packages. + self.nothing_to_uninstall = False self.use_user_site = False self.target_dir = None self.options = options if options else {} @@ -606,6 +612,26 @@ class InstallRequirement(object): ) dist = self.satisfied_by or self.conflicts_with + dist_path = normalize_path(dist.location) + if not dist_is_local(dist): + logger.info( + "Not uninstalling %s at %s, outside environment %s", + dist.key, + dist_path, + sys.prefix, + ) + self.nothing_to_uninstall = True + return + + if dist_path in get_stdlib(): + logger.info( + "Not uninstalling %s at %s, as it is in the standard library.", + dist.key, + dist_path, + ) + self.nothing_to_uninstall = True + return + paths_to_remove = UninstallPathSet(dist) develop_egg_link = egg_link_path(dist) develop_egg_link_egg_info = '{0}.egg-info'.format( @@ -647,12 +673,14 @@ class InstallRequirement(object): paths_to_remove.add(path + '.pyo') elif distutils_egg_info: - raise UninstallationError( - "Detected a distutils installed project ({0!r}) which we " - "cannot uninstall. The metadata provided by distutils does " - "not contain a list of files which have been installed, so " - "pip does not know which files to uninstall.".format(self.name) + warnings.warn( + "Uninstalling a distutils installed project ({0}) has been " + "deprecated and will be removed in a future version. This is " + "due to the fact that uninstalling a distutils project will " + "only partially uninstall the project.".format(self.name), + RemovedInPip10Warning, ) + paths_to_remove.add(distutils_egg_info) elif dist.location.endswith('.egg'): # package installed by easy_install @@ -729,15 +757,15 @@ class InstallRequirement(object): self.uninstalled.rollback() else: logger.error( - "Can't rollback %s, nothing uninstalled.", self.project_name, + "Can't rollback %s, nothing uninstalled.", self.name, ) def commit_uninstall(self): if self.uninstalled: self.uninstalled.commit() - else: + elif not self.nothing_to_uninstall: logger.error( - "Can't commit %s, nothing uninstalled.", self.project_name, + "Can't commit %s, nothing uninstalled.", self.name, ) def archive(self, build_dir): diff --git a/pip/req/req_uninstall.py b/pip/req/req_uninstall.py index c7bc042dbf8aeb42bf193909641ef6512e345023..5248430a9b17c102fcabe4f9ccbafd2a307f5701 100644 --- a/pip/req/req_uninstall.py +++ b/pip/req/req_uninstall.py @@ -2,13 +2,11 @@ from __future__ import absolute_import import logging import os -import sys import tempfile from pip.compat import uses_pycache, WINDOWS, cache_from_source from pip.exceptions import UninstallationError -from pip.utils import (rmtree, ask, is_local, dist_is_local, renames, - normalize_path) +from pip.utils import rmtree, ask, is_local, renames, normalize_path from pip.utils.logging import indent_log @@ -34,17 +32,6 @@ class UninstallPathSet(object): """ return is_local(path) - def _can_uninstall(self): - if not dist_is_local(self.dist): - logger.info( - "Not uninstalling %s at %s, outside environment %s", - self.dist.project_name, - normalize_path(self.dist.location), - sys.prefix, - ) - return False - return True - def add(self, path): head, tail = os.path.split(path) @@ -94,8 +81,6 @@ class UninstallPathSet(object): def remove(self, auto_confirm=False): """Remove paths in ``self.paths`` with confirmation (unless ``auto_confirm`` is True).""" - if not self._can_uninstall(): - return if not self.paths: logger.info( "Can't uninstall '%s'. No files were found to uninstall.", diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index ca218d1f540f18b8f65ef406cd801c0048921548..cdb8d5b20322d3031060a4fd2c948e2f6e6bfc11 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -305,6 +305,29 @@ def test_editable_install_from_local_directory_with_no_setup_py(script, data): assert "is not installable. File 'setup.py' not found." in result.stderr +@pytest.mark.skipif("sys.version_info < (2,7) or sys.version_info >= (3,4)") +@pytest.mark.xfail +def test_install_argparse_shadowed(script, data): + # When argparse is in the stdlib, we support installing it + # even though thats pretty useless because older packages did need to + # depend on it, and not having its metadata will cause pkg_resources + # requirements checks to fail // trigger easy-install, both of which are + # bad. + # XXX: Note, this test hits the outside-environment check, not the + # in-stdlib check, because our tests run in virtualenvs... + result = script.pip('install', 'argparse>=1.4') + assert "Not uninstalling argparse" in result.stdout + + +@pytest.mark.skipif("sys.version_info < (3,4)") +def test_upgrade_argparse_shadowed(script, data): + # If argparse is installed - even if shadowed for imported - we support + # upgrading it and properly remove the older versions files. + script.pip('install', 'argparse==1.3') + result = script.pip('install', 'argparse>=1.4') + assert "Not uninstalling argparse" not in result.stdout + + def test_install_as_egg(script, data): """ Test installing as egg, instead of flat install. diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index ad503cf1c2dcfa24760eabf770e4cbbacaef4909..8847e598d36ea4d4935f9e9218979f78eacee508 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -147,3 +147,22 @@ def test_outdated_editables_flag(script, data): assert os.path.join('src', 'pip-test-package') in result.stdout, ( str(result) ) + + +def test_outdated_pre(script, data): + script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0') + + # Let's build a fake wheelhouse + script.scratch_path.join("wheelhouse").mkdir() + wheelhouse_path = script.scratch_path / 'wheelhouse' + wheelhouse_path.join('simple-1.1-py2.py3-none-any.whl').write('') + wheelhouse_path.join('simple-2.0.dev0-py2.py3-none-any.whl').write('') + result = script.pip('list', '--no-index', '--find-links', wheelhouse_path) + assert 'simple (1.0)' in result.stdout + result = script.pip('list', '--no-index', '--find-links', wheelhouse_path, + '--outdated') + assert 'simple (1.0) - Latest: 1.1 [wheel]' in result.stdout + result_pre = script.pip('list', '--no-index', + '--find-links', wheelhouse_path, + '--outdated', '--pre') + assert 'simple (1.0) - Latest: 2.0.dev0 [wheel]' in result_pre.stdout diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index b8003f274bb30177ffd4bc16bcdf6f75eeb80618..d3e7c35048f3ca219ba5bb32499310e9fec40f31 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -4,12 +4,14 @@ import textwrap import os import sys import pytest +import pretend + from os.path import join, normpath from tempfile import mkdtemp -from mock import patch from tests.lib import assert_all_changes, pyversion from tests.lib.local_repos import local_repo, local_checkout +from pip.req import InstallRequirement from pip.utils import rmtree @@ -30,6 +32,28 @@ def test_simple_uninstall(script): assert_all_changes(result, result2, [script.venv / 'build', 'cache']) +def test_simple_uninstall_distutils(script): + """ + Test simple install and uninstall. + + """ + script.scratch_path.join("distutils_install").mkdir() + pkg_path = script.scratch_path / 'distutils_install' + pkg_path.join("setup.py").write(textwrap.dedent(""" + from distutils.core import setup + setup( + name='distutils-install', + version='0.1', + ) + """)) + result = script.run('python', pkg_path / 'setup.py', 'install') + result = script.pip('list') + assert "distutils-install (0.1)" in result.stdout + script.pip('uninstall', 'distutils_install', '-y', expect_stderr=True) + result2 = script.pip('list') + assert "distutils-install (0.1)" not in result2.stdout + + @pytest.mark.network def test_uninstall_with_scripts(script): """ @@ -317,11 +341,8 @@ def test_uninstallpathset_no_paths(caplog): from pip.req.req_uninstall import UninstallPathSet from pkg_resources import get_distribution test_dist = get_distribution('pip') - # ensure that the distribution is "local" - with patch("pip.req.req_uninstall.dist_is_local") as mock_dist_is_local: - mock_dist_is_local.return_value = True - uninstall_set = UninstallPathSet(test_dist) - uninstall_set.remove() # with no files added to set + uninstall_set = UninstallPathSet(test_dist) + uninstall_set.remove() # with no files added to set assert ( "Can't uninstall 'pip'. No files were found to uninstall." @@ -329,31 +350,25 @@ def test_uninstallpathset_no_paths(caplog): ) -def test_uninstallpathset_non_local(caplog): - """ - Test UninstallPathSet logs notification and returns (with no exception) - when dist is non-local - """ - nonlocal_path = os.path.abspath("/nonlocal") - from pip.req.req_uninstall import UninstallPathSet - from pkg_resources import get_distribution - test_dist = get_distribution('pip') - test_dist.location = nonlocal_path - # ensure that the distribution is "non-local" - # setting location isn't enough, due to egg-link file checking for - # develop-installs - with patch("pip.req.req_uninstall.dist_is_local") as mock_dist_is_local: - mock_dist_is_local.return_value = False - uninstall_set = UninstallPathSet(test_dist) - # with no files added to set; which is the case when trying to remove - # non-local dists - uninstall_set.remove() +def test_uninstall_non_local_distutils(caplog, monkeypatch, tmpdir): + einfo = tmpdir.join("thing-1.0.egg-info") + with open(einfo, "wb"): + pass - assert ( - "Not uninstalling pip at %s, outside environment %s" - % (nonlocal_path, sys.prefix) - in caplog.text() + dist = pretend.stub( + key="thing", + project_name="thing", + egg_info=einfo, + location=einfo, + _provider=pretend.stub(), ) + get_dist = pretend.call_recorder(lambda x: dist) + monkeypatch.setattr("pip._vendor.pkg_resources.get_distribution", get_dist) + + req = InstallRequirement.from_line("thing") + req.uninstall() + + assert os.path.exists(einfo) def test_uninstall_wheel(script, data):