From 738e6005067618c339e637ad78a839c97ad771ca Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Thu, 18 Feb 2021 19:27:30 +0800 Subject: [PATCH 01/17] Move distuitls location logic into subpackage --- src/pip/_internal/commands/install.py | 22 ++++--- src/pip/_internal/locations/__init__.py | 53 +++++++++++++++++ .../{locations.py => locations/_distutils.py} | 59 ++++--------------- src/pip/_internal/locations/base.py | 51 ++++++++++++++++ src/pip/_internal/locations/sysconfig.py | 0 src/pip/_internal/req/req_uninstall.py | 10 ++-- 6 files changed, 136 insertions(+), 59 deletions(-) create mode 100644 src/pip/_internal/locations/__init__.py rename src/pip/_internal/{locations.py => locations/_distutils.py} (77%) create mode 100644 src/pip/_internal/locations/base.py create mode 100644 src/pip/_internal/locations/sysconfig.py 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/locations/__init__.py b/src/pip/_internal/locations/__init__.py new file mode 100644 index 00000000000..9285165d7ee --- /dev/null +++ b/src/pip/_internal/locations/__init__.py @@ -0,0 +1,53 @@ +from typing import Optional + +from pip._internal.models.scheme import Scheme + +from . import distutils as _distutils +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_scheme", + "get_src_prefix", + "init_backend", + "site_packages", + "user_site", +] + + +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 + return _distutils.get_scheme( + dist_name, + user=user, + home=home, + root=root, + isolated=isolated, + prefix=prefix, + ) + + +def get_bin_prefix(): + # type: () -> str + return _distutils.get_bin_prefix() + + +def get_bin_user(): + # type: () -> str + return _distutils.get_bin_user() diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations/_distutils.py similarity index 77% rename from src/pip/_internal/locations.py rename to src/pip/_internal/locations/_distutils.py index 19c039eabf8..efc12e8da29 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations/_distutils.py @@ -5,62 +5,19 @@ 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 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, user_site -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 - if WINDOWS: bin_py = os.path.join(sys.prefix, 'Scripts') bin_user = os.path.join(user_site, 'Scripts') @@ -78,7 +35,7 @@ def get_src_prefix(): 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] @@ -168,7 +125,7 @@ def get_scheme( :param prefix: indicates to use the "prefix" scheme and provides the base directory for the same """ - scheme = distutils_scheme( + scheme = _distutils_scheme( dist_name, user, home, root, isolated, prefix ) return Scheme( @@ -178,3 +135,13 @@ def get_scheme( scripts=scheme["scripts"], data=scheme["data"], ) + + +def get_bin_prefix(): + # type: () -> str + return bin_py + + +def get_bin_user(): + # type: () -> str + return bin_user diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py new file mode 100644 index 00000000000..8c9f7751179 --- /dev/null +++ b/src/pip/_internal/locations/base.py @@ -0,0 +1,51 @@ +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() +except AttributeError: + user_site = site.USER_SITE diff --git a/src/pip/_internal/locations/sysconfig.py b/src/pip/_internal/locations/sysconfig.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 519b79166a4..776652de1cc 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') From 7662c5961e5d2fe8a9cbceb48206b3f7daaec169 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Thu, 18 Feb 2021 20:56:45 +0800 Subject: [PATCH 02/17] Implement sysconfig locations and warn on mismatch --- src/pip/_internal/exceptions.py | 15 +++ src/pip/_internal/locations/__init__.py | 61 +++++++++- src/pip/_internal/locations/_sysconfig.py | 137 ++++++++++++++++++++++ src/pip/_internal/locations/sysconfig.py | 0 tests/unit/test_locations.py | 19 +-- 5 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 src/pip/_internal/locations/_sysconfig.py delete mode 100644 src/pip/_internal/locations/sysconfig.py 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 index 9285165d7ee..6fc4d53e15d 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -1,8 +1,11 @@ +import logging +import pathlib +import sysconfig from typing import Optional -from pip._internal.models.scheme import Scheme +from pip._internal.models.scheme import SCHEME_KEYS, Scheme -from . import distutils as _distutils +from . import _distutils, _sysconfig from .base import ( USER_CACHE_DIR, get_major_minor_version, @@ -24,6 +27,31 @@ ] +logger = logging.getLogger(__name__) + + +def _default_base(*, user): + # type: (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, new, *, key): + # type: (pathlib.Path, pathlib.Path, str) -> None + if old == new: + return + message = ( + "Value for %s does not match. Please report this: " + "\ndistutils: %s" + "\nsysconfig: %s" + ) + logger.warning(message, key, old, new) + + def get_scheme( dist_name, # type: str user=False, # type: bool @@ -33,7 +61,15 @@ def get_scheme( prefix=None, # type: Optional[str] ): # type: (...) -> Scheme - return _distutils.get_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, @@ -42,12 +78,27 @@ def get_scheme( prefix=prefix, ) + base = prefix or home or _default_base(user=user) + 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)) + _warn_if_mismatch(old_v, new_v, key=f"scheme.{k}") + + return old + def get_bin_prefix(): # type: () -> str - return _distutils.get_bin_prefix() + old = _distutils.get_bin_prefix() + new = _sysconfig.get_bin_prefix() + _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix") + return old def get_bin_user(): # type: () -> str - return _distutils.get_bin_user() + old = _distutils.get_bin_user() + new = _sysconfig.get_bin_user() + _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_user") + return old diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py new file mode 100644 index 00000000000..b9687870466 --- /dev/null +++ b/src/pip/_internal/locations/_sysconfig.py @@ -0,0 +1,137 @@ +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__) + + +_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names()) + + +def _infer_scheme(variant): + # (typing.Literal["home", "prefix", "user"]) -> str + """Try to find a scheme for the current platform. + + Unfortunately ``_get_default_scheme()`` is private, so there's no way to + ask things like "what is the '_home' scheme on this platform". This tries + to answer that with some heuristics while accounting for ad-hoc platforms + not covered by CPython's default sysconfig implementation. + """ + # Most schemes are named like this e.g. "posix_home", "nt_user". + suffixed = f"{os.name}_{variant}" + if suffixed in _AVAILABLE_SCHEMES: + return suffixed + + # The user scheme is not available. + if variant == "user" and sysconfig.get_config_var("userbase") is None: + raise UserInstallationInvalid() + + # On Windows, prefx and home schemes are the same and just called "nt". + if os.name in _AVAILABLE_SCHEMES: + return os.name + + # Not sure what's happening, some obscure platform that does not fully + # implement sysconfig? Just use the POSIX scheme. + logger.warning("No %r scheme for %r; fallback to POSIX.", variant, os.name) + return f"posix_{variant}" + + +# 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 += ("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 = _infer_scheme("home") + elif user: + scheme = _infer_scheme("user") + else: + scheme = _infer_scheme("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, vars=variables) + + # Special header path for compatibility to distutils. + if running_under_virtualenv(): + base = variables.get("base", sys.prefix) + python_xy = f"python{get_major_minor_version()}" + paths["include"] = os.path.join(base, "include", "site", python_xy) + + 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_path("scripts", scheme=_infer_scheme("prefix")) + + +def get_bin_user(): + return sysconfig.get_path("scripts", scheme=_infer_scheme("user")) diff --git a/src/pip/_internal/locations/sysconfig.py b/src/pip/_internal/locations/sysconfig.py deleted file mode 100644 index e69de29bb2d..00000000000 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)) From edbda257c6b19cbabd5a3f3328427a68f10fc780 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Fri, 19 Feb 2021 17:14:11 +0800 Subject: [PATCH 03/17] Abstract out get_python_lib() usages --- src/pip/_internal/build_env.py | 16 ++--------- src/pip/_internal/locations/__init__.py | 35 ++++++++++++++++++++++- src/pip/_internal/locations/_distutils.py | 21 +++++++++++++- src/pip/_internal/locations/_sysconfig.py | 16 +++++++++++ 4 files changed, 73 insertions(+), 15 deletions(-) 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/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 6fc4d53e15d..f57bb9a8b07 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -1,7 +1,7 @@ import logging import pathlib import sysconfig -from typing import Optional +from typing import List, Optional from pip._internal.models.scheme import SCHEME_KEYS, Scheme @@ -19,6 +19,9 @@ "get_bin_prefix", "get_bin_user", "get_major_minor_version", + "get_platlib", + "get_prefixed_libs", + "get_purelib", "get_scheme", "get_src_prefix", "init_backend", @@ -102,3 +105,33 @@ def get_bin_user(): new = _sysconfig.get_bin_user() _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_user") return old + + +def get_purelib(): + # type: () -> str + """Return the default pure-Python lib location.""" + old = _distutils.get_purelib() + new = _sysconfig.get_purelib() + _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib") + return old + + +def get_platlib(): + # type: () -> str + """Return the default platform-shared lib location.""" + old = _distutils.get_platlib() + new = _sysconfig.get_platlib() + _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib") + 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) + _warn_if_mismatch(old_pure, new_pure, key="prefixed-purelib") + _warn_if_mismatch(old_plat, new_plat, key="prefixed-platlib") + if old_pure == old_plat: + return [old_pure] + return [old_pure, old_plat] diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index efc12e8da29..bf3ff728f6d 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -9,7 +9,8 @@ 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.compat import WINDOWS @@ -145,3 +146,21 @@ def get_bin_prefix(): def get_bin_user(): # type: () -> str return bin_user + + +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 index b9687870466..bc49cb75f77 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -135,3 +135,19 @@ def get_bin_prefix(): def get_bin_user(): return sysconfig.get_path("scripts", scheme=_infer_scheme("user")) + + +def get_purelib(): + # type: () -> str + return sysconfig.get_path("purelib") + + +def get_platlib(): + # type: () -> str + return sysconfig.get_path("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"]) From 686734fdd1e3c6e280b84157e9abc3da41357bfb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Fri, 19 Feb 2021 17:53:27 +0800 Subject: [PATCH 04/17] Provide issue URL to report --- src/pip/_internal/locations/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index f57bb9a8b07..db1c1309ee4 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -47,12 +47,13 @@ def _warn_if_mismatch(old, new, *, key): # type: (pathlib.Path, pathlib.Path, str) -> None if old == new: return + issue_url = "https://github.com/pypa/pip/issues/9617" message = ( - "Value for %s does not match. Please report this: " + "Value for %s does not match. Please report this to %s" "\ndistutils: %s" "\nsysconfig: %s" ) - logger.warning(message, key, old, new) + logger.warning(message, key, issue_url, old, new) def get_scheme( From b4545a01441ec3df4130664cbe2c15f3dfd783cc Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Fri, 19 Feb 2021 17:59:18 +0800 Subject: [PATCH 05/17] News --- news/9617.process.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/9617.process.rst 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. From 08c282919f28c324de48118828e670f8fac88fd3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Fri, 19 Feb 2021 18:11:20 +0800 Subject: [PATCH 06/17] Blackify --- src/pip/_internal/locations/_distutils.py | 39 +++++++++++------------ src/pip/_internal/locations/_sysconfig.py | 6 +--- src/pip/_internal/locations/base.py | 11 +++---- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index bf3ff728f6d..70b7e8d6dd3 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -20,20 +20,20 @@ if WINDOWS: - bin_py = os.path.join(sys.prefix, 'Scripts') - bin_user = os.path.join(user_site, 'Scripts') + bin_py = os.path.join(sys.prefix, "Scripts") + bin_user = os.path.join(user_site, "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(user_site, 'bin') + bin_py = os.path.join(sys.prefix, "bin") + bin_user = os.path.join(user_site, "bin") else: - bin_py = os.path.join(sys.prefix, 'bin') - bin_user = os.path.join(user_site, 'bin') + bin_py = os.path.join(sys.prefix, "bin") + bin_user = os.path.join(user_site, "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' + if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/": + bin_py = "/usr/local/bin" def _distutils_scheme( @@ -45,14 +45,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 @@ -70,28 +70,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:], @@ -126,9 +125,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"], diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index bc49cb75f77..d29580dcc9f 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -5,16 +5,12 @@ import sysconfig import typing -from pip._internal.exceptions import ( - InvalidSchemeCombination, - UserInstallationInvalid, -) +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__) diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 8c9f7751179..7f8ef670aac 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -7,7 +7,6 @@ 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") @@ -21,22 +20,20 @@ def get_major_minor_version(): Return the major-minor version of the current Python as a string, e.g. "3.7" or "3.10". """ - return '{}.{}'.format(*sys.version_info) + return "{}.{}".format(*sys.version_info) def get_src_prefix(): # type: () -> str if running_under_virtualenv(): - src_prefix = os.path.join(sys.prefix, 'src') + 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') + 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." - ) + 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/.. From 790bd02ede18995195ed08fc0ac2aa142c56537c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Fri, 19 Feb 2021 18:15:45 +0800 Subject: [PATCH 07/17] Type fixes --- src/pip/_internal/locations/__init__.py | 12 +++++++++-- src/pip/_internal/locations/_sysconfig.py | 25 ++++++++++++----------- src/pip/_internal/locations/base.py | 2 +- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index db1c1309ee4..327d75a957b 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -131,8 +131,16 @@ def get_prefixed_libs(prefix): """Return the lib locations under ``prefix``.""" old_pure, old_plat = _distutils.get_prefixed_libs(prefix) new_pure, new_plat = _sysconfig.get_prefixed_libs(prefix) - _warn_if_mismatch(old_pure, new_pure, key="prefixed-purelib") - _warn_if_mismatch(old_plat, new_plat, key="prefixed-platlib") + _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 old_pure == old_plat: return [old_pure] return [old_pure, old_plat] diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index d29580dcc9f..64aa487dc68 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -18,7 +18,7 @@ def _infer_scheme(variant): - # (typing.Literal["home", "prefix", "user"]) -> str + # type: (typing.Literal["home", "prefix", "user"]) -> str """Try to find a scheme for the current platform. Unfortunately ``_get_default_scheme()`` is private, so there's no way to @@ -46,16 +46,16 @@ def _infer_scheme(variant): # Update these keys if the user sets a custom home. -_HOME_KEYS = ( +_HOME_KEYS = [ "installed_base", "base", "installed_platbase", "platbase", "prefix", "exec_prefix", -) +] if sysconfig.get_config_var("userbase") is not None: - _HOME_KEYS += ("userbase",) + _HOME_KEYS.append("userbase") def get_scheme( @@ -86,11 +86,11 @@ def get_scheme( raise InvalidSchemeCombination("--home", "--prefix") if home is not None: - scheme = _infer_scheme("home") + scheme_name = _infer_scheme("home") elif user: - scheme = _infer_scheme("user") + scheme_name = _infer_scheme("user") else: - scheme = _infer_scheme("prefix") + scheme_name = _infer_scheme("prefix") if home is not None: variables = {k: home for k in _HOME_KEYS} @@ -99,7 +99,7 @@ def get_scheme( else: variables = {} - paths = sysconfig.get_paths(scheme=scheme, vars=variables) + paths = sysconfig.get_paths(scheme=scheme_name, vars=variables) # Special header path for compatibility to distutils. if running_under_virtualenv(): @@ -126,21 +126,22 @@ def get_bin_prefix(): # 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_path("scripts", scheme=_infer_scheme("prefix")) + return sysconfig.get_paths(scheme=_infer_scheme("prefix"))["scripts"] def get_bin_user(): - return sysconfig.get_path("scripts", scheme=_infer_scheme("user")) + # type: () -> str + return sysconfig.get_paths(scheme=_infer_scheme("user"))["scripts"] def get_purelib(): # type: () -> str - return sysconfig.get_path("purelib") + return sysconfig.get_paths()["purelib"] def get_platlib(): # type: () -> str - return sysconfig.get_path("platlib") + return sysconfig.get_paths()["platlib"] def get_prefixed_libs(prefix): diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 7f8ef670aac..98557abbe63 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -43,6 +43,6 @@ def get_src_prefix(): try: # Use getusersitepackages if this is present, as it ensures that the # value is initialised properly. - user_site = site.getusersitepackages() + user_site = site.getusersitepackages() # type: typing.Optional[str] except AttributeError: user_site = site.USER_SITE From 7563a9c6cc1c576020d3e92ca0ddcae7e19e228d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 23 Feb 2021 02:04:15 +0800 Subject: [PATCH 08/17] Goodness --- src/pip/_internal/locations/_distutils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index 70b7e8d6dd3..d4cb6b17948 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -18,7 +18,6 @@ from .base import get_major_minor_version, user_site - if WINDOWS: bin_py = os.path.join(sys.prefix, "Scripts") bin_user = os.path.join(user_site, "Scripts") From 1e1289e550e9aa329b1e3aa3f49edb6022dde525 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 23 Feb 2021 03:51:12 +0800 Subject: [PATCH 09/17] User-site special case fixes --- src/pip/_internal/locations/_distutils.py | 20 +------------------- src/pip/_internal/locations/_sysconfig.py | 21 +++++++++++++++++---- src/pip/_internal/locations/base.py | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index d4cb6b17948..7eecf8d8fa4 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -5,7 +5,6 @@ import os import os.path -import sys from distutils.cmd import Command as DistutilsCommand from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command @@ -13,26 +12,9 @@ from typing import Dict, List, Optional, Tuple, Union, cast from pip._internal.models.scheme import Scheme -from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv -from .base import get_major_minor_version, user_site - -if WINDOWS: - bin_py = os.path.join(sys.prefix, "Scripts") - bin_user = os.path.join(user_site, "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(user_site, "bin") -else: - bin_py = os.path.join(sys.prefix, "bin") - bin_user = os.path.join(user_site, "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" +from .base import bin_py, bin_user, get_major_minor_version def _distutils_scheme( diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index 64aa487dc68..a71cd8adc43 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -9,7 +9,7 @@ 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 +from .base import bin_user, get_major_minor_version logger = logging.getLogger(__name__) @@ -101,12 +101,22 @@ def get_scheme( paths = sysconfig.get_paths(scheme=scheme_name, vars=variables) - # Special header path for compatibility to distutils. + # Pip historically uses a special header path in virtual environments. if running_under_virtualenv(): - base = variables.get("base", sys.prefix) + 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"]["scritps"]``. + 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"], @@ -131,7 +141,10 @@ def get_bin_prefix(): def get_bin_user(): # type: () -> str - return sysconfig.get_paths(scheme=_infer_scheme("user"))["scripts"] + # pip puts the scripts directory in site-packages, not under userbase. + # I'm honestly not sure if this is a bug (because ``get_scheme()`` puts it + # correctly under userbase), but we need to be compatible. + return bin_user def get_purelib(): diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 98557abbe63..5035662e3a4 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -5,6 +5,7 @@ import typing from pip._internal.utils import appdirs +from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv # Application Directories @@ -46,3 +47,20 @@ def get_src_prefix(): user_site = site.getusersitepackages() # type: typing.Optional[str] except AttributeError: user_site = site.USER_SITE + + +if WINDOWS: + bin_py = os.path.join(sys.prefix, "Scripts") + bin_user = os.path.join(user_site, "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(user_site, "bin") +else: + bin_py = os.path.join(sys.prefix, "bin") + bin_user = os.path.join(user_site, "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" From b7068f643e1b0b4fddab040878319ae8276efd0c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 23 Feb 2021 04:24:17 +0800 Subject: [PATCH 10/17] Split bin_user and bin_prefix implementations Module-level logic is bad. --- src/pip/_internal/locations/__init__.py | 9 +------- src/pip/_internal/locations/_distutils.py | 22 +++++++++++------- src/pip/_internal/locations/_sysconfig.py | 10 +------- src/pip/_internal/locations/base.py | 28 +++++++++++------------ 4 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 327d75a957b..78969546345 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -8,6 +8,7 @@ from . import _distutils, _sysconfig from .base import ( USER_CACHE_DIR, + get_bin_user, get_major_minor_version, get_src_prefix, site_packages, @@ -100,14 +101,6 @@ def get_bin_prefix(): return old -def get_bin_user(): - # type: () -> str - old = _distutils.get_bin_user() - new = _sysconfig.get_bin_user() - _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_user") - return old - - def get_purelib(): # type: () -> str """Return the default pure-Python lib location.""" diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index 7eecf8d8fa4..2d7ab73213c 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -4,7 +4,7 @@ # mypy: strict-optional=False import os -import os.path +import sys from distutils.cmd import Command as DistutilsCommand from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command @@ -12,9 +12,10 @@ from typing import Dict, List, Optional, Tuple, Union, cast from pip._internal.models.scheme import Scheme +from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv -from .base import bin_py, bin_user, get_major_minor_version +from .base import get_major_minor_version def _distutils_scheme( @@ -118,12 +119,17 @@ def get_scheme( def get_bin_prefix(): # type: () -> str - return bin_py - - -def get_bin_user(): - # type: () -> str - return 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") + 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(): diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index a71cd8adc43..8ef72813b3b 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -9,7 +9,7 @@ from pip._internal.models.scheme import SCHEME_KEYS, Scheme from pip._internal.utils.virtualenv import running_under_virtualenv -from .base import bin_user, get_major_minor_version +from .base import get_major_minor_version logger = logging.getLogger(__name__) @@ -139,14 +139,6 @@ def get_bin_prefix(): return sysconfig.get_paths(scheme=_infer_scheme("prefix"))["scripts"] -def get_bin_user(): - # type: () -> str - # pip puts the scripts directory in site-packages, not under userbase. - # I'm honestly not sure if this is a bug (because ``get_scheme()`` puts it - # correctly under userbase), but we need to be compatible. - return bin_user - - def get_purelib(): # type: () -> str return sysconfig.get_paths()["purelib"] diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 5035662e3a4..3a03a79565c 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -49,18 +49,18 @@ def get_src_prefix(): user_site = site.USER_SITE -if WINDOWS: - bin_py = os.path.join(sys.prefix, "Scripts") - bin_user = os.path.join(user_site, "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(user_site, "bin") -else: - bin_py = os.path.join(sys.prefix, "bin") - bin_user = os.path.join(user_site, "bin") +def get_bin_user(): + # type: () -> str + """Get the user-site scripts directory. - # 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" + Pip puts the scripts directory in site-packages, not under userbase. + I'm honestly not sure if this is a bug (because ``get_scheme()`` puts it + correctly under userbase), but we need to keep backwards compatibility. + """ + assert user_site is not None, "user site unavailable" + if not WINDOWS: + return os.path.join(user_site, "bin") + # Special case for buildout, which uses 'bin' on Windows too? + if not os.path.exists(os.path.join(sys.prefix, "Scripts")): + os.path.join(user_site, "bin") + return os.path.join(user_site, "Scripts") From 60a82e7a0eb47191ad92f045c1522dfce6faaeb7 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 23 Feb 2021 04:38:32 +0800 Subject: [PATCH 11/17] Better erroring --- src/pip/_internal/locations/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 3a03a79565c..e373ab30c75 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -4,6 +4,7 @@ import sysconfig import typing +from pip._internal.exceptions import UserInstallationInvalid from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv @@ -57,7 +58,8 @@ def get_bin_user(): I'm honestly not sure if this is a bug (because ``get_scheme()`` puts it correctly under userbase), but we need to keep backwards compatibility. """ - assert user_site is not None, "user site unavailable" + if user_site is None: + raise UserInstallationInvalid() if not WINDOWS: return os.path.join(user_site, "bin") # Special case for buildout, which uses 'bin' on Windows too? From 0c4bd55706cb85166c6b9634284c5b03c39c0103 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 23 Feb 2021 05:04:21 +0800 Subject: [PATCH 12/17] Stray name --- src/pip/_internal/locations/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 78969546345..57fbbfd250c 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -25,7 +25,6 @@ "get_purelib", "get_scheme", "get_src_prefix", - "init_backend", "site_packages", "user_site", ] From a0a3bde15275d0d0b976c9080f210395a33f6d81 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 23 Feb 2021 19:27:27 +0800 Subject: [PATCH 13/17] Split scheme inference functions for clarity This also helps catch bugs in the logic, which are also fixed in this commit. --- src/pip/_internal/locations/_sysconfig.py | 59 ++++++++++++++--------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index 8ef72813b3b..b78366a9db9 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -14,35 +14,46 @@ logger = logging.getLogger(__name__) -_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names()) +# 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_scheme(variant): - # type: (typing.Literal["home", "prefix", "user"]) -> str - """Try to find a scheme for the current platform. - Unfortunately ``_get_default_scheme()`` is private, so there's no way to - ask things like "what is the '_home' scheme on this platform". This tries - to answer that with some heuristics while accounting for ad-hoc platforms - not covered by CPython's default sysconfig implementation. - """ - # Most schemes are named like this e.g. "posix_home", "nt_user". - suffixed = f"{os.name}_{variant}" +def _infer_prefix(): + # type: () -> str + """Try to find a prefix scheme for the current platform.""" + 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" + - # The user scheme is not available. - if variant == "user" and sysconfig.get_config_var("userbase") is None: +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" - # On Windows, prefx and home schemes are the same and just called "nt". - if os.name in _AVAILABLE_SCHEMES: - return os.name - # Not sure what's happening, some obscure platform that does not fully - # implement sysconfig? Just use the POSIX scheme. - logger.warning("No %r scheme for %r; fallback to POSIX.", variant, os.name) - return f"posix_{variant}" +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. @@ -86,11 +97,11 @@ def get_scheme( raise InvalidSchemeCombination("--home", "--prefix") if home is not None: - scheme_name = _infer_scheme("home") + scheme_name = _infer_home() elif user: - scheme_name = _infer_scheme("user") + scheme_name = _infer_user() else: - scheme_name = _infer_scheme("prefix") + scheme_name = _infer_prefix() if home is not None: variables = {k: home for k in _HOME_KEYS} @@ -136,7 +147,7 @@ def get_bin_prefix(): # 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(scheme=_infer_scheme("prefix"))["scripts"] + return sysconfig.get_paths()["scripts"] def get_purelib(): From e0d6028ebf5f136e16223714c4fb87decea3f456 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 24 Feb 2021 17:46:05 +0800 Subject: [PATCH 14/17] Additional logging to get context --- src/pip/_internal/locations/__init__.py | 61 ++++++++++++++++++------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 57fbbfd250c..9513d8b0fea 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -44,16 +44,30 @@ def _default_base(*, user): def _warn_if_mismatch(old, new, *, key): - # type: (pathlib.Path, pathlib.Path, str) -> None + # type: (pathlib.Path, pathlib.Path, str) -> bool if old == new: - return + return False issue_url = "https://github.com/pypa/pip/issues/9617" message = ( - "Value for %s does not match. Please report this to %s" + "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( @@ -83,11 +97,15 @@ def get_scheme( ) 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)) - _warn_if_mismatch(old_v, new_v, key=f"scheme.{k}") + 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 @@ -96,7 +114,8 @@ def get_bin_prefix(): # type: () -> str old = _distutils.get_bin_prefix() new = _sysconfig.get_bin_prefix() - _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix") + if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"): + _log_context() return old @@ -105,7 +124,8 @@ def get_purelib(): """Return the default pure-Python lib location.""" old = _distutils.get_purelib() new = _sysconfig.get_purelib() - _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib") + if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib"): + _log_context() return old @@ -114,7 +134,8 @@ def get_platlib(): """Return the default platform-shared lib location.""" old = _distutils.get_platlib() new = _sysconfig.get_platlib() - _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib") + if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"): + _log_context() return old @@ -123,16 +144,22 @@ def get_prefixed_libs(prefix): """Return the lib locations under ``prefix``.""" old_pure, old_plat = _distutils.get_prefixed_libs(prefix) new_pure, new_plat = _sysconfig.get_prefixed_libs(prefix) - _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", - ) + + 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] From b4ce28923710dd17f2b60d849fa97036dfd2020c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Sun, 28 Feb 2021 06:42:39 +0800 Subject: [PATCH 15/17] Add a couple of special cases for PyPy --- src/pip/_internal/locations/_sysconfig.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index b78366a9db9..ee75a6fb9b6 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -27,7 +27,22 @@ def _infer_prefix(): # type: () -> str - """Try to find a prefix scheme for the current platform.""" + """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 From 826234e3acf1406e855d69207b15caf17d541f6d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Sun, 28 Feb 2021 07:37:23 +0800 Subject: [PATCH 16/17] New style type hints --- src/pip/_internal/locations/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 688403724bb..580da8eae38 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -32,8 +32,7 @@ logger = logging.getLogger(__name__) -def _default_base(*, user): - # type: (bool) -> str +def _default_base(*, user: bool) -> str: if user: base = sysconfig.get_config_var("userbase") else: @@ -42,8 +41,7 @@ def _default_base(*, user): return base -def _warn_if_mismatch(old, new, *, key): - # type: (pathlib.Path, pathlib.Path, str) -> bool +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" From 6d018bf2209cd4ade569a5333a52487c00be033e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Sun, 28 Feb 2021 07:47:17 +0800 Subject: [PATCH 17/17] Special-case PyPy's lib location differences --- src/pip/_internal/locations/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 580da8eae38..18bf0319f3d 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -1,5 +1,6 @@ import logging import pathlib +import sys import sysconfig from typing import List, Optional @@ -99,6 +100,22 @@ def get_scheme( # 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):