From b777bcda819961092ae87ca7512e0b27c576d19c Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 5 Oct 2022 18:19:28 +0200 Subject: [PATCH 01/23] Add the "base" config level concept, to allow configuration of all virtual environments sharing the same base. The new functionality serves a use case which was not previously possible with pip configuration files, namely the situation where you have a base Python installation and want to influence the pip configuration for all derivative virtual environments *without* changing the config for all other environments on a machine (global), or for all other environment run by the same user (user). Concretely, this could be used for a centrally managed network mounted filesystem based Python installation, from which multiple users can build virtual environments and inside which a specific pip configuration is needed (e.g. an index URL). --- docs/html/topics/configuration.md | 27 ++++++++++++++++-------- news/9752.feature.rst | 1 + src/pip/_internal/configuration.py | 18 +++++++++++++--- tests/unit/test_configuration.py | 33 ++++++++++++++++++++++++++++-- tests/unit/test_options.py | 2 +- 5 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 news/9752.feature.rst diff --git a/docs/html/topics/configuration.md b/docs/html/topics/configuration.md index e4aafcd2b..c544d0c70 100644 --- a/docs/html/topics/configuration.md +++ b/docs/html/topics/configuration.md @@ -19,13 +19,14 @@ and how they are related to pip's various command line options. ## Configuration Files -Configuration files can change the default values for command line option. -They are written using a standard INI style configuration files. +Configuration files can change the default values for command line options. +They are written using standard INI style configuration files. -pip has 3 "levels" of configuration files: +pip has 4 "levels" of configuration files: -- `global`: system-wide configuration file, shared across users. -- `user`: per-user configuration file. +- `global`: system-wide configuration file, shared across all users. +- `user`: per-user configuration file, shared across all environments. +- `base` : per-base environment configuration file, shared across all virtualenvs with the same base. (added in pip `v23`) - `site`: per-environment configuration file; i.e. per-virtualenv. ### Location @@ -47,8 +48,11 @@ User The legacy "per-user" configuration file is also loaded, if it exists: {file}`$HOME/.pip/pip.conf`. +Base +: {file}`\{sys.base_prefix\}/pip.conf` + Site -: {file}`$VIRTUAL_ENV/pip.conf` +: {file}`\{sys.prefix\}/pip.conf` ``` ```{tab} MacOS @@ -63,8 +67,11 @@ User The legacy "per-user" configuration file is also loaded, if it exists: {file}`$HOME/.pip/pip.conf`. +Base +: {file}`\{sys.base_prefix\}/pip.conf` + Site -: {file}`$VIRTUAL_ENV/pip.conf` +: {file}`\{sys.prefix\}/pip.conf` ``` ```{tab} Windows @@ -81,8 +88,11 @@ User The legacy "per-user" configuration file is also loaded, if it exists: {file}`%HOME%\\pip\\pip.ini` +Base +: {file}`\{sys.base_prefix\}\\pip.ini` + Site -: {file}`%VIRTUAL_ENV%\\pip.ini` +: {file}`\{sys.prefix\}\\pip.ini` ``` ### `PIP_CONFIG_FILE` @@ -102,6 +112,7 @@ order: - `PIP_CONFIG_FILE`, if given. - Global - User +- Base - Site Each file read overrides any values read from previous files, so if the diff --git a/news/9752.feature.rst b/news/9752.feature.rst new file mode 100644 index 000000000..d515267be --- /dev/null +++ b/news/9752.feature.rst @@ -0,0 +1 @@ +In the case of virtual environments, configuration files are now also included from the base installation. diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 8fd46c9b8..6cce8bcbc 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -36,12 +36,20 @@ ENV_NAMES_IGNORED = "version", "help" kinds = enum( USER="user", # User Specific GLOBAL="global", # System Wide - SITE="site", # [Virtual] Environment Specific + BASE="base", # Base environment specific (e.g. for all venvs with the same base) + SITE="site", # Environment Specific (e.g. per venv) ENV="env", # from PIP_CONFIG_FILE ENV_VAR="env-var", # from Environment Variables ) -OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR -VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE +OVERRIDE_ORDER = ( + kinds.GLOBAL, + kinds.USER, + kinds.BASE, + kinds.SITE, + kinds.ENV, + kinds.ENV_VAR, +) +VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.BASE, kinds.SITE logger = getLogger(__name__) @@ -70,6 +78,7 @@ def get_configuration_files() -> Dict[Kind, List[str]]: os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs("pip") ] + base_config_file = os.path.join(sys.base_prefix, CONFIG_BASENAME) site_config_file = os.path.join(sys.prefix, CONFIG_BASENAME) legacy_config_file = os.path.join( os.path.expanduser("~"), @@ -78,6 +87,7 @@ def get_configuration_files() -> Dict[Kind, List[str]]: ) new_config_file = os.path.join(appdirs.user_config_dir("pip"), CONFIG_BASENAME) return { + kinds.BASE: [base_config_file], kinds.GLOBAL: global_config_files, kinds.SITE: [site_config_file], kinds.USER: [legacy_config_file, new_config_file], @@ -344,6 +354,8 @@ class Configuration: # The legacy config file is overridden by the new config file yield kinds.USER, config_files[kinds.USER] + yield kinds.BASE, config_files[kinds.BASE] + # finally virtualenv configuration first trumping others yield kinds.SITE, config_files[kinds.SITE] diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index c6b44d45a..b0d655d8f 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -24,12 +24,18 @@ class TestConfigurationLoading(ConfigurationMixin): self.configuration.load() assert self.configuration.get_value("test.hello") == "2" - def test_site_loading(self) -> None: - self.patch_configuration(kinds.SITE, {"test.hello": "3"}) + def test_base_loading(self) -> None: + self.patch_configuration(kinds.BASE, {"test.hello": "3"}) self.configuration.load() assert self.configuration.get_value("test.hello") == "3" + def test_site_loading(self) -> None: + self.patch_configuration(kinds.SITE, {"test.hello": "4"}) + + self.configuration.load() + assert self.configuration.get_value("test.hello") == "4" + def test_environment_config_loading(self, monkeypatch: pytest.MonkeyPatch) -> None: contents = """ [test] @@ -107,6 +113,15 @@ class TestConfigurationLoading(ConfigurationMixin): with pytest.raises(ConfigurationError, match=pat): self.configuration.get_value("global.index-url") + def test_overrides_normalization(self) -> None: + # Check that normalized names are used in precedence calculations. + # Reminder: USER has higher precedence than GLOBAL. + self.patch_configuration(kinds.USER, {"test.hello-world": "1"}) + self.patch_configuration(kinds.GLOBAL, {"test.hello_world": "0"}) + self.configuration.load() + + assert self.configuration.get_value("test.hello_world") == "1" + class TestConfigurationPrecedence(ConfigurationMixin): # Tests for methods to that determine the order of precedence of @@ -133,6 +148,13 @@ class TestConfigurationPrecedence(ConfigurationMixin): assert self.configuration.get_value("test.hello") == "0" + def test_site_overides_base(self) -> None: + self.patch_configuration(kinds.BASE, {"test.hello": "2"}) + self.patch_configuration(kinds.SITE, {"test.hello": "1"}) + self.configuration.load() + + assert self.configuration.get_value("test.hello") == "1" + def test_site_overides_user(self) -> None: self.patch_configuration(kinds.USER, {"test.hello": "2"}) self.patch_configuration(kinds.SITE, {"test.hello": "1"}) @@ -147,6 +169,13 @@ class TestConfigurationPrecedence(ConfigurationMixin): assert self.configuration.get_value("test.hello") == "1" + def test_base_overides_user(self) -> None: + self.patch_configuration(kinds.USER, {"test.hello": "2"}) + self.patch_configuration(kinds.BASE, {"test.hello": "1"}) + self.configuration.load() + + assert self.configuration.get_value("test.hello") == "1" + def test_user_overides_global(self) -> None: self.patch_configuration(kinds.GLOBAL, {"test.hello": "3"}) self.patch_configuration(kinds.USER, {"test.hello": "2"}) diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index ada5e1c30..39396512a 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -588,7 +588,7 @@ class TestOptionsConfigFiles: for _, val in cp.iter_config_files(): files.extend(val) - assert len(files) == 4 + assert len(files) == 5 @pytest.mark.parametrize( "args, expect", From 2b82b386601f1ded7a4f1c64ffc1a1083124590a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 18 Oct 2022 20:44:00 -0400 Subject: [PATCH 02/23] Fix grammar --- src/pip/_internal/self_outdated_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 9e2149c52..f87c9a5d2 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -133,7 +133,7 @@ class UpgradePrompt: return Group( Text(), Text.from_markup( - f"{notice} A new release of pip available: " + f"{notice} A new release of pip is available: " f"[red]{self.old}[reset] -> [green]{self.new}[reset]" ), Text.from_markup( From 0e48cae74e4921b4a09115e890435c464540b91b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 19 Oct 2022 00:02:05 -0400 Subject: [PATCH 03/23] Add a news fragment for the grammar fix --- news/news/11529.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/news/11529.bugfix.rst diff --git a/news/news/11529.bugfix.rst b/news/news/11529.bugfix.rst new file mode 100644 index 000000000..d05e40460 --- /dev/null +++ b/news/news/11529.bugfix.rst @@ -0,0 +1 @@ +Fix grammar by changing "A new release of pip available:" to "A new release of pip is available:" in the notice used for indicating that. From 22d401bfb7814b0ef023c21c90be20e0d6bad993 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 19 Oct 2022 00:07:31 -0400 Subject: [PATCH 04/23] Update the news fragment for the grammar fix --- news/{news => }/11529.bugfix.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{news => }/11529.bugfix.rst (100%) diff --git a/news/news/11529.bugfix.rst b/news/11529.bugfix.rst similarity index 100% rename from news/news/11529.bugfix.rst rename to news/11529.bugfix.rst From 93ade8586e3eb3792250f1f0eb924c1664bc4df7 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 16 Nov 2022 21:17:33 +0100 Subject: [PATCH 05/23] Update docs/html/topics/configuration.md Co-authored-by: Tzu-ping Chung --- docs/html/topics/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/topics/configuration.md b/docs/html/topics/configuration.md index c544d0c70..ddeee0826 100644 --- a/docs/html/topics/configuration.md +++ b/docs/html/topics/configuration.md @@ -26,7 +26,7 @@ pip has 4 "levels" of configuration files: - `global`: system-wide configuration file, shared across all users. - `user`: per-user configuration file, shared across all environments. -- `base` : per-base environment configuration file, shared across all virtualenvs with the same base. (added in pip `v23`) +- `base` : per-base environment configuration file, shared across all virtualenvs with the same base. (available since pip 23.0) - `site`: per-environment configuration file; i.e. per-virtualenv. ### Location From 81d6053ee3ce5c4a8b0572cb80c542ab9f7e461e Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Thu, 17 Nov 2022 17:25:30 +0100 Subject: [PATCH 06/23] Use the VIRTUAL_ENV environment variable in the configuration documentation This follows the discussion in https://github.com/pypa/pip/pull/11487/files#r988625394, that despite the VIRTUAL_ENV environment variable not being the technically correct value, it is more readily understood by readers than ``sys.prefix``. --- docs/html/topics/configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/html/topics/configuration.md b/docs/html/topics/configuration.md index ddeee0826..521bc9af4 100644 --- a/docs/html/topics/configuration.md +++ b/docs/html/topics/configuration.md @@ -52,7 +52,7 @@ Base : {file}`\{sys.base_prefix\}/pip.conf` Site -: {file}`\{sys.prefix\}/pip.conf` +: {file}`$VIRTUAL_ENV/pip.conf` ``` ```{tab} MacOS @@ -71,7 +71,7 @@ Base : {file}`\{sys.base_prefix\}/pip.conf` Site -: {file}`\{sys.prefix\}/pip.conf` +: {file}`$VIRTUAL_ENV/pip.conf` ``` ```{tab} Windows @@ -92,7 +92,7 @@ Base : {file}`\{sys.base_prefix\}\\pip.ini` Site -: {file}`\{sys.prefix\}\\pip.ini` +: {file}`%VIRTUAL_ENV%\\pip.ini` ``` ### `PIP_CONFIG_FILE` From d21af1c986589cda1f722f5ca5b4748bfe026705 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 30 Jan 2023 15:13:08 +0000 Subject: [PATCH 07/23] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 2aead080d..ce90d06bf 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1,6 +1,6 @@ from typing import List, Optional -__version__ = "23.0" +__version__ = "23.1.dev0" def main(args: Optional[List[str]] = None) -> int: From 9058b900214579a8430e5aaec383d61c9608022a Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Fri, 3 Feb 2023 09:32:53 +0800 Subject: [PATCH 08/23] fix: correct the way to decide if keyring is available --- src/pip/_internal/network/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index c16213268..a1c52315b 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -359,7 +359,7 @@ class MultiDomainBasicAuth(AuthBase): # Factored out to allow for easy patching in tests def _should_save_password_to_keyring(self) -> bool: - if get_keyring_provider() is None: + if isinstance(get_keyring_provider(), KeyRingNullProvider): return False return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y" From 706456c5cf463f3ed8f1a949a71bf4379e6baf3a Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Fri, 3 Feb 2023 09:39:21 +0800 Subject: [PATCH 09/23] add news --- news/11774.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/11774.bugfix.rst diff --git a/news/11774.bugfix.rst b/news/11774.bugfix.rst new file mode 100644 index 000000000..771246b0b --- /dev/null +++ b/news/11774.bugfix.rst @@ -0,0 +1 @@ +Correct the way to decide if keyring is available. From 2d0a5c9cd29f72348031b8b517068f98aed14ad7 Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Fri, 3 Feb 2023 15:33:55 +0800 Subject: [PATCH 10/23] use a attribute to tell if the provider is null --- src/pip/_internal/network/auth.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index a1c52315b..ac8cbf23b 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -39,6 +39,8 @@ class Credentials(NamedTuple): class KeyRingBaseProvider(ABC): """Keyring base provider interface""" + has_keyring: bool + @abstractmethod def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]: ... @@ -51,6 +53,8 @@ class KeyRingBaseProvider(ABC): class KeyRingNullProvider(KeyRingBaseProvider): """Keyring null provider""" + has_keyring = False + def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]: return None @@ -61,6 +65,8 @@ class KeyRingNullProvider(KeyRingBaseProvider): class KeyRingPythonProvider(KeyRingBaseProvider): """Keyring interface which uses locally imported `keyring`""" + has_keyring = True + def __init__(self) -> None: import keyring @@ -97,6 +103,8 @@ class KeyRingCliProvider(KeyRingBaseProvider): PATH. """ + has_keyring = True + def __init__(self, cmd: str) -> None: self.keyring = cmd @@ -359,7 +367,7 @@ class MultiDomainBasicAuth(AuthBase): # Factored out to allow for easy patching in tests def _should_save_password_to_keyring(self) -> bool: - if isinstance(get_keyring_provider(), KeyRingNullProvider): + if not get_keyring_provider().has_keyring: return False return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y" @@ -432,9 +440,7 @@ class MultiDomainBasicAuth(AuthBase): def save_credentials(self, resp: Response, **kwargs: Any) -> None: """Response callback to save credentials on success.""" keyring = get_keyring_provider() - assert not isinstance( - keyring, KeyRingNullProvider - ), "should never reach here without keyring" + assert keyring.has_keyring, "should never reach here without keyring" creds = self._credentials_to_save self._credentials_to_save = None From e5c88951a036fced59c77c0212c9d27150da335d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 5 Feb 2023 00:13:32 +0100 Subject: [PATCH 11/23] Do not crash in presence of misformatted hash field in ``direct_url.json``. --- news/11773.bugfix.rst | 1 + src/pip/_internal/models/direct_url.py | 7 ++++++- tests/unit/test_direct_url.py | 7 +++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 news/11773.bugfix.rst diff --git a/news/11773.bugfix.rst b/news/11773.bugfix.rst new file mode 100644 index 000000000..077bf0612 --- /dev/null +++ b/news/11773.bugfix.rst @@ -0,0 +1 @@ +Do not crash in presence of misformatted hash field in ``direct_url.json``. diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index 09b540f91..c3de70a74 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -108,7 +108,12 @@ class ArchiveInfo: if hash is not None: # Auto-populate the hashes key to upgrade to the new format automatically. # We don't back-populate the legacy hash key. - hash_name, hash_value = hash.split("=", 1) + try: + hash_name, hash_value = hash.split("=", 1) + except ValueError: + raise DirectUrlValidationError( + f"invalid archive_info.hash format: {hash!r}" + ) if hashes is None: hashes = {hash_name: hash_value} elif hash_name not in hash: diff --git a/tests/unit/test_direct_url.py b/tests/unit/test_direct_url.py index e1708ae93..3ca982b50 100644 --- a/tests/unit/test_direct_url.py +++ b/tests/unit/test_direct_url.py @@ -102,6 +102,13 @@ def test_parsing_validation() -> None: match="more than one of archive_info, dir_info, vcs_info", ): DirectUrl.from_dict({"url": "http://...", "dir_info": {}, "archive_info": {}}) + with pytest.raises( + DirectUrlValidationError, + match="invalid archive_info.hash format", + ): + DirectUrl.from_dict( + {"url": "http://...", "archive_info": {"hash": "sha256:aaa"}} + ) def test_redact_url() -> None: From 62fb64ac9697e36efce3f72193e4bff0a39dbe14 Mon Sep 17 00:00:00 2001 From: Oliver Mannion <125105+tekumara@users.noreply.github.com> Date: Mon, 6 Feb 2023 18:05:31 +1100 Subject: [PATCH 12/23] Ignore PIP_REQUIRE_VIRTUALENV for `pip index` (#11671) Ignore PIP_REQUIRE_VIRTUALENV in `pip index` --- news/11671.feature.rst | 1 + src/pip/_internal/commands/index.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/11671.feature.rst diff --git a/news/11671.feature.rst b/news/11671.feature.rst new file mode 100644 index 000000000..31f81f8da --- /dev/null +++ b/news/11671.feature.rst @@ -0,0 +1 @@ +Ignore PIP_REQUIRE_VIRTUALENV for ``pip index`` diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index b4bf0ac06..7267effed 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -24,6 +24,7 @@ class IndexCommand(IndexGroupCommand): Inspect information available from package indexes. """ + ignore_require_venv = True usage = """ %prog versions """ From 9abb3c899a3b6e4dad590791f3c8d2421bce66c5 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Sun, 5 Feb 2023 23:06:59 -0800 Subject: [PATCH 13/23] Implement `--break-system-packages` for EXTERNALLY-MANAGED installations (#11780) The PEP 668 expects an override mechanism to ease the transition. This provides an override. --------- Co-authored-by: Pradyun Gedam --- news/11780.feature.rst | 2 ++ src/pip/_internal/cli/cmdoptions.py | 8 ++++++++ src/pip/_internal/commands/install.py | 6 +++++- src/pip/_internal/commands/uninstall.py | 4 +++- src/pip/_internal/exceptions.py | 4 +++- tests/functional/test_pep668.py | 16 ++++++++++++++++ 6 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 news/11780.feature.rst diff --git a/news/11780.feature.rst b/news/11780.feature.rst new file mode 100644 index 000000000..b765de6c5 --- /dev/null +++ b/news/11780.feature.rst @@ -0,0 +1,2 @@ +Implement ``--break-system-packages`` to permit installing packages into +``EXTERNALLY-MANAGED`` Python installations. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 661c489c7..1f804097e 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -164,6 +164,14 @@ require_virtualenv: Callable[..., Option] = partial( ), ) +override_externally_managed: Callable[..., Option] = partial( + Option, + "--break-system-packages", + dest="override_externally_managed", + action="store_true", + help="Allow pip to modify an EXTERNALLY-MANAGED Python installation", +) + python: Callable[..., Option] = partial( Option, "--python", diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index cecaac2bc..b20aeddf8 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -215,6 +215,7 @@ class InstallCommand(RequirementCommand): self.cmd_opts.add_option(cmdoptions.use_pep517()) self.cmd_opts.add_option(cmdoptions.no_use_pep517()) self.cmd_opts.add_option(cmdoptions.check_build_deps()) + self.cmd_opts.add_option(cmdoptions.override_externally_managed()) self.cmd_opts.add_option(cmdoptions.config_settings()) self.cmd_opts.add_option(cmdoptions.install_options()) @@ -296,7 +297,10 @@ class InstallCommand(RequirementCommand): and options.target_dir is None and options.prefix_path is None ) - if installing_into_current_environment: + if ( + installing_into_current_environment + and not options.override_externally_managed + ): check_externally_managed() upgrade_strategy = "to-satisfy-only" diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index e5a4c8e10..f198fc313 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -58,6 +58,7 @@ class UninstallCommand(Command, SessionCommandMixin): help="Don't ask for confirmation of uninstall deletions.", ) self.cmd_opts.add_option(cmdoptions.root_user_action()) + self.cmd_opts.add_option(cmdoptions.override_externally_managed()) self.parser.insert_option_group(0, self.cmd_opts) def run(self, options: Values, args: List[str]) -> int: @@ -93,7 +94,8 @@ class UninstallCommand(Command, SessionCommandMixin): f'"pip help {self.name}")' ) - check_externally_managed() + if not options.override_externally_managed: + check_externally_managed() protect_pip_from_modification_on_windows( modifying_pip="pip" in reqs_to_uninstall diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index d28713ff7..d4527295d 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -696,7 +696,9 @@ class ExternallyManagedEnvironment(DiagnosticPipError): context=context, note_stmt=( "If you believe this is a mistake, please contact your " - "Python installation or OS distribution provider." + "Python installation or OS distribution provider. " + "You can override this, at the risk of breaking your Python " + "installation or OS, by passing --break-system-packages." ), hint_stmt=Text("See PEP 668 for the detailed specification."), ) diff --git a/tests/functional/test_pep668.py b/tests/functional/test_pep668.py index 1fed85e70..3c1085668 100644 --- a/tests/functional/test_pep668.py +++ b/tests/functional/test_pep668.py @@ -42,6 +42,22 @@ def test_fails(script: PipTestEnvironment, arguments: List[str]) -> None: assert "I am externally managed" in result.stderr +@pytest.mark.parametrize( + "arguments", + [ + pytest.param(["install"], id="install"), + pytest.param(["install", "--dry-run"], id="install-dry-run"), + pytest.param(["uninstall", "-y"], id="uninstall"), + ], +) +@pytest.mark.usefixtures("patch_check_externally_managed") +def test_succeeds_when_overridden( + script: PipTestEnvironment, arguments: List[str] +) -> None: + result = script.pip(*arguments, "pip", "--break-system-packages") + assert "I am externally managed" not in result.stderr + + @pytest.mark.parametrize( "arguments", [ From a2b0eb683821ec4f16f953de76aef58378767d05 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Fri, 3 Feb 2023 12:00:14 +0100 Subject: [PATCH 14/23] Document the limitations of the pip install --prefix argument, and cross-reference the --python flag, which can be harder to find due to it being a pip level argument --- src/pip/_internal/commands/install.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index cecaac2bc..739de020d 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -156,7 +156,12 @@ class InstallCommand(RequirementCommand): default=None, help=( "Installation prefix where lib, bin and other top-level " - "folders are placed" + "folders are placed. Note that the resulting installation may " + "contain scripts and other resources which reference the " + "Python interpreter of pip, and not that of ``--prefix``. " + "See also the ``--python`` option if the intention is to " + "install packages into another (possibly pip-free) " + "environment." ), ) From 7ff4da6e8f835867e01e36166cef4fe8a2be8b4b Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Mon, 6 Feb 2023 12:27:44 +0100 Subject: [PATCH 15/23] Reconcile computation of isolated build environment paths (#11740) Use the same code to determine isolated environment paths at dependency install time and at environment setup time. We do not care about the exact paths but the paths needs to be consistent at package installation time and environment setup. This should fix issues observed on platforms that customize the installation schemes, such as Debian and Homebrew, where dependency installation and isolated build environment setup resolved to different paths. --- news/11740.bugfix.rst | 3 + src/pip/_internal/build_env.py | 18 +++--- src/pip/_internal/locations/__init__.py | 68 +---------------------- src/pip/_internal/locations/_distutils.py | 9 +-- src/pip/_internal/locations/_sysconfig.py | 12 ---- 5 files changed, 14 insertions(+), 96 deletions(-) create mode 100644 news/11740.bugfix.rst diff --git a/news/11740.bugfix.rst b/news/11740.bugfix.rst new file mode 100644 index 000000000..917beb535 --- /dev/null +++ b/news/11740.bugfix.rst @@ -0,0 +1,3 @@ +Improve handling of isolated build environments on platforms that +customize the Python's installation schemes, such as Debian and +Homebrew. diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 24bfa870b..4f704a354 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -9,7 +9,7 @@ import sys import textwrap from collections import OrderedDict from types import TracebackType -from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type +from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union from pip._vendor.certifi import where from pip._vendor.packaging.requirements import Requirement @@ -17,12 +17,7 @@ from pip._vendor.packaging.version import Version from pip import __file__ as pip_location from pip._internal.cli.spinners import open_spinner -from pip._internal.locations import ( - get_isolated_environment_bin_path, - get_isolated_environment_lib_paths, - get_platlib, - get_purelib, -) +from pip._internal.locations import get_platlib, get_purelib, get_scheme from pip._internal.metadata import get_default_environment, get_environment from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds @@ -33,12 +28,17 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +def _dedup(a: str, b: str) -> Union[Tuple[str], Tuple[str, str]]: + return (a, b) if a != b else (a,) + + class _Prefix: def __init__(self, path: str) -> None: self.path = path self.setup = False - self.bin_dir = get_isolated_environment_bin_path(path) - self.lib_dirs = get_isolated_environment_lib_paths(path) + scheme = get_scheme("", prefix=path) + self.bin_dir = scheme.scripts + self.lib_dirs = _dedup(scheme.purelib, scheme.platlib) def get_runnable_pip() -> str: diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 496844be1..d54bc63eb 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -4,7 +4,7 @@ import os import pathlib import sys import sysconfig -from typing import Any, Dict, Generator, List, Optional, Tuple +from typing import Any, Dict, Generator, Optional, Tuple from pip._internal.models.scheme import SCHEME_KEYS, Scheme from pip._internal.utils.compat import WINDOWS @@ -25,8 +25,6 @@ __all__ = [ "USER_CACHE_DIR", "get_bin_prefix", "get_bin_user", - "get_isolated_environment_bin_path", - "get_isolated_environment_lib_paths", "get_major_minor_version", "get_platlib", "get_purelib", @@ -467,67 +465,3 @@ def get_platlib() -> str: if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"): _log_context() return old - - -def _deduplicated(v1: str, v2: str) -> List[str]: - """Deduplicate values from a list.""" - if v1 == v2: - return [v1] - return [v1, v2] - - -def _looks_like_apple_library(path: str) -> bool: - """Apple patches sysconfig to *always* look under */Library/Python*.""" - if sys.platform[:6] != "darwin": - return False - return path == f"/Library/Python/{get_major_minor_version()}/site-packages" - - -def get_isolated_environment_lib_paths(prefix: str) -> List[str]: - """Return the lib locations under ``prefix``.""" - new_pure, new_plat = _sysconfig.get_isolated_environment_lib_paths(prefix) - if _USE_SYSCONFIG: - return _deduplicated(new_pure, new_plat) - - old_pure, old_plat = _distutils.get_isolated_environment_lib_paths(prefix) - old_lib_paths = _deduplicated(old_pure, old_plat) - - # Apple's Python (shipped with Xcode and Command Line Tools) hard-code - # platlib and purelib to '/Library/Python/X.Y/site-packages'. This will - # cause serious build isolation bugs when Apple starts shipping 3.10 because - # pip will install build backends to the wrong location. This tells users - # who is at fault so Apple may notice it and fix the issue in time. - if all(_looks_like_apple_library(p) for p in old_lib_paths): - deprecated( - reason=( - "Python distributed by Apple's Command Line Tools incorrectly " - "patches sysconfig to always point to '/Library/Python'. This " - "will cause build isolation to operate incorrectly on Python " - "3.10 or later. Please help report this to Apple so they can " - "fix this. https://developer.apple.com/bug-reporting/" - ), - replacement=None, - gone_in=None, - ) - return old_lib_paths - - 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) - - return old_lib_paths - - -def get_isolated_environment_bin_path(prefix: str) -> str: - return _sysconfig.get_isolated_environment_paths(prefix)["scripts"] diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index a6fbcd2f0..92bd93179 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -21,7 +21,7 @@ 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 distutils.sysconfig import get_python_lib -from typing import Dict, List, Optional, Tuple, Union, cast +from typing import Dict, List, Optional, Union, cast from pip._internal.models.scheme import Scheme from pip._internal.utils.compat import WINDOWS @@ -171,10 +171,3 @@ def get_purelib() -> str: def get_platlib() -> str: return get_python_lib(plat_specific=True) - - -def get_isolated_environment_lib_paths(prefix: 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 38e400f94..97aef1f1a 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -211,15 +211,3 @@ def get_purelib() -> str: def get_platlib() -> str: return sysconfig.get_paths()["platlib"] - - -def get_isolated_environment_paths(prefix: str) -> typing.Dict[str, str]: - variables = {"base": prefix, "platbase": prefix} - if "venv" in sysconfig.get_scheme_names(): - return sysconfig.get_paths(vars=variables, scheme="venv") - return sysconfig.get_paths(vars=variables) - - -def get_isolated_environment_lib_paths(prefix: str) -> typing.Tuple[str, str]: - paths = get_isolated_environment_paths(prefix) - return (paths["purelib"], paths["platlib"]) From a373982a0cae47bb9f5a311b07f13b445cc97f9e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 6 Feb 2023 17:26:46 +0000 Subject: [PATCH 16/23] Implement package-selection CLI in `nox -s vendoring` This makes it easier to update/not update certain packages. --- noxfile.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index 1345c417d..5c4683b7d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,7 @@ """Automation using nox. """ +import argparse import glob import os import shutil @@ -183,7 +184,13 @@ def lint(session: nox.Session) -> None: def vendoring(session: nox.Session) -> None: session.install("vendoring~=1.2.0") - if "--upgrade" not in session.posargs: + parser = argparse.ArgumentParser(prog="nox -s vendoring") + parser.add_argument("--upgrade-all", action="store_true") + parser.add_argument("--upgrade", action="append", default=[]) + parser.add_argument("--skip", action="append", default=[]) + args = parser.parse_args(session.posargs) + + if not (args.upgrade or args.upgrade_all): session.run("vendoring", "sync", "-v") return @@ -199,7 +206,9 @@ def vendoring(session: nox.Session) -> None: vendor_txt = Path("src/pip/_vendor/vendor.txt") for name, old_version in pinned_requirements(vendor_txt): - if name == "setuptools": + if name in args.skip: + continue + if args.upgrade and name not in args.upgrade: continue # update requirements.txt From 6245fedc9d2bc6a5a6b332080ad34ad8f2f87e23 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 6 Feb 2023 17:19:17 +0000 Subject: [PATCH 17/23] Make resolvelib's provider capable of handling empty iterators This is _technically_ possible with the API, and accounting for that enables the resolver to evolve with this information. --- .../resolution/resolvelib/provider.py | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 6300dfc57..64e6356f1 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -124,14 +124,29 @@ class PipProvider(_ProviderBase): * If equal, prefer if any requirement is "pinned", i.e. contains operator ``===`` or ``==``. * If equal, calculate an approximate "depth" and resolve requirements - closer to the user-specified requirements first. + closer to the user-specified requirements first. If the depth cannot + by determined (eg: due to no matching parents), it is considered + infinite. * Order user-specified requirements by the order they are specified. * If equal, prefers "non-free" requirements, i.e. contains at least one operator, such as ``>=`` or ``<``. * If equal, order alphabetically for consistency (helps debuggability). """ - lookups = (r.get_candidate_lookup() for r, _ in information[identifier]) - candidate, ireqs = zip(*lookups) + try: + next(iter(information[identifier])) + except StopIteration: + # There is no information for this identifier, so there's no known + # candidates. + has_information = False + else: + has_information = True + + if has_information: + lookups = (r.get_candidate_lookup() for r, _ in information[identifier]) + candidate, ireqs = zip(*lookups) + else: + candidate, ireqs = None, () + operators = [ specifier.operator for specifier_set in (ireq.specifier for ireq in ireqs if ireq) @@ -146,11 +161,14 @@ class PipProvider(_ProviderBase): requested_order: Union[int, float] = self._user_requested[identifier] except KeyError: requested_order = math.inf - parent_depths = ( - self._known_depths[parent.name] if parent is not None else 0.0 - for _, parent in information[identifier] - ) - inferred_depth = min(d for d in parent_depths) + 1.0 + if has_information: + parent_depths = ( + self._known_depths[parent.name] if parent is not None else 0.0 + for _, parent in information[identifier] + ) + inferred_depth = min(d for d in parent_depths) + 1.0 + else: + inferred_depth = math.inf else: inferred_depth = 1.0 self._known_depths[identifier] = inferred_depth From 8dbc2db274b0724d7b86eb1c2fbac6b0b4a4c4f6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 6 Feb 2023 17:29:08 +0000 Subject: [PATCH 18/23] Upgrade resolvelib to 0.9.0 --- news/resolvelib.vendor.rst | 1 + src/pip/_vendor/resolvelib/__init__.py | 2 +- .../resolvelib/compat/collections_abc.pyi | 1 + src/pip/_vendor/resolvelib/providers.py | 14 +++--- src/pip/_vendor/resolvelib/providers.pyi | 4 +- src/pip/_vendor/resolvelib/reporters.py | 2 +- src/pip/_vendor/resolvelib/reporters.pyi | 2 +- src/pip/_vendor/resolvelib/resolvers.py | 44 ++++++++++++++++++- src/pip/_vendor/resolvelib/resolvers.pyi | 12 +++++ src/pip/_vendor/resolvelib/structs.py | 11 +++-- src/pip/_vendor/resolvelib/structs.pyi | 2 +- src/pip/_vendor/vendor.txt | 2 +- 12 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 news/resolvelib.vendor.rst create mode 100644 src/pip/_vendor/resolvelib/compat/collections_abc.pyi diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst new file mode 100644 index 000000000..c8b5c928d --- /dev/null +++ b/news/resolvelib.vendor.rst @@ -0,0 +1 @@ +Upgrade resolvelib to 0.9.0 diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index ce05fd302..fa6995e32 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ __all__ = [ "ResolutionTooDeep", ] -__version__ = "0.8.1" +__version__ = "0.9.0" from .providers import AbstractProvider, AbstractResolver diff --git a/src/pip/_vendor/resolvelib/compat/collections_abc.pyi b/src/pip/_vendor/resolvelib/compat/collections_abc.pyi new file mode 100644 index 000000000..2a088b19a --- /dev/null +++ b/src/pip/_vendor/resolvelib/compat/collections_abc.pyi @@ -0,0 +1 @@ +from collections.abc import Mapping, Sequence diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py index 7d0a9c22a..e99d87ee7 100644 --- a/src/pip/_vendor/resolvelib/providers.py +++ b/src/pip/_vendor/resolvelib/providers.py @@ -1,5 +1,5 @@ class AbstractProvider(object): - """Delegate class to provide requirement interface for the resolver.""" + """Delegate class to provide the required interface for the resolver.""" def identify(self, requirement_or_candidate): """Given a requirement, return an identifier for it. @@ -24,9 +24,9 @@ class AbstractProvider(object): this group of arguments is. :param identifier: An identifier as returned by ``identify()``. This - identifies the dependency matches of which should be returned. + identifies the dependency matches which should be returned. :param resolutions: Mapping of candidates currently pinned by the - resolver. Each key is an identifier, and the value a candidate. + resolver. Each key is an identifier, and the value is a candidate. The candidate may conflict with requirements from ``information``. :param candidates: Mapping of each dependency's possible candidates. Each value is an iterator of candidates. @@ -39,10 +39,10 @@ class AbstractProvider(object): * ``requirement`` specifies a requirement contributing to the current list of candidates. - * ``parent`` specifies the candidate that provides (dependend on) the + * ``parent`` specifies the candidate that provides (depended on) the requirement, or ``None`` to indicate a root requirement. - The preference could depend on a various of issues, including (not + The preference could depend on various issues, including (not necessarily in this order): * Is this package pinned in the current resolution result? @@ -61,7 +61,7 @@ class AbstractProvider(object): raise NotImplementedError def find_matches(self, identifier, requirements, incompatibilities): - """Find all possible candidates that satisfy given constraints. + """Find all possible candidates that satisfy the given constraints. :param identifier: An identifier as returned by ``identify()``. This identifies the dependency matches of which should be returned. @@ -92,7 +92,7 @@ class AbstractProvider(object): def is_satisfied_by(self, requirement, candidate): """Whether the given requirement can be satisfied by a candidate. - The candidate is guarenteed to have been generated from the + The candidate is guaranteed to have been generated from the requirement. A boolean should be returned to indicate whether ``candidate`` is a diff --git a/src/pip/_vendor/resolvelib/providers.pyi b/src/pip/_vendor/resolvelib/providers.pyi index 47d6f8aba..ec054194e 100644 --- a/src/pip/_vendor/resolvelib/providers.pyi +++ b/src/pip/_vendor/resolvelib/providers.pyi @@ -1,12 +1,11 @@ from typing import ( Any, - Collection, Generic, Iterable, Iterator, Mapping, - Optional, Protocol, + Sequence, Union, ) @@ -25,6 +24,7 @@ class AbstractProvider(Generic[RT, CT, KT]): resolutions: Mapping[KT, CT], candidates: Mapping[KT, Iterator[CT]], information: Mapping[KT, Iterator[RequirementInformation[RT, CT]]], + backtrack_causes: Sequence[RequirementInformation[RT, CT]], ) -> Preference: ... def find_matches( self, diff --git a/src/pip/_vendor/resolvelib/reporters.py b/src/pip/_vendor/resolvelib/reporters.py index 6695480ff..688b5e10d 100644 --- a/src/pip/_vendor/resolvelib/reporters.py +++ b/src/pip/_vendor/resolvelib/reporters.py @@ -36,7 +36,7 @@ class BaseReporter(object): :param causes: The information on the collision that caused the backtracking. """ - def backtracking(self, candidate): + def rejecting_candidate(self, criterion, candidate): """Called when rejecting a candidate during backtracking.""" def pinning(self, candidate): diff --git a/src/pip/_vendor/resolvelib/reporters.pyi b/src/pip/_vendor/resolvelib/reporters.pyi index 03d4f09a3..b2ad286ba 100644 --- a/src/pip/_vendor/resolvelib/reporters.pyi +++ b/src/pip/_vendor/resolvelib/reporters.pyi @@ -6,6 +6,6 @@ class BaseReporter: def ending_round(self, index: int, state: Any) -> Any: ... def ending(self, state: Any) -> Any: ... def adding_requirement(self, requirement: Any, parent: Any) -> Any: ... - def backtracking(self, candidate: Any) -> Any: ... + def rejecting_candidate(self, criterion: Any, candidate: Any) -> Any: ... def resolving_conflicts(self, causes: Any) -> Any: ... def pinning(self, candidate: Any) -> Any: ... diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index 787681b03..49e30c7f5 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -173,6 +173,31 @@ class Resolution(object): raise RequirementsConflicted(criterion) criteria[identifier] = criterion + def _remove_information_from_criteria(self, criteria, parents): + """Remove information from parents of criteria. + + Concretely, removes all values from each criterion's ``information`` + field that have one of ``parents`` as provider of the requirement. + + :param criteria: The criteria to update. + :param parents: Identifiers for which to remove information from all criteria. + """ + if not parents: + return + for key, criterion in criteria.items(): + criteria[key] = Criterion( + criterion.candidates, + [ + information + for information in criterion.information + if ( + information[1] is None + or self._p.identify(information[1]) not in parents + ) + ], + criterion.incompatibilities, + ) + def _get_preference(self, name): return self._p.get_preference( identifier=name, @@ -212,6 +237,7 @@ class Resolution(object): try: criteria = self._get_updated_criteria(candidate) except RequirementsConflicted as e: + self._r.rejecting_candidate(e.criterion, candidate) causes.append(e.criterion) continue @@ -281,8 +307,6 @@ class Resolution(object): # Also mark the newly known incompatibility. incompatibilities_from_broken.append((name, [candidate])) - self._r.backtracking(candidate=candidate) - # Create a new state from the last known-to-work one, and apply # the previously gathered incompatibility information. def _patch_criteria(): @@ -368,6 +392,11 @@ class Resolution(object): self._r.ending(state=self.state) return self.state + # keep track of satisfied names to calculate diff after pinning + satisfied_names = set(self.state.criteria.keys()) - set( + unsatisfied_names + ) + # Choose the most preferred unpinned criterion to try. name = min(unsatisfied_names, key=self._get_preference) failure_causes = self._attempt_to_pin_criterion(name) @@ -384,6 +413,17 @@ class Resolution(object): if not success: raise ResolutionImpossible(self.state.backtrack_causes) else: + # discard as information sources any invalidated names + # (unsatisfied names that were previously satisfied) + newly_unsatisfied_names = { + key + for key, criterion in self.state.criteria.items() + if key in satisfied_names + and not self._is_current_pin_satisfying(key, criterion) + } + self._remove_information_from_criteria( + self.state.criteria, newly_unsatisfied_names + ) # Pinning was successful. Push a new state to do another pin. self._push_new_state() diff --git a/src/pip/_vendor/resolvelib/resolvers.pyi b/src/pip/_vendor/resolvelib/resolvers.pyi index 0eb5b2162..528a1a259 100644 --- a/src/pip/_vendor/resolvelib/resolvers.pyi +++ b/src/pip/_vendor/resolvelib/resolvers.pyi @@ -55,6 +55,18 @@ class ResolutionImpossible(ResolutionError, Generic[RT, CT]): class ResolutionTooDeep(ResolutionError): round_count: int +# This should be a NamedTuple, but Python 3.6 has a bug that prevents it. +# https://stackoverflow.com/a/50531189/1376863 +class State(tuple, Generic[RT, CT, KT]): + mapping: Mapping[KT, CT] + criteria: Mapping[KT, Criterion[RT, CT, KT]] + backtrack_causes: Collection[RequirementInformation[RT, CT]] + +class Resolution(Generic[RT, CT, KT]): + def resolve( + self, requirements: Iterable[RT], max_rounds: int + ) -> State[RT, CT, KT]: ... + class Result(Generic[RT, CT, KT]): mapping: Mapping[KT, CT] graph: DirectedGraph[Optional[KT]] diff --git a/src/pip/_vendor/resolvelib/structs.py b/src/pip/_vendor/resolvelib/structs.py index 93d1568bd..359a34f60 100644 --- a/src/pip/_vendor/resolvelib/structs.py +++ b/src/pip/_vendor/resolvelib/structs.py @@ -117,13 +117,14 @@ class _FactoryIterableView(object): def __init__(self, factory): self._factory = factory + self._iterable = None def __repr__(self): - return "{}({})".format(type(self).__name__, list(self._factory())) + return "{}({})".format(type(self).__name__, list(self)) def __bool__(self): try: - next(self._factory()) + next(iter(self)) except StopIteration: return False return True @@ -131,7 +132,11 @@ class _FactoryIterableView(object): __nonzero__ = __bool__ # XXX: Python 2. def __iter__(self): - return self._factory() + iterable = ( + self._factory() if self._iterable is None else self._iterable + ) + self._iterable, current = itertools.tee(iterable) + return current class _SequenceIterableView(object): diff --git a/src/pip/_vendor/resolvelib/structs.pyi b/src/pip/_vendor/resolvelib/structs.pyi index fae2a2fce..0ac59f0f0 100644 --- a/src/pip/_vendor/resolvelib/structs.pyi +++ b/src/pip/_vendor/resolvelib/structs.pyi @@ -16,7 +16,7 @@ RT = TypeVar("RT") # Requirement. CT = TypeVar("CT") # Candidate. _T = TypeVar("_T") -Matches = Union[Iterable[CT], Callable[[], Iterator[CT]]] +Matches = Union[Iterable[CT], Callable[[], Iterable[CT]]] class IteratorMapping(Mapping[KT, _T], metaclass=ABCMeta): pass diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 67452d89f..703daf196 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -15,7 +15,7 @@ requests==2.28.2 rich==12.6.0 pygments==2.13.0 typing_extensions==4.4.0 -resolvelib==0.8.1 +resolvelib==0.9.0 setuptools==44.0.0 six==1.16.0 tenacity==8.1.0 From 88cccfc142c32b2a957cd90ea1a2535c7102528a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 6 Feb 2023 17:32:54 +0000 Subject: [PATCH 19/23] Handle `backtracking` -> `rejecting_candidate` change This is a newer method on resolvelib's end for reporting when a candidate is rejected. --- .../resolution/resolvelib/reporter.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 6ced5329b..a95a8e4cf 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -11,9 +11,9 @@ logger = getLogger(__name__) class PipReporter(BaseReporter): def __init__(self) -> None: - self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int) + self.reject_count_by_package: DefaultDict[str, int] = defaultdict(int) - self._messages_at_backtrack = { + self._messages_at_reject_count = { 1: ( "pip is looking at multiple versions of {package_name} to " "determine which version is compatible with other " @@ -32,14 +32,14 @@ class PipReporter(BaseReporter): ), } - def backtracking(self, candidate: Candidate) -> None: - self.backtracks_by_package[candidate.name] += 1 + def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None: + self.reject_count_by_package[candidate.name] += 1 - count = self.backtracks_by_package[candidate.name] - if count not in self._messages_at_backtrack: + count = self.reject_count_by_package[candidate.name] + if count not in self._messages_at_reject_count: return - message = self._messages_at_backtrack[count] + message = self._messages_at_reject_count[count] logger.info("INFO: %s", message.format(package_name=candidate.name)) @@ -61,8 +61,8 @@ class PipDebuggingReporter(BaseReporter): def adding_requirement(self, requirement: Requirement, parent: Candidate) -> None: logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent) - def backtracking(self, candidate: Candidate) -> None: - logger.info("Reporter.backtracking(%r)", candidate) + def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None: + logger.info("Reporter.rejecting_candidate(%r, %r)", criterion, candidate) def pinning(self, candidate: Candidate) -> None: logger.info("Reporter.pinning(%r)", candidate) From 4f455ae0b0a6159c25cfcce691d9078bc3c0b454 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 6 Feb 2023 17:45:40 +0000 Subject: [PATCH 20/23] Drop an unused `type: ignore` comment --- src/pip/_internal/resolution/resolvelib/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 64e6356f1..b08cce7f3 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -104,7 +104,7 @@ class PipProvider(_ProviderBase): def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str: return requirement_or_candidate.name - def get_preference( # type: ignore + def get_preference( self, identifier: str, resolutions: Mapping[str, Candidate], From 8844795f454e9997639ad5a5b893fc95762d1734 Mon Sep 17 00:00:00 2001 From: Tobias Hermann Date: Tue, 7 Feb 2023 08:03:08 +0100 Subject: [PATCH 21/23] Fix minor typo in features list of version 23.0 (#11785) --- NEWS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 2e24fab37..19f759477 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -22,7 +22,7 @@ Features modify the externally managed environment. (`#11381 `_) - Enable the use of ``keyring`` found on ``PATH``. This allows ``keyring`` installed using ``pipx`` to be used by ``pip``. (`#11589 `_) -- The inspect and installation report formats are now declared stabled, and their version +- The inspect and installation report formats are now declared stable, and their version has been bumped from ``0`` to ``1``. (`#11757 `_) Bug Fixes From f12a2ef2a216315e1e65a844f855f15600e13cfd Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Thu, 9 Feb 2023 13:17:07 +0100 Subject: [PATCH 22/23] Include a news item for the documentation change --- news/11775.doc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/11775.doc.rst diff --git a/news/11775.doc.rst b/news/11775.doc.rst new file mode 100644 index 000000000..d4eff89c0 --- /dev/null +++ b/news/11775.doc.rst @@ -0,0 +1 @@ +Cross-reference the --python flag in the docs for the --prefix flag, and mention the --prefix limitations with regards to installed console scripts. From e399d7d4ede3f5fad6a3f58b97dbae532daf5e19 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Thu, 9 Feb 2023 20:21:11 +0800 Subject: [PATCH 23/23] Doc formatting --- news/11775.doc.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/news/11775.doc.rst b/news/11775.doc.rst index d4eff89c0..18274b769 100644 --- a/news/11775.doc.rst +++ b/news/11775.doc.rst @@ -1 +1,2 @@ -Cross-reference the --python flag in the docs for the --prefix flag, and mention the --prefix limitations with regards to installed console scripts. +Cross-reference the ``--python`` flag from the ``--prefix`` flag, +and mention limitations of ``--prefix`` regarding script installation.