Skip to content

Commit a99e24e

Browse files
committed
Avoid using bdist_wheel in editable_wheel
1 parent 539a05a commit a99e24e

File tree

2 files changed

+68
-57
lines changed

2 files changed

+68
-57
lines changed

setuptools/_wheelbuilder.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
_Path = Union[str, Path]
1717
_Timestamp = Tuple[int, int, int, int, int, int]
18-
_StrOrIter = Union[str, Iterable[str]]
18+
_TextOrIter = Union[str, bytes, Iterable[str], Iterable[bytes]]
1919

2020
_HASH_ALG = "sha256"
2121
_HASH_BUF_SIZE = 65536
@@ -121,7 +121,7 @@ def add_tree(
121121
arcname = os.path.join(prefix, arcname)
122122
self.add_existing_file(arcname, file)
123123

124-
def new_file(self, arcname: str, contents: _StrOrIter, permissions: int = 0o664):
124+
def new_file(self, arcname: str, contents: _TextOrIter, permissions: int = 0o664):
125125
"""
126126
Create a new entry in the wheel named ``arcname`` that contains
127127
the UTF-8 text specified by ``contents``.
@@ -131,10 +131,10 @@ def new_file(self, arcname: str, contents: _StrOrIter, permissions: int = 0o664)
131131
zipinfo.compress_type = self._compression
132132
hashsum = hashlib.new(_HASH_ALG)
133133
file_size = 0
134-
iter_contents = [contents] if isinstance(contents, str) else contents
134+
iter_contents = [contents] if isinstance(contents, (str, bytes)) else contents
135135
with self._zip.open(zipinfo, "w") as fp:
136136
for part in iter_contents:
137-
bpart = bytes(part, "utf-8")
137+
bpart = bytes(part, "utf-8") if isinstance(part, str) else part
138138
file_size += fp.write(bpart)
139139
hashsum.update(bpart)
140140
hash_digest = urlsafe_b64encode(hashsum.digest()).decode('ascii').rstrip('=')

setuptools/command/editable_wheel.py

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
import shutil
1717
import sys
1818
import traceback
19-
from contextlib import suppress
19+
from contextlib import ExitStack, suppress
2020
from enum import Enum
21+
from functools import lru_cache
2122
from inspect import cleandoc
2223
from itertools import chain
2324
from pathlib import Path
@@ -33,6 +34,7 @@
3334
Tuple,
3435
TypeVar,
3536
Union,
37+
cast,
3638
)
3739

3840
from .. import (
@@ -42,6 +44,8 @@
4244
errors,
4345
namespaces,
4446
)
47+
from .._wheelbuilder import WheelBuilder
48+
from ..extern.packaging.tags import sys_tags
4549
from ..discovery import find_package_path
4650
from ..dist import Distribution
4751
from ..warnings import (
@@ -51,9 +55,6 @@
5155
)
5256
from .build_py import build_py as build_py_cls
5357

54-
if TYPE_CHECKING:
55-
from wheel.wheelfile import WheelFile # noqa
56-
5758
if sys.version_info >= (3, 8):
5859
from typing import Protocol
5960
elif TYPE_CHECKING:
@@ -63,6 +64,7 @@
6364

6465
_Path = Union[str, Path]
6566
_P = TypeVar("_P", bound=_Path)
67+
_Tag = Tuple[str, str, str]
6668
_logger = logging.getLogger(__name__)
6769

6870

@@ -117,6 +119,20 @@ def convert(cls, mode: Optional[str]) -> "_EditableMode":
117119
"""
118120

119121

122+
@lru_cache(maxsize=0)
123+
def _any_compat_tag() -> _Tag:
124+
"""
125+
PEP 660 does not require the tag to be identical to the tag that will be used
126+
in production, it only requires the tag to be compatible with the current system.
127+
Moreover, PEP 660 also guarantees that the generated wheel file should be used in
128+
the same system where it was produced.
129+
Therefore we can just be pragmatic and pick one of the compatible tags.
130+
"""
131+
tag = next(sys_tags())
132+
components = (tag.interpreter, tag.abi, tag.platform)
133+
return cast(_Tag, tuple(map(_normalization.filename_component, components)))
134+
135+
120136
class editable_wheel(Command):
121137
"""Build 'editable' wheel for development.
122138
This command is private and reserved for internal use of setuptools,
@@ -142,34 +158,34 @@ def finalize_options(self):
142158
self.project_dir = dist.src_root or os.curdir
143159
self.package_dir = dist.package_dir or {}
144160
self.dist_dir = Path(self.dist_dir or os.path.join(self.project_dir, "dist"))
161+
if self.dist_info_dir:
162+
self.dist_info_dir = Path(self.dist_info_dir)
145163

146164
def run(self):
147165
try:
148166
self.dist_dir.mkdir(exist_ok=True)
149-
self._ensure_dist_info()
150-
151-
# Add missing dist_info files
152-
self.reinitialize_command("bdist_wheel")
153-
bdist_wheel = self.get_finalized_command("bdist_wheel")
154-
bdist_wheel.write_wheelfile(self.dist_info_dir)
155-
156-
self._create_wheel_file(bdist_wheel)
167+
self._create_wheel_file()
157168
except Exception:
158169
traceback.print_exc()
159170
project = self.distribution.name or self.distribution.get_name()
160171
_DebuggingTips.emit(project=project)
161172
raise
162173

163-
def _ensure_dist_info(self):
174+
def _get_dist_info_name(self, tmp_dir):
164175
if self.dist_info_dir is None:
165176
dist_info = self.reinitialize_command("dist_info")
166-
dist_info.output_dir = self.dist_dir
177+
dist_info.output_dir = tmp_dir
167178
dist_info.ensure_finalized()
168-
dist_info.run()
169179
self.dist_info_dir = dist_info.dist_info_dir
170-
else:
171-
assert str(self.dist_info_dir).endswith(".dist-info")
172-
assert Path(self.dist_info_dir, "METADATA").exists()
180+
return dist_info.name
181+
182+
assert str(self.dist_info_dir).endswith(".dist-info")
183+
assert (self.dist_info_dir / "METADATA").exists()
184+
return self.dist_info_dir.name[: -len(".dist-info")]
185+
186+
def _ensure_dist_info(self):
187+
if not Path(self.dist_info_dir, "METADATA").exists():
188+
self.distribution.run_command("dist_info")
173189

174190
def _install_namespaces(self, installation_dir, pth_prefix):
175191
# XXX: Only required to support the deprecated namespace practice
@@ -209,8 +225,7 @@ def _configure_build(
209225
scripts = str(Path(unpacked_wheel, f"{name}.data", "scripts"))
210226

211227
# egg-info may be generated again to create a manifest (used for package data)
212-
egg_info = dist.reinitialize_command("egg_info", reinit_subcommands=True)
213-
egg_info.egg_base = str(tmp_dir)
228+
egg_info = dist.get_command_obj("egg_info")
214229
egg_info.ignore_egg_info_in_manifest = True
215230

216231
build = dist.reinitialize_command("build", reinit_subcommands=True)
@@ -322,31 +337,29 @@ def _safely_run(self, cmd_name: str):
322337
# needs work.
323338
)
324339

325-
def _create_wheel_file(self, bdist_wheel):
326-
from wheel.wheelfile import WheelFile
327-
328-
dist_info = self.get_finalized_command("dist_info")
329-
dist_name = dist_info.name
330-
tag = "-".join(bdist_wheel.get_tag())
331-
build_tag = "0.editable" # According to PEP 427 needs to start with digit
332-
archive_name = f"{dist_name}-{build_tag}-{tag}.whl"
333-
wheel_path = Path(self.dist_dir, archive_name)
334-
if wheel_path.exists():
335-
wheel_path.unlink()
336-
337-
unpacked_wheel = TemporaryDirectory(suffix=archive_name)
338-
build_lib = TemporaryDirectory(suffix=".build-lib")
339-
build_tmp = TemporaryDirectory(suffix=".build-temp")
340-
341-
with unpacked_wheel as unpacked, build_lib as lib, build_tmp as tmp:
342-
unpacked_dist_info = Path(unpacked, Path(self.dist_info_dir).name)
343-
shutil.copytree(self.dist_info_dir, unpacked_dist_info)
344-
self._install_namespaces(unpacked, dist_info.name)
340+
def _create_wheel_file(self):
341+
with ExitStack() as stack:
342+
lib = stack.enter_context(TemporaryDirectory(suffix=".build-lib"))
343+
tmp = stack.enter_context(TemporaryDirectory(suffix=".build-temp"))
344+
dist_name = self._get_dist_info_name(tmp)
345+
346+
tag = "-".join(_any_compat_tag()) # Loose tag for the sake of simplicity...
347+
build_tag = "0.editable" # According to PEP 427 needs to start with digit.
348+
archive_name = f"{dist_name}-{build_tag}-{tag}.whl"
349+
wheel_path = Path(self.dist_dir, archive_name)
350+
if wheel_path.exists():
351+
wheel_path.unlink()
352+
353+
unpacked = stack.enter_context(TemporaryDirectory(suffix=archive_name))
354+
self._install_namespaces(unpacked, dist_name)
345355
files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)
346-
strategy = self._select_strategy(dist_name, tag, lib)
347-
with strategy, WheelFile(wheel_path, "w") as wheel_obj:
348-
strategy(wheel_obj, files, mapping)
349-
wheel_obj.write_files(unpacked)
356+
357+
strategy = stack.enter_context(self._select_strategy(dist_name, tag, lib))
358+
builder = stack.enter_context(WheelBuilder(wheel_path))
359+
strategy(builder, files, mapping)
360+
builder.add_tree(unpacked, exclude=["*.dist-info/*", "*.egg-info/*"])
361+
self._ensure_dist_info()
362+
builder.add_tree(self.dist_info_dir, prefix=self.dist_info_dir.name)
350363

351364
return wheel_path
352365

@@ -384,7 +397,7 @@ def _select_strategy(
384397

385398

386399
class EditableStrategy(Protocol):
387-
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
400+
def __call__(self, wheel: WheelBuilder, files: List[str], mapping: Dict[str, str]):
388401
...
389402

390403
def __enter__(self):
@@ -400,10 +413,10 @@ def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):
400413
self.name = name
401414
self.path_entries = path_entries
402415

403-
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
416+
def __call__(self, wheel: WheelBuilder, files: List[str], mapping: Dict[str, str]):
404417
entries = "\n".join((str(p.resolve()) for p in self.path_entries))
405418
contents = _encode_pth(f"{entries}\n")
406-
wheel.writestr(f"__editable__.{self.name}.pth", contents)
419+
wheel.new_file(f"__editable__.{self.name}.pth", contents)
407420

408421
def __enter__(self):
409422
msg = f"""
@@ -440,7 +453,7 @@ def __init__(
440453
self._file = dist.get_command_obj("build_py").copy_file
441454
super().__init__(dist, name, [self.auxiliary_dir])
442455

443-
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
456+
def __call__(self, wheel: WheelBuilder, files: List[str], mapping: Dict[str, str]):
444457
self._create_links(files, mapping)
445458
super().__call__(wheel, files, mapping)
446459

@@ -492,7 +505,7 @@ def __init__(self, dist: Distribution, name: str):
492505
self.dist = dist
493506
self.name = name
494507

495-
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
508+
def __call__(self, wheel: WheelBuilder, files: List[str], mapping: Dict[str, str]):
496509
src_root = self.dist.src_root or os.curdir
497510
top_level = chain(_find_packages(self.dist), _find_top_level_modules(self.dist))
498511
package_dir = self.dist.package_dir or {}
@@ -507,11 +520,9 @@ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]
507520

508521
name = f"__editable__.{self.name}.finder"
509522
finder = _normalization.safe_identifier(name)
510-
content = bytes(_finder_template(name, roots, namespaces_), "utf-8")
511-
wheel.writestr(f"{finder}.py", content)
512-
523+
wheel.new_file(f"{finder}.py", _finder_template(name, roots, namespaces_))
513524
content = _encode_pth(f"import {finder}; {finder}.install()")
514-
wheel.writestr(f"__editable__.{self.name}.pth", content)
525+
wheel.new_file(f"__editable__.{self.name}.pth", content)
515526

516527
def __enter__(self):
517528
msg = "Editable install will be performed using a meta path finder.\n"

0 commit comments

Comments
 (0)