diff --git a/news/9617.process.rst b/news/9617.process.rst new file mode 100644 index 00000000000..f505c460541 --- /dev/null +++ b/news/9617.process.rst @@ -0,0 +1,3 @@ +Start installation scheme migration from ``distutils`` to ``sysconfig``. A +warning is implemented to detect differences between the two implementations to +encourage user reports, so we can avoid breakages before they happen. diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index b1c877cfd9b..f4a1c93353a 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -6,7 +6,6 @@ import sys import textwrap from collections import OrderedDict -from distutils.sysconfig import get_python_lib from sysconfig import get_paths from types import TracebackType from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type @@ -15,6 +14,7 @@ from pip import __file__ as pip_location from pip._internal.cli.spinners import open_spinner +from pip._internal.locations import get_platlib, get_prefixed_libs, get_purelib from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds @@ -34,14 +34,7 @@ def __init__(self, path): 'nt' if os.name == 'nt' else 'posix_prefix', vars={'base': path, 'platbase': path} )['scripts'] - # Note: prefer distutils' sysconfig to get the - # library paths so PyPy is correctly supported. - purelib = get_python_lib(plat_specific=False, prefix=path) - platlib = get_python_lib(plat_specific=True, prefix=path) - if purelib == platlib: - self.lib_dirs = [purelib] - else: - self.lib_dirs = [purelib, platlib] + self.lib_dirs = get_prefixed_libs(path) class BuildEnvironment: @@ -69,10 +62,7 @@ def __init__(self): # - ensure .pth files are honored # - prevent access to system site packages system_sites = { - os.path.normcase(site) for site in ( - get_python_lib(plat_specific=False), - get_python_lib(plat_specific=True), - ) + os.path.normcase(site) for site in (get_purelib(), get_platlib()) } self._site_dir = os.path.join(temp_dir.path, 'site') if not os.path.exists(self._site_dir): diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 78cd0b5cf68..dc743ee0b50 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -15,7 +15,7 @@ from pip._internal.cli.req_command import RequirementCommand, with_cleanup from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import CommandError, InstallationError -from pip._internal.locations import distutils_scheme +from pip._internal.locations import get_scheme from pip._internal.metadata import get_environment from pip._internal.models.format_control import FormatControl from pip._internal.operations.check import ConflictDetails, check_install_conflicts @@ -455,10 +455,10 @@ def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): # Checking both purelib and platlib directories for installed # packages to be moved to target directory - scheme = distutils_scheme('', home=target_temp_dir.path) - purelib_dir = scheme['purelib'] - platlib_dir = scheme['platlib'] - data_dir = scheme['data'] + scheme = get_scheme('', home=target_temp_dir.path) + purelib_dir = scheme.purelib + platlib_dir = scheme.platlib + data_dir = scheme.data if os.path.exists(purelib_dir): lib_dir_list.append(purelib_dir) @@ -574,9 +574,15 @@ def get_lib_location_guesses( prefix=None # type: Optional[str] ): # type:(...) -> List[str] - scheme = distutils_scheme('', user=user, home=home, root=root, - isolated=isolated, prefix=prefix) - return [scheme['purelib'], scheme['platlib']] + scheme = get_scheme( + '', + user=user, + home=home, + root=root, + isolated=isolated, + prefix=prefix, + ) + return [scheme.purelib, scheme.platlib] def site_packages_writable(root, isolated): diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 01ee4b76984..8aacf812014 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -59,6 +59,21 @@ def __str__(self): ) +class UserInstallationInvalid(InstallationError): + """A --user install is requested on an environment without user site.""" + + def __str__(self): + # type: () -> str + return "User base directory is not specified" + + +class InvalidSchemeCombination(InstallationError): + def __str__(self): + # type: () -> str + before = ", ".join(str(a) for a in self.args[:-1]) + return f"Cannot set {before} and {self.args[-1]} together" + + class DistributionNotFound(InstallationError): """Raised when a distribution cannot be found to satisfy a requirement""" diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py new file mode 100644 index 00000000000..18bf0319f3d --- /dev/null +++ b/src/pip/_internal/locations/__init__.py @@ -0,0 +1,184 @@ +import logging +import pathlib +import sys +import sysconfig +from typing import List, Optional + +from pip._internal.models.scheme import SCHEME_KEYS, Scheme + +from . import _distutils, _sysconfig +from .base import ( + USER_CACHE_DIR, + get_major_minor_version, + get_src_prefix, + site_packages, + user_site, +) + +__all__ = [ + "USER_CACHE_DIR", + "get_bin_prefix", + "get_bin_user", + "get_major_minor_version", + "get_platlib", + "get_prefixed_libs", + "get_purelib", + "get_scheme", + "get_src_prefix", + "site_packages", + "user_site", +] + + +logger = logging.getLogger(__name__) + + +def _default_base(*, user: bool) -> str: + if user: + base = sysconfig.get_config_var("userbase") + else: + base = sysconfig.get_config_var("base") + assert base is not None + return base + + +def _warn_if_mismatch(old: pathlib.Path, new: pathlib.Path, *, key: str) -> bool: + if old == new: + return False + issue_url = "https://github.com/pypa/pip/issues/9617" + message = ( + "Value for %s does not match. Please report this to <%s>" + "\ndistutils: %s" + "\nsysconfig: %s" + ) + logger.warning(message, key, issue_url, old, new) + return True + + +def _log_context( + *, + user: bool = False, + home: Optional[str] = None, + root: Optional[str] = None, + prefix: Optional[str] = None, +) -> None: + message = ( + "Additional context:" "\nuser = %r" "\nhome = %r" "\nroot = %r" "\nprefix = %r" + ) + logger.warning(message, user, home, root, prefix) + + +def get_scheme( + dist_name, # type: str + user=False, # type: bool + home=None, # type: Optional[str] + root=None, # type: Optional[str] + isolated=False, # type: bool + prefix=None, # type: Optional[str] +): + # type: (...) -> Scheme + old = _distutils.get_scheme( + dist_name, + user=user, + home=home, + root=root, + isolated=isolated, + prefix=prefix, + ) + new = _sysconfig.get_scheme( + dist_name, + user=user, + home=home, + root=root, + isolated=isolated, + prefix=prefix, + ) + + base = prefix or home or _default_base(user=user) + warned = [] + for k in SCHEME_KEYS: + # Extra join because distutils can return relative paths. + old_v = pathlib.Path(base, getattr(old, k)) + new_v = pathlib.Path(getattr(new, k)) + + # distutils incorrectly put PyPy packages under ``site-packages/python`` + # in the ``posix_home`` scheme, but PyPy devs said they expect the + # directory name to be ``pypy`` instead. So we treat this as a bug fix + # and not warn about it. See bpo-43307 and python/cpython#24628. + skip_pypy_special_case = ( + sys.implementation.name == "pypy" + and home is not None + and k in ("platlib", "purelib") + and old_v.parent == new_v.parent + and old_v.name == "python" + and new_v.name == "pypy" + ) + if skip_pypy_special_case: + continue + + warned.append(_warn_if_mismatch(old_v, new_v, key=f"scheme.{k}")) + + if any(warned): + _log_context(user=user, home=home, root=root, prefix=prefix) + + return old + + +def get_bin_prefix(): + # type: () -> str + old = _distutils.get_bin_prefix() + new = _sysconfig.get_bin_prefix() + if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"): + _log_context() + return old + + +def get_bin_user(): + # type: () -> str + return _sysconfig.get_scheme("", user=True).scripts + + +def get_purelib(): + # type: () -> str + """Return the default pure-Python lib location.""" + old = _distutils.get_purelib() + new = _sysconfig.get_purelib() + if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib"): + _log_context() + return old + + +def get_platlib(): + # type: () -> str + """Return the default platform-shared lib location.""" + old = _distutils.get_platlib() + new = _sysconfig.get_platlib() + if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"): + _log_context() + return old + + +def get_prefixed_libs(prefix): + # type: (str) -> List[str] + """Return the lib locations under ``prefix``.""" + old_pure, old_plat = _distutils.get_prefixed_libs(prefix) + new_pure, new_plat = _sysconfig.get_prefixed_libs(prefix) + + warned = [ + _warn_if_mismatch( + pathlib.Path(old_pure), + pathlib.Path(new_pure), + key="prefixed-purelib", + ), + _warn_if_mismatch( + pathlib.Path(old_plat), + pathlib.Path(new_plat), + key="prefixed-platlib", + ), + ] + if any(warned): + _log_context(prefix=prefix) + + if old_pure == old_plat: + return [old_pure] + return [old_pure, old_plat] diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations/_distutils.py similarity index 58% rename from src/pip/_internal/locations.py rename to src/pip/_internal/locations/_distutils.py index 35a27a3b903..2d7ab73213c 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations/_distutils.py @@ -4,92 +4,21 @@ # mypy: strict-optional=False import os -import os.path -import site import sys -import sysconfig from distutils.cmd import Command as DistutilsCommand from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command -from typing import Dict, List, Optional, Union, cast +from distutils.sysconfig import get_python_lib +from typing import Dict, List, Optional, Tuple, Union, cast from pip._internal.models.scheme import Scheme -from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv -# Application Directories -USER_CACHE_DIR = appdirs.user_cache_dir("pip") +from .base import get_major_minor_version -def get_major_minor_version(): - # type: () -> str - """ - Return the major-minor version of the current Python as a string, e.g. - "3.7" or "3.10". - """ - return '{}.{}'.format(*sys.version_info) - - -def get_src_prefix(): - # type: () -> str - if running_under_virtualenv(): - src_prefix = os.path.join(sys.prefix, 'src') - else: - # FIXME: keep src in cwd for now (it is not a temporary folder) - try: - src_prefix = os.path.join(os.getcwd(), 'src') - except OSError: - # In case the current working directory has been renamed or deleted - sys.exit( - "The folder you are executing pip from can no longer be found." - ) - - # under macOS + virtualenv sys.prefix is not properly resolved - # it is something like /path/to/python/bin/.. - return os.path.abspath(src_prefix) - - -# FIXME doesn't account for venv linked to global site-packages - -site_packages = sysconfig.get_path("purelib") # type: Optional[str] - -try: - # Use getusersitepackages if this is present, as it ensures that the - # value is initialised properly. - user_site = site.getusersitepackages() -except AttributeError: - user_site = site.USER_SITE - - -def _get_bin_user(): - # type: () -> str - scheme = "{}_user".format(os.name) - if scheme not in sysconfig.get_scheme_names(): - scheme = "posix_user" # Default to POSIX for unknown platforms. - path = sysconfig.get_path("scripts", scheme=scheme) - assert path is not None - return path - - -bin_user = _get_bin_user() - -if WINDOWS: - bin_py = os.path.join(sys.prefix, 'Scripts') - # buildout uses 'bin' on Windows too? - if not os.path.exists(bin_py): - bin_py = os.path.join(sys.prefix, 'bin') - bin_user = os.path.join(os.path.dirname(bin_user), 'bin') -else: - bin_py = os.path.join(sys.prefix, 'bin') - - # Forcing to use /usr/local/bin for standard macOS framework installs - # Also log to ~/Library/Logs/ for use with the Console.app log viewer - if sys.platform[:6] == 'darwin' and sys.prefix[:16] == '/System/Library/': - bin_py = '/usr/local/bin' - - -def distutils_scheme( +def _distutils_scheme( dist_name, user=False, home=None, root=None, isolated=False, prefix=None ): # type:(str, bool, str, str, bool, str) -> Dict[str, str] @@ -98,14 +27,14 @@ def distutils_scheme( """ from distutils.dist import Distribution - dist_args = {'name': dist_name} # type: Dict[str, Union[str, List[str]]] + dist_args = {"name": dist_name} # type: Dict[str, Union[str, List[str]]] if isolated: dist_args["script_args"] = ["--no-user-cfg"] d = Distribution(dist_args) d.parse_config_files() obj = None # type: Optional[DistutilsCommand] - obj = d.get_command_obj('install', create=True) + obj = d.get_command_obj("install", create=True) assert obj is not None i = cast(distutils_install_command, obj) # NOTE: setting user or home has the side-effect of creating the home dir @@ -123,28 +52,27 @@ def distutils_scheme( scheme = {} for key in SCHEME_KEYS: - scheme[key] = getattr(i, 'install_' + key) + scheme[key] = getattr(i, "install_" + key) # install_lib specified in setup.cfg should install *everything* # into there (i.e. it takes precedence over both purelib and # platlib). Note, i.install_lib is *always* set after # finalize_options(); we only want to override here if the user # has explicitly requested it hence going back to the config - if 'install_lib' in d.get_option_dict('install'): + if "install_lib" in d.get_option_dict("install"): scheme.update(dict(purelib=i.install_lib, platlib=i.install_lib)) if running_under_virtualenv(): - scheme['headers'] = os.path.join( + scheme["headers"] = os.path.join( i.prefix, - 'include', - 'site', - f'python{get_major_minor_version()}', + "include", + "site", + f"python{get_major_minor_version()}", dist_name, ) if root is not None: - path_no_drive = os.path.splitdrive( - os.path.abspath(scheme["headers"]))[1] + path_no_drive = os.path.splitdrive(os.path.abspath(scheme["headers"]))[1] scheme["headers"] = os.path.join( root, path_no_drive[1:], @@ -179,9 +107,7 @@ def get_scheme( :param prefix: indicates to use the "prefix" scheme and provides the base directory for the same """ - scheme = distutils_scheme( - dist_name, user, home, root, isolated, prefix - ) + scheme = _distutils_scheme(dist_name, user, home, root, isolated, prefix) return Scheme( platlib=scheme["platlib"], purelib=scheme["purelib"], @@ -189,3 +115,36 @@ def get_scheme( scripts=scheme["scripts"], data=scheme["data"], ) + + +def get_bin_prefix(): + # type: () -> str + if WINDOWS: + bin_py = os.path.join(sys.prefix, "Scripts") + # buildout uses 'bin' on Windows too? + if not os.path.exists(bin_py): + bin_py = os.path.join(sys.prefix, "bin") + return bin_py + # Forcing to use /usr/local/bin for standard macOS framework installs + # Also log to ~/Library/Logs/ for use with the Console.app log viewer + if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/": + return "/usr/local/bin" + return os.path.join(sys.prefix, "bin") + + +def get_purelib(): + # type: () -> str + return get_python_lib(plat_specific=False) + + +def get_platlib(): + # type: () -> str + return get_python_lib(plat_specific=True) + + +def get_prefixed_libs(prefix): + # type: (str) -> Tuple[str, str] + return ( + get_python_lib(plat_specific=False, prefix=prefix), + get_python_lib(plat_specific=True, prefix=prefix), + ) diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py new file mode 100644 index 00000000000..93e8d40b125 --- /dev/null +++ b/src/pip/_internal/locations/_sysconfig.py @@ -0,0 +1,181 @@ +import distutils.util # FIXME: For change_root. +import logging +import os +import sys +import sysconfig +import typing + +from pip._internal.exceptions import InvalidSchemeCombination, UserInstallationInvalid +from pip._internal.models.scheme import SCHEME_KEYS, Scheme +from pip._internal.utils.virtualenv import running_under_virtualenv + +from .base import get_major_minor_version + +logger = logging.getLogger(__name__) + + +# Notes on _infer_* functions. +# Unfortunately ``_get_default_scheme()`` is private, so there's no way to +# ask things like "what is the '_prefix' scheme on this platform". These +# functions try to answer that with some heuristics while accounting for ad-hoc +# platforms not covered by CPython's default sysconfig implementation. If the +# ad-hoc implementation does not fully implement sysconfig, we'll fall back to +# a POSIX scheme. + +_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names()) + + +def _infer_prefix(): + # type: () -> str + """Try to find a prefix scheme for the current platform. + + This tries: + + * Implementation + OS, used by PyPy on Windows (``pypy_nt``). + * Implementation without OS, used by PyPy on POSIX (``pypy``). + * OS + "prefix", used by CPython on POSIX (``posix_prefix``). + * Just the OS name, used by CPython on Windows (``nt``). + + If none of the above works, fall back to ``posix_prefix``. + """ + implementation_suffixed = f"{sys.implementation.name}_{os.name}" + if implementation_suffixed in _AVAILABLE_SCHEMES: + return implementation_suffixed + if sys.implementation.name in _AVAILABLE_SCHEMES: + return sys.implementation.name + suffixed = f"{os.name}_prefix" + if suffixed in _AVAILABLE_SCHEMES: + return suffixed + if os.name in _AVAILABLE_SCHEMES: # On Windows, prefx is just called "nt". + return os.name + return "posix_prefix" + + +def _infer_user(): + # type: () -> str + """Try to find a user scheme for the current platform.""" + suffixed = f"{os.name}_user" + if suffixed in _AVAILABLE_SCHEMES: + return suffixed + if "posix_user" not in _AVAILABLE_SCHEMES: # User scheme unavailable. + raise UserInstallationInvalid() + return "posix_user" + + +def _infer_home(): + # type: () -> str + """Try to find a home for the current platform.""" + suffixed = f"{os.name}_home" + if suffixed in _AVAILABLE_SCHEMES: + return suffixed + return "posix_home" + + +# Update these keys if the user sets a custom home. +_HOME_KEYS = [ + "installed_base", + "base", + "installed_platbase", + "platbase", + "prefix", + "exec_prefix", +] +if sysconfig.get_config_var("userbase") is not None: + _HOME_KEYS.append("userbase") + + +def get_scheme( + dist_name, # type: str + user=False, # type: bool + home=None, # type: typing.Optional[str] + root=None, # type: typing.Optional[str] + isolated=False, # type: bool + prefix=None, # type: typing.Optional[str] +): + # type: (...) -> Scheme + """ + Get the "scheme" corresponding to the input parameters. + + :param dist_name: the name of the package to retrieve the scheme for, used + in the headers scheme path + :param user: indicates to use the "user" scheme + :param home: indicates to use the "home" scheme + :param root: root under which other directories are re-based + :param isolated: ignored, but kept for distutils compatibility (where + this controls whether the user-site pydistutils.cfg is honored) + :param prefix: indicates to use the "prefix" scheme and provides the + base directory for the same + """ + if user and prefix: + raise InvalidSchemeCombination("--user", "--prefix") + if home and prefix: + raise InvalidSchemeCombination("--home", "--prefix") + + if home is not None: + scheme_name = _infer_home() + elif user: + scheme_name = _infer_user() + else: + scheme_name = _infer_prefix() + + if home is not None: + variables = {k: home for k in _HOME_KEYS} + elif prefix is not None: + variables = {k: prefix for k in _HOME_KEYS} + else: + variables = {} + + paths = sysconfig.get_paths(scheme=scheme_name, vars=variables) + + # Pip historically uses a special header path in virtual environments. + if running_under_virtualenv(): + if user: + base = variables.get("userbase", sys.prefix) + else: + base = variables.get("base", sys.prefix) + python_xy = f"python{get_major_minor_version()}" + paths["include"] = os.path.join(base, "include", "site", python_xy) + + # Special user scripts path on Windows for compatibility to distutils. + # See ``distutils.commands.install.INSTALL_SCHEMES["nt_user"]["scripts"]``. + if scheme_name == "nt_user": + base = variables.get("userbase", sys.prefix) + python_xy = f"Python{sys.version_info.major}{sys.version_info.minor}" + paths["scripts"] = os.path.join(base, python_xy, "Scripts") + + scheme = Scheme( + platlib=paths["platlib"], + purelib=paths["purelib"], + headers=os.path.join(paths["include"], dist_name), + scripts=paths["scripts"], + data=paths["data"], + ) + if root is not None: + for key in SCHEME_KEYS: + value = distutils.util.change_root(root, getattr(scheme, key)) + setattr(scheme, key, value) + return scheme + + +def get_bin_prefix(): + # type: () -> str + # Forcing to use /usr/local/bin for standard macOS framework installs. + if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/": + return "/usr/local/bin" + return sysconfig.get_paths()["scripts"] + + +def get_purelib(): + # type: () -> str + return sysconfig.get_paths()["purelib"] + + +def get_platlib(): + # type: () -> str + return sysconfig.get_paths()["platlib"] + + +def get_prefixed_libs(prefix): + # type: (str) -> typing.Tuple[str, str] + paths = sysconfig.get_paths(vars={"base": prefix, "platbase": prefix}) + return (paths["purelib"], paths["platlib"]) diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py new file mode 100644 index 00000000000..98557abbe63 --- /dev/null +++ b/src/pip/_internal/locations/base.py @@ -0,0 +1,48 @@ +import os +import site +import sys +import sysconfig +import typing + +from pip._internal.utils import appdirs +from pip._internal.utils.virtualenv import running_under_virtualenv + +# Application Directories +USER_CACHE_DIR = appdirs.user_cache_dir("pip") + +# FIXME doesn't account for venv linked to global site-packages +site_packages = sysconfig.get_path("purelib") # type: typing.Optional[str] + + +def get_major_minor_version(): + # type: () -> str + """ + Return the major-minor version of the current Python as a string, e.g. + "3.7" or "3.10". + """ + return "{}.{}".format(*sys.version_info) + + +def get_src_prefix(): + # type: () -> str + if running_under_virtualenv(): + src_prefix = os.path.join(sys.prefix, "src") + else: + # FIXME: keep src in cwd for now (it is not a temporary folder) + try: + src_prefix = os.path.join(os.getcwd(), "src") + except OSError: + # In case the current working directory has been renamed or deleted + sys.exit("The folder you are executing pip from can no longer be found.") + + # under macOS + virtualenv sys.prefix is not properly resolved + # it is something like /path/to/python/bin/.. + return os.path.abspath(src_prefix) + + +try: + # Use getusersitepackages if this is present, as it ensures that the + # value is initialised properly. + user_site = site.getusersitepackages() # type: typing.Optional[str] +except AttributeError: + user_site = site.USER_SITE diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 64890e33912..b72234175b2 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -11,7 +11,7 @@ from pip._vendor.pkg_resources import Distribution from pip._internal.exceptions import UninstallationError -from pip._internal.locations import bin_py, bin_user +from pip._internal.locations import get_bin_prefix, get_bin_user from pip._internal.utils.compat import WINDOWS from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( @@ -36,9 +36,9 @@ def _script_names(dist, script_name, is_gui): Returns the list of file names """ if dist_in_usersite(dist): - bin_dir = bin_user + bin_dir = get_bin_user() else: - bin_dir = bin_py + bin_dir = get_bin_prefix() exe_name = os.path.join(bin_dir, script_name) paths_to_remove = [exe_name] if WINDOWS: @@ -551,9 +551,9 @@ def from_dist(cls, dist): if dist.has_metadata('scripts') and dist.metadata_isdir('scripts'): for script in dist.metadata_listdir('scripts'): if dist_in_usersite(dist): - bin_dir = bin_user + bin_dir = get_bin_user() else: - bin_dir = bin_py + bin_dir = get_bin_prefix() paths_to_remove.add(os.path.join(bin_dir, script)) if WINDOWS: paths_to_remove.add(os.path.join(bin_dir, script) + '.bat') diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py index 3d4ec946245..067f4e84486 100644 --- a/tests/unit/test_locations.py +++ b/tests/unit/test_locations.py @@ -11,7 +11,7 @@ import pytest -from pip._internal.locations import distutils_scheme +from pip._internal.locations import SCHEME_KEYS, get_scheme if sys.platform == 'win32': pwd = Mock() @@ -19,6 +19,11 @@ import pwd +def _get_scheme_dict(*args, **kwargs): + scheme = get_scheme(*args, **kwargs) + return {k: getattr(scheme, k) for k in SCHEME_KEYS} + + class TestLocations: def setup(self): self.tempdir = tempfile.mkdtemp() @@ -83,8 +88,8 @@ def test_root_modifies_appropriately(self, monkeypatch): # root is c:\somewhere\else or /somewhere/else root = os.path.normcase(os.path.abspath( os.path.join(os.path.sep, 'somewhere', 'else'))) - norm_scheme = distutils_scheme("example") - root_scheme = distutils_scheme("example", root=root) + norm_scheme = _get_scheme_dict("example") + root_scheme = _get_scheme_dict("example", root=root) for key, value in norm_scheme.items(): drive, path = os.path.splitdrive(os.path.abspath(value)) @@ -107,7 +112,7 @@ def test_distutils_config_file_read(self, tmpdir, monkeypatch): 'find_config_files', lambda self: [f], ) - scheme = distutils_scheme('example') + scheme = _get_scheme_dict('example') assert scheme['scripts'] == install_scripts @pytest.mark.incompatible_with_venv @@ -129,15 +134,15 @@ def test_install_lib_takes_precedence(self, tmpdir, monkeypatch): 'find_config_files', lambda self: [f], ) - scheme = distutils_scheme('example') + scheme = _get_scheme_dict('example') assert scheme['platlib'] == install_lib + os.path.sep assert scheme['purelib'] == install_lib + os.path.sep def test_prefix_modifies_appropriately(self): prefix = os.path.abspath(os.path.join('somewhere', 'else')) - normal_scheme = distutils_scheme("example") - prefix_scheme = distutils_scheme("example", prefix=prefix) + normal_scheme = _get_scheme_dict("example") + prefix_scheme = _get_scheme_dict("example", prefix=prefix) def _calculate_expected(value): path = os.path.join(prefix, os.path.relpath(value, sys.prefix))