-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid adding an empty/incomplete .egg-info
to sys.path
while traversing importlib.metadata.entry_points
#4670
Changes from all commits
2220d40
47c215b
aa7e04f
d486431
e1cd904
8421e5d
e588f89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
"""Convenience layer on top of stdlib's shutil and os""" | ||
|
||
import os | ||
import stat | ||
from typing import Callable, TypeVar | ||
|
||
from .compat import py311 | ||
|
||
from distutils import log | ||
|
||
try: | ||
from os import chmod # pyright: ignore[reportAssignmentType] | ||
# Losing type-safety w/ pyright, but that's ok | ||
except ImportError: # pragma: no cover | ||
# Jython compatibility | ||
def chmod(*args: object, **kwargs: object) -> None: # type: ignore[misc] # Mypy reuses the imported definition anyway | ||
pass | ||
|
||
|
||
_T = TypeVar("_T") | ||
|
||
|
||
def attempt_chmod_verbose(path, mode): | ||
log.debug("changing mode of %s to %o", path, mode) | ||
try: | ||
chmod(path, mode) | ||
except OSError as e: # pragma: no cover | ||
log.debug("chmod failed: %s", e) | ||
|
||
|
||
# Must match shutil._OnExcCallback | ||
def _auto_chmod( | ||
func: Callable[..., _T], arg: str, exc: BaseException | ||
) -> _T: # pragma: no cover | ||
"""shutils onexc callback to automatically call chmod for certain functions.""" | ||
# Only retry for scenarios known to have an issue | ||
if func in [os.unlink, os.remove] and os.name == 'nt': | ||
attempt_chmod_verbose(arg, stat.S_IWRITE) | ||
return func(arg) | ||
raise exc | ||
|
||
|
||
def rmtree(path, ignore_errors=False, onexc=_auto_chmod): | ||
return py311.shutil_rmtree(path, ignore_errors, onexc) | ||
|
||
|
||
def rmdir(path, **opts): | ||
if os.path.isdir(path): | ||
rmtree(path, **opts) | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,8 +6,10 @@ | |
import functools | ||
import os | ||
import re | ||
import shutil | ||
import sys | ||
import time | ||
from tempfile import TemporaryDirectory | ||
|
||
import packaging | ||
import packaging.requirements | ||
|
@@ -20,7 +22,7 @@ | |
from setuptools.command.setopt import edit_config | ||
from setuptools.glob import glob | ||
|
||
from .. import _entry_points, _normalization | ||
from .. import _entry_points, _normalization, _shutil | ||
from .._importlib import metadata | ||
from ..warnings import SetuptoolsDeprecationWarning | ||
from . import _requirestxt | ||
|
@@ -293,20 +295,35 @@ def delete_file(self, filename): | |
os.unlink(filename) | ||
|
||
def run(self): | ||
self.mkpath(self.egg_info) | ||
try: | ||
os.utime(self.egg_info, None) | ||
except OSError as e: | ||
msg = f"Cannot update time stamp of directory '{self.egg_info}'" | ||
raise distutils.errors.DistutilsFileError(msg) from e | ||
for ep in metadata.entry_points(group='egg_info.writers'): | ||
writer = ep.load() | ||
writer(self, ep.name, os.path.join(self.egg_info, ep.name)) | ||
|
||
# Get rid of native_libs.txt if it was put there by older bdist_egg | ||
nl = os.path.join(self.egg_info, "native_libs.txt") | ||
if os.path.exists(nl): | ||
self.delete_file(nl) | ||
# Avoid adding an empty .egg-info to sys.path while using importlib.metadata | ||
# See pypa/pyproject-hooks#206 | ||
self.mkpath(self.egg_base) # avoid file system errors with tmpdir / os.replace | ||
with TemporaryDirectory(prefix=".tmp-", dir=self.egg_base) as tmp: | ||
staging = os.path.join(tmp, "egg_info") | ||
if os.path.isdir(self.egg_info): | ||
shutil.copytree(self.egg_info, staging) # account pre-existing files | ||
else: | ||
os.mkdir(staging) | ||
|
||
for ep in metadata.entry_points(group='egg_info.writers'): | ||
writer = ep.load() | ||
writer(self, ep.name, os.path.join(staging, ep.name)) | ||
|
||
# Get rid of native_libs.txt if it was put there by older bdist_egg | ||
nl = os.path.join(staging, "native_libs.txt") | ||
if os.path.exists(nl): # pragma: no cover | ||
self.delete_file(nl) | ||
|
||
# Remove old directory and create the new one | ||
try: | ||
# Unfortunately os.replace does not work for existing destination dirs, | ||
# so we cannot have a single atomic operation | ||
_shutil.rmdir(self.egg_info) | ||
os.replace(staging, self.egg_info) | ||
log.info(f"renaming {staging!r} to {self.egg_info!r}") | ||
except OSError as e: | ||
msg = f"Cannot create directory '{self.egg_info}' ({e})" | ||
raise distutils.errors.DistutilsFileError(msg) from e | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jaraco in this rewrite the main idea is to use a temporary directory to avoid adding an empty May be a bit convoluted, but this is what I could come up to make the process a bit more atomic. This is related to the discussion in pypa/pyproject-hooks#206. |
||
|
||
self.find_sources() | ||
|
||
|
@@ -654,13 +671,14 @@ def write_pkg_info(cmd, basename, filename): | |
try: | ||
# write unescaped data to PKG-INFO, so older pkg_resources | ||
# can still parse it | ||
metadata.write_pkg_info(cmd.egg_info) | ||
parent_dir = os.path.dirname(filename) | ||
metadata.write_pkg_info(parent_dir) | ||
finally: | ||
metadata.name, metadata.version = oldname, oldver | ||
|
||
safe = getattr(cmd.distribution, 'zip_safe', None) | ||
|
||
bdist_egg.write_safety_flag(cmd.egg_info, safe) | ||
bdist_egg.write_safety_flag(parent_dir, safe) | ||
|
||
|
||
def warn_depends_obsolete(cmd, basename, filename): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -504,7 +504,7 @@ def warn_dash_deprecation(self, opt, section): | |
versions. Please use the underscore name {underscore_opt!r} instead. | ||
""", | ||
see_docs="userguide/declarative_config.html", | ||
due_date=(2024, 9, 26), | ||
due_date=(2025, 3, 3), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
# Warning initially introduced in 3 Mar 2021 | ||
) | ||
return underscore_opt | ||
|
@@ -529,7 +529,7 @@ def make_option_lowercase(self, opt, section): | |
future versions. Please use lowercase {lowercase_opt!r} instead. | ||
""", | ||
see_docs="userguide/declarative_config.html", | ||
due_date=(2024, 9, 26), | ||
due_date=(2025, 3, 3), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be discussed in a separated issue/PR. For now just postponing to get the CI error out of the way. |
||
# Warning initially introduced in 6 Mar 2021 | ||
) | ||
return lowercase_opt | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -165,19 +165,17 @@ def test_expected_files_produced(self, tmpdir_cwd, env): | |
] | ||
assert sorted(actual) == expected | ||
|
||
def test_handling_utime_error(self, tmpdir_cwd, env): | ||
dist = Distribution() | ||
def test_handling_file_system_error(self, tmpdir_cwd, env): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Repurposing test, since the implementation no longer uses |
||
dist = Distribution({"script_name": "hello.py"}) | ||
ei = egg_info(dist) | ||
utime_patch = mock.patch('os.utime', side_effect=OSError("TEST")) | ||
mkpath_patch = mock.patch( | ||
'setuptools.command.egg_info.egg_info.mkpath', return_val=None | ||
) | ||
rm_patch = mock.patch('setuptools._shutil.rmdir', side_effect=OSError("TEST")) | ||
|
||
with utime_patch, mkpath_patch: | ||
with rm_patch: | ||
import distutils.errors | ||
|
||
msg = r"Cannot update time stamp of directory 'None'" | ||
msg = r"Cannot create directory .*egg-info" | ||
with pytest.raises(distutils.errors.DistutilsFileError, match=msg): | ||
ei.ensure_finalized() | ||
ei.run() | ||
|
||
def test_license_is_a_string(self, tmpdir_cwd, env): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This module was extracted from
easy_install
for re-use.