From d94adba065cae4556e9d02577c1e51ea72f6d087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 1 Oct 2023 15:17:13 +0200 Subject: [PATCH 1/5] Remove redundant detection of installed eggs The importlib.metadata backend will discover them as long as they are in sys.path, which setup.py install and easy_install do by adding them to easy-install.pth. --- news/12308.bugfix.rst | 4 ++ src/pip/_internal/metadata/importlib/_envs.py | 56 ------------------- 2 files changed, 4 insertions(+), 56 deletions(-) create mode 100644 news/12308.bugfix.rst diff --git a/news/12308.bugfix.rst b/news/12308.bugfix.rst new file mode 100644 index 00000000000..4ac3f54bc69 --- /dev/null +++ b/news/12308.bugfix.rst @@ -0,0 +1,4 @@ +When using the ``importlib.metadata`` backend (the default on Python 3.11+), +``pip list`` does not show installed egg distributions more than once anymore. +Additionally, egg distributions whose parent directory was in ``sys.path`` but +the egg themselves were not in ``sys.path`` are not detected anymore. diff --git a/src/pip/_internal/metadata/importlib/_envs.py b/src/pip/_internal/metadata/importlib/_envs.py index c7b09d215e3..8c0f404c7f0 100644 --- a/src/pip/_internal/metadata/importlib/_envs.py +++ b/src/pip/_internal/metadata/importlib/_envs.py @@ -1,11 +1,9 @@ -import functools import importlib.metadata import logging import os import pathlib import sys import zipfile -import zipimport from typing import Iterator, List, Optional, Sequence, Set, Tuple from pip._vendor.packaging.utils import ( @@ -16,7 +14,6 @@ ) from pip._internal.metadata.base import BaseDistribution, BaseEnvironment -from pip._internal.utils.deprecation import deprecated from pip._internal.utils.filetypes import WHEEL_EXTENSION from ._compat import BadMetadata, BasePath, get_dist_canonical_name, get_info_location @@ -112,54 +109,6 @@ def find_linked(self, location: str) -> Iterator[BaseDistribution]: for dist, info_location in self._find_impl(target_location): yield Distribution(dist, info_location, path) - def _find_eggs_in_dir(self, location: str) -> Iterator[BaseDistribution]: - from pip._vendor.pkg_resources import find_distributions - - from pip._internal.metadata import pkg_resources as legacy - - with os.scandir(location) as it: - for entry in it: - if not entry.name.endswith(".egg"): - continue - for dist in find_distributions(entry.path): - yield legacy.Distribution(dist) - - def _find_eggs_in_zip(self, location: str) -> Iterator[BaseDistribution]: - from pip._vendor.pkg_resources import find_eggs_in_zip - - from pip._internal.metadata import pkg_resources as legacy - - try: - importer = zipimport.zipimporter(location) - except zipimport.ZipImportError: - return - for dist in find_eggs_in_zip(importer, location): - yield legacy.Distribution(dist) - - def find_eggs(self, location: str) -> Iterator[BaseDistribution]: - """Find eggs in a location. - - This actually uses the old *pkg_resources* backend. We likely want to - deprecate this so we can eventually remove the *pkg_resources* - dependency entirely. Before that, this should first emit a deprecation - warning for some versions when using the fallback since importing - *pkg_resources* is slow for those who don't need it. - """ - if os.path.isdir(location): - yield from self._find_eggs_in_dir(location) - if zipfile.is_zipfile(location): - yield from self._find_eggs_in_zip(location) - - -@functools.lru_cache(maxsize=None) # Warn a distribution exactly once. -def _emit_egg_deprecation(location: Optional[str]) -> None: - deprecated( - reason=f"Loading egg at {location} is deprecated.", - replacement="to use pip for package installation", - gone_in="25.1", - issue=12330, - ) - class Environment(BaseEnvironment): def __init__(self, paths: Sequence[str]) -> None: @@ -179,11 +128,6 @@ def _iter_distributions(self) -> Iterator[BaseDistribution]: finder = _DistributionFinder() for location in self._paths: yield from finder.find(location) - if sys.version_info < (3, 14): - for dist in finder.find_eggs(location): - _emit_egg_deprecation(dist.location) - yield dist - # This must go last because that's how pkg_resources tie-breaks. yield from finder.find_linked(location) def get_distribution(self, name: str) -> Optional[BaseDistribution]: From 367ca6a45b9306a3a58b045f3e1b9e205dd13365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 6 Apr 2025 19:53:14 +0200 Subject: [PATCH 2/5] Make double uninstall test independent of order Discovery order depends on the metadata backend. For the importlib backend, it has changed following the previous commit, because now the .egg-link file is found while inspecting site-package, and the .egg directory is found after because it comes after in sys.path. Before the dedictated .egg detection logic was triggered while inspecting site package and found it, before looking for .egg-link. --- tests/functional/test_uninstall.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index db2d25f2233..92886566f88 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -628,10 +628,6 @@ def test_uninstall_with_symlink( assert symlink_target.stat().st_mode == st_mode -@pytest.mark.skipif( - "sys.version_info >= (3, 14)", - reason="Uninstall of .egg distributions not supported in Python 3.14+", -) def test_uninstall_setuptools_develop_install( script: PipTestEnvironment, data: TestData ) -> None: @@ -642,11 +638,19 @@ def test_uninstall_setuptools_develop_install( script.assert_installed(FSPkg="0.1.dev0") # Uninstall both develop and install uninstall = script.pip("uninstall", "FSPkg", "-y") - assert any(p.suffix == ".egg" for p in uninstall.files_deleted), str(uninstall) uninstall2 = script.pip("uninstall", "FSPkg", "-y") - assert ( + # Depending on the metadata backend, the egg-link will be uninstalled first + # or second, so we use xor in the assertions below. + assert (join(script.site_packages, "FSPkg.egg-link") in uninstall.files_deleted) ^ ( join(script.site_packages, "FSPkg.egg-link") in uninstall2.files_deleted - ), str(uninstall2) + ) + assert any( + p.name.startswith("FSPkg") and p.suffix == ".egg" + for p in uninstall.files_deleted + ) ^ any( + p.name.startswith("FSPkg") and p.suffix == ".egg" + for p in uninstall2.files_deleted + ) script.assert_not_installed("FSPkg") From 4a765606f9c1d39059e429cd5394c246045fb34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 7 Apr 2025 00:03:02 +0200 Subject: [PATCH 3/5] Fix uninstallation of zipped eggs --- src/pip/_internal/metadata/base.py | 4 +++- src/pip/_internal/req/req_uninstall.py | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 9eabcdb278b..ea5a0756dd7 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -231,7 +231,9 @@ def installed_as_egg(self) -> bool: location = self.location if not location: return False - return location.endswith(".egg") + # XXX if the distribution is a zipped egg, location has a trailing / + # so we resort to pathlib.Path to check the suffix in a reliable way. + return pathlib.Path(location).suffix == ".egg" @property def installed_with_setuptools_egg_info(self) -> bool: diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index e48c53eb1e4..a41082317d1 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -505,10 +505,13 @@ def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet": # package installed by easy_install # We cannot match on dist.egg_name because it can slightly vary # i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg - paths_to_remove.add(dist_location) - easy_install_egg = os.path.split(dist_location)[1] + # XXX We use normalized_dist_location because dist_location my contain + # a trailing / if the distribution is a zipped egg + # (which is not a directory). + paths_to_remove.add(normalized_dist_location) + easy_install_egg = os.path.split(normalized_dist_location)[1] easy_install_pth = os.path.join( - os.path.dirname(dist_location), + os.path.dirname(normalized_dist_location), "easy-install.pth", ) paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg) From e8794224f513a2b964d5f969026f283dc9a23003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 7 Apr 2025 00:32:15 +0200 Subject: [PATCH 4/5] Rename find_linked to find_legacy_editables --- src/pip/_internal/metadata/importlib/_envs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/metadata/importlib/_envs.py b/src/pip/_internal/metadata/importlib/_envs.py index 8c0f404c7f0..314e75e6731 100644 --- a/src/pip/_internal/metadata/importlib/_envs.py +++ b/src/pip/_internal/metadata/importlib/_envs.py @@ -85,7 +85,7 @@ def find(self, location: str) -> Iterator[BaseDistribution]: installed_location = info_location.parent yield Distribution(dist, info_location, installed_location) - def find_linked(self, location: str) -> Iterator[BaseDistribution]: + def find_legacy_editables(self, location: str) -> Iterator[BaseDistribution]: """Read location in egg-link files and return distributions in there. The path should be a directory; otherwise this returns nothing. This @@ -128,7 +128,7 @@ def _iter_distributions(self) -> Iterator[BaseDistribution]: finder = _DistributionFinder() for location in self._paths: yield from finder.find(location) - yield from finder.find_linked(location) + yield from finder.find_legacy_editables(location) def get_distribution(self, name: str) -> Optional[BaseDistribution]: canonical_name = canonicalize_name(name) From d35c08df09cebe2f4887b0a31bb1127e730d8ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 12 Apr 2025 11:54:04 +0200 Subject: [PATCH 5/5] Clarify what the removal of the pkg_ressources backend implies This does not actually imply that eggs are not detected. When they are in sys.path they continue to be detected. --- news/13010.removal.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/news/13010.removal.rst b/news/13010.removal.rst index 1b665cc20bf..7af2f09b3ac 100644 --- a/news/13010.removal.rst +++ b/news/13010.removal.rst @@ -1,2 +1 @@ -On python 3.14+, the ``pkg_resources`` metadata backend is not used anymore, -and pip does not attempt to detect installed ``.egg`` distributions. +On python 3.14+, the ``pkg_resources`` metadata backend cannot be used anymore.