Skip to content

Commit d852ebd

Browse files
authored
Merge pull request #12308
Remove additional detection of installed egg distributions when using the imporlib.metadata backend
2 parents be3818a + d35c08d commit d852ebd

File tree

6 files changed

+27
-71
lines changed

6 files changed

+27
-71
lines changed

news/12308.bugfix.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
When using the ``importlib.metadata`` backend (the default on Python 3.11+),
2+
``pip list`` does not show installed egg distributions more than once anymore.
3+
Additionally, egg distributions whose parent directory was in ``sys.path`` but
4+
the egg themselves were not in ``sys.path`` are not detected anymore.

news/13010.removal.rst

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
On python 3.14+, the ``pkg_resources`` metadata backend is not used anymore,
2-
and pip does not attempt to detect installed ``.egg`` distributions.
1+
On python 3.14+, the ``pkg_resources`` metadata backend cannot be used anymore.

src/pip/_internal/metadata/base.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,9 @@ def installed_as_egg(self) -> bool:
231231
location = self.location
232232
if not location:
233233
return False
234-
return location.endswith(".egg")
234+
# XXX if the distribution is a zipped egg, location has a trailing /
235+
# so we resort to pathlib.Path to check the suffix in a reliable way.
236+
return pathlib.Path(location).suffix == ".egg"
235237

236238
@property
237239
def installed_with_setuptools_egg_info(self) -> bool:

src/pip/_internal/metadata/importlib/_envs.py

+2-58
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import functools
21
import importlib.metadata
32
import logging
43
import os
54
import pathlib
65
import sys
76
import zipfile
8-
import zipimport
97
from typing import Iterator, List, Optional, Sequence, Set, Tuple
108

119
from pip._vendor.packaging.utils import (
@@ -16,7 +14,6 @@
1614
)
1715

1816
from pip._internal.metadata.base import BaseDistribution, BaseEnvironment
19-
from pip._internal.utils.deprecation import deprecated
2017
from pip._internal.utils.filetypes import WHEEL_EXTENSION
2118

2219
from ._compat import BadMetadata, BasePath, get_dist_canonical_name, get_info_location
@@ -88,7 +85,7 @@ def find(self, location: str) -> Iterator[BaseDistribution]:
8885
installed_location = info_location.parent
8986
yield Distribution(dist, info_location, installed_location)
9087

91-
def find_linked(self, location: str) -> Iterator[BaseDistribution]:
88+
def find_legacy_editables(self, location: str) -> Iterator[BaseDistribution]:
9289
"""Read location in egg-link files and return distributions in there.
9390
9491
The path should be a directory; otherwise this returns nothing. This
@@ -112,54 +109,6 @@ def find_linked(self, location: str) -> Iterator[BaseDistribution]:
112109
for dist, info_location in self._find_impl(target_location):
113110
yield Distribution(dist, info_location, path)
114111

115-
def _find_eggs_in_dir(self, location: str) -> Iterator[BaseDistribution]:
116-
from pip._vendor.pkg_resources import find_distributions
117-
118-
from pip._internal.metadata import pkg_resources as legacy
119-
120-
with os.scandir(location) as it:
121-
for entry in it:
122-
if not entry.name.endswith(".egg"):
123-
continue
124-
for dist in find_distributions(entry.path):
125-
yield legacy.Distribution(dist)
126-
127-
def _find_eggs_in_zip(self, location: str) -> Iterator[BaseDistribution]:
128-
from pip._vendor.pkg_resources import find_eggs_in_zip
129-
130-
from pip._internal.metadata import pkg_resources as legacy
131-
132-
try:
133-
importer = zipimport.zipimporter(location)
134-
except zipimport.ZipImportError:
135-
return
136-
for dist in find_eggs_in_zip(importer, location):
137-
yield legacy.Distribution(dist)
138-
139-
def find_eggs(self, location: str) -> Iterator[BaseDistribution]:
140-
"""Find eggs in a location.
141-
142-
This actually uses the old *pkg_resources* backend. We likely want to
143-
deprecate this so we can eventually remove the *pkg_resources*
144-
dependency entirely. Before that, this should first emit a deprecation
145-
warning for some versions when using the fallback since importing
146-
*pkg_resources* is slow for those who don't need it.
147-
"""
148-
if os.path.isdir(location):
149-
yield from self._find_eggs_in_dir(location)
150-
if zipfile.is_zipfile(location):
151-
yield from self._find_eggs_in_zip(location)
152-
153-
154-
@functools.lru_cache(maxsize=None) # Warn a distribution exactly once.
155-
def _emit_egg_deprecation(location: Optional[str]) -> None:
156-
deprecated(
157-
reason=f"Loading egg at {location} is deprecated.",
158-
replacement="to use pip for package installation",
159-
gone_in="25.1",
160-
issue=12330,
161-
)
162-
163112

164113
class Environment(BaseEnvironment):
165114
def __init__(self, paths: Sequence[str]) -> None:
@@ -179,12 +128,7 @@ def _iter_distributions(self) -> Iterator[BaseDistribution]:
179128
finder = _DistributionFinder()
180129
for location in self._paths:
181130
yield from finder.find(location)
182-
if sys.version_info < (3, 14):
183-
for dist in finder.find_eggs(location):
184-
_emit_egg_deprecation(dist.location)
185-
yield dist
186-
# This must go last because that's how pkg_resources tie-breaks.
187-
yield from finder.find_linked(location)
131+
yield from finder.find_legacy_editables(location)
188132

189133
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
190134
canonical_name = canonicalize_name(name)

src/pip/_internal/req/req_uninstall.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -505,10 +505,13 @@ def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet":
505505
# package installed by easy_install
506506
# We cannot match on dist.egg_name because it can slightly vary
507507
# i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
508-
paths_to_remove.add(dist_location)
509-
easy_install_egg = os.path.split(dist_location)[1]
508+
# XXX We use normalized_dist_location because dist_location my contain
509+
# a trailing / if the distribution is a zipped egg
510+
# (which is not a directory).
511+
paths_to_remove.add(normalized_dist_location)
512+
easy_install_egg = os.path.split(normalized_dist_location)[1]
510513
easy_install_pth = os.path.join(
511-
os.path.dirname(dist_location),
514+
os.path.dirname(normalized_dist_location),
512515
"easy-install.pth",
513516
)
514517
paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)

tests/functional/test_uninstall.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -628,10 +628,6 @@ def test_uninstall_with_symlink(
628628
assert symlink_target.stat().st_mode == st_mode
629629

630630

631-
@pytest.mark.skipif(
632-
"sys.version_info >= (3, 14)",
633-
reason="Uninstall of .egg distributions not supported in Python 3.14+",
634-
)
635631
def test_uninstall_setuptools_develop_install(
636632
script: PipTestEnvironment, data: TestData
637633
) -> None:
@@ -642,11 +638,19 @@ def test_uninstall_setuptools_develop_install(
642638
script.assert_installed(FSPkg="0.1.dev0")
643639
# Uninstall both develop and install
644640
uninstall = script.pip("uninstall", "FSPkg", "-y")
645-
assert any(p.suffix == ".egg" for p in uninstall.files_deleted), str(uninstall)
646641
uninstall2 = script.pip("uninstall", "FSPkg", "-y")
647-
assert (
642+
# Depending on the metadata backend, the egg-link will be uninstalled first
643+
# or second, so we use xor in the assertions below.
644+
assert (join(script.site_packages, "FSPkg.egg-link") in uninstall.files_deleted) ^ (
648645
join(script.site_packages, "FSPkg.egg-link") in uninstall2.files_deleted
649-
), str(uninstall2)
646+
)
647+
assert any(
648+
p.name.startswith("FSPkg") and p.suffix == ".egg"
649+
for p in uninstall.files_deleted
650+
) ^ any(
651+
p.name.startswith("FSPkg") and p.suffix == ".egg"
652+
for p in uninstall2.files_deleted
653+
)
650654
script.assert_not_installed("FSPkg")
651655

652656

0 commit comments

Comments
 (0)