Skip to content

Commit 4822477

Browse files
committed
Avoid using bdist_wheel in editable_wheel
1 parent 7f05c32 commit 4822477

File tree

1 file changed

+74
-60
lines changed

1 file changed

+74
-60
lines changed

setuptools/command/editable_wheel.py

Lines changed: 74 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
import shutil
1616
import sys
1717
import traceback
18-
from contextlib import suppress
18+
from contextlib import ExitStack, suppress
1919
from enum import Enum
20+
from functools import lru_cache
2021
from inspect import cleandoc
2122
from itertools import chain
2223
from pathlib import Path
@@ -32,6 +33,7 @@
3233
Tuple,
3334
TypeVar,
3435
Union,
36+
cast,
3537
)
3638

3739
from .. import (
@@ -41,6 +43,8 @@
4143
errors,
4244
namespaces,
4345
)
46+
from .._wheelbuilder import WheelBuilder
47+
from ..extern.packaging.tags import sys_tags
4448
from ..discovery import find_package_path
4549
from ..dist import Distribution
4650
from ..warnings import (
@@ -50,9 +54,6 @@
5054
)
5155
from .build_py import build_py as build_py_cls
5256

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

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

6769

@@ -116,6 +118,20 @@ def convert(cls, mode: Optional[str]) -> "_EditableMode":
116118
"""
117119

118120

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

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

162-
def _ensure_dist_info(self):
173+
def _get_dist_info_name(self, tmp_dir):
163174
if self.dist_info_dir is None:
164175
dist_info = self.reinitialize_command("dist_info")
165-
dist_info.output_dir = self.dist_dir
176+
dist_info.output_dir = tmp_dir
166177
dist_info.ensure_finalized()
167-
dist_info.run()
168178
self.dist_info_dir = dist_info.dist_info_dir
169-
else:
170-
assert str(self.dist_info_dir).endswith(".dist-info")
171-
assert Path(self.dist_info_dir, "METADATA").exists()
179+
return dist_info.name
180+
181+
assert str(self.dist_info_dir).endswith(".dist-info")
182+
assert (self.dist_info_dir / "METADATA").exists()
183+
return self.dist_info_dir.name[: -len(".dist-info")]
184+
185+
def _ensure_dist_info(self):
186+
if not Path(self.dist_info_dir, "METADATA").exists():
187+
self.distribution.run_command("dist_info")
172188

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

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

215230
build = dist.reinitialize_command("build", reinit_subcommands=True)
@@ -321,31 +336,29 @@ def _safely_run(self, cmd_name: str):
321336
# needs work.
322337
)
323338

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

350363
return wheel_path
351364

@@ -383,7 +396,7 @@ def _select_strategy(
383396

384397

385398
class EditableStrategy(Protocol):
386-
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
399+
def __call__(self, wheel: WheelBuilder, files: List[str], mapping: Dict[str, str]):
387400
...
388401

389402
def __enter__(self):
@@ -399,10 +412,9 @@ def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):
399412
self.name = name
400413
self.path_entries = path_entries
401414

402-
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
415+
def __call__(self, wheel: WheelBuilder, files: List[str], mapping: Dict[str, str]):
403416
entries = "\n".join((str(p.resolve()) for p in self.path_entries))
404-
contents = bytes(f"{entries}\n", "utf-8")
405-
wheel.writestr(f"__editable__.{self.name}.pth", contents)
417+
wheel.new_file(f"__editable__.{self.name}.pth", f"{entries}\n")
406418

407419
def __enter__(self):
408420
msg = f"""
@@ -426,8 +438,10 @@ class _LinkTree(_StaticPth):
426438
By collocating ``auxiliary_dir`` and the original source code, limitations
427439
with hardlinks should be avoided.
428440
"""
441+
429442
def __init__(
430-
self, dist: Distribution,
443+
self,
444+
dist: Distribution,
431445
name: str,
432446
auxiliary_dir: _Path,
433447
build_lib: _Path,
@@ -437,7 +451,7 @@ def __init__(
437451
self._file = dist.get_command_obj("build_py").copy_file
438452
super().__init__(dist, name, [self.auxiliary_dir])
439453

440-
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
454+
def __call__(self, wheel: WheelBuilder, files: List[str], mapping: Dict[str, str]):
441455
self._create_links(files, mapping)
442456
super().__call__(wheel, files, mapping)
443457

@@ -492,24 +506,24 @@ def __init__(self, dist: Distribution, name: str):
492506
self.dist = dist
493507
self.name = name
494508

495-
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
509+
def __call__(self, wheel: WheelBuilder, files: List[str], mapping: Dict[str, str]):
496510
src_root = self.dist.src_root or os.curdir
497511
top_level = chain(_find_packages(self.dist), _find_top_level_modules(self.dist))
498512
package_dir = self.dist.package_dir or {}
499513
roots = _find_package_roots(top_level, package_dir, src_root)
500514

501-
namespaces_: Dict[str, List[str]] = dict(chain(
502-
_find_namespaces(self.dist.packages or [], roots),
503-
((ns, []) for ns in _find_virtual_namespaces(roots)),
504-
))
515+
namespaces_: Dict[str, List[str]] = dict(
516+
chain(
517+
_find_namespaces(self.dist.packages or [], roots),
518+
((ns, []) for ns in _find_virtual_namespaces(roots)),
519+
)
520+
)
505521

506522
name = f"__editable__.{self.name}.finder"
507523
finder = _normalization.safe_identifier(name)
508-
content = bytes(_finder_template(name, roots, namespaces_), "utf-8")
509-
wheel.writestr(f"{finder}.py", content)
510-
511-
content = bytes(f"import {finder}; {finder}.install()", "utf-8")
512-
wheel.writestr(f"__editable__.{self.name}.pth", content)
524+
wheel.new_file(f"{finder}.py", _finder_template(name, roots, namespaces_))
525+
pth = f"__editable__.{self.name}.pth"
526+
wheel.new_file(pth, f"import {finder}; {finder}.install()")
513527

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

0 commit comments

Comments
 (0)