diff options
| author | Anderson Bravalheri <andersonbravalheri@gmail.com> | 2022-06-15 15:44:57 +0100 |
|---|---|---|
| committer | Anderson Bravalheri <andersonbravalheri@gmail.com> | 2022-06-15 16:43:46 +0100 |
| commit | 0bf1d4c26261fdd128709075787d26182f9a12ca (patch) | |
| tree | b2383cfe3bcd2a961da18452d94e3436beb345d0 | |
| parent | c64252975a77ede171bc1f6b9aa4f1ce197643d8 (diff) | |
| parent | 33f24313116e38d6cc30009fcad55871b5d9a6ee (diff) | |
| download | python-setuptools-git-0bf1d4c26261fdd128709075787d26182f9a12ca.tar.gz | |
Rework PEP 660 PoC to re-use bdist_wheel
Avoid using the editables dependency
- The current implementation is using it to simply place .pth file
pointing to the project directory anyway...
- Adding a dependency for creating a file with a single line is a bit
overkill.
Avoid importing pkg_resources directly
- Setuptools wants to move away from pkg_resources
Replace custom wheel build with re-use of bdist_wheel
- pro: avoid re-implementing the archiving logic and make sure it is
compatible with the same archive format used by the final wheels.
- con: the API of wheel is not exactly stable or exported as public.
| -rw-r--r-- | bootstrap.egg-info/entry_points.txt | 1 | ||||
| -rw-r--r-- | setuptools/build_meta.py | 18 | ||||
| -rw-r--r-- | setuptools/command/editable_wheel.py | 184 | ||||
| -rw-r--r-- | setuptools/tests/test_editable_install.py | 18 |
4 files changed, 84 insertions, 137 deletions
diff --git a/bootstrap.egg-info/entry_points.txt b/bootstrap.egg-info/entry_points.txt index c00d1d3a..a21ca227 100644 --- a/bootstrap.egg-info/entry_points.txt +++ b/bootstrap.egg-info/entry_points.txt @@ -2,6 +2,7 @@ egg_info = setuptools.command.egg_info:egg_info build_py = setuptools.command.build_py:build_py sdist = setuptools.command.sdist:sdist +editable_wheel = setuptools.command.editable_wheel:editable_wheel [distutils.setup_keywords] include_package_data = setuptools.dist:assert_bool diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 66e2602f..1d67e756 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -251,22 +251,27 @@ class _BuildMetaBackend: '.tar.gz', sdist_directory, config_settings) - # PEP660 hooks: # build_editable # get_requires_for_build_editable # prepare_metadata_for_build_editable def build_editable( - self, wheel_directory, scheme=None, config_settings=None + self, wheel_directory, config_settings=None, metadata_directory=None ): # XXX can or should we hide our editable_wheel command normally? return self._build_with_temp_dir( - ["editable_wheel"], ".whl", wheel_directory, config_settings + ["editable_wheel", "--dist-info-dir", metadata_directory], + ".whl", wheel_directory, config_settings ) - def get_requires_for_build_editable(self, config_settings=None): - return ['editables', 'wheel'] + return self.get_requires_for_build_wheel(config_settings) + + def prepare_metadata_for_build_editable(self, metadata_directory, + config_settings=None): + return self.prepare_metadata_for_build_wheel( + metadata_directory, config_settings + ) class _BuildMetaLegacyBackend(_BuildMetaBackend): @@ -314,8 +319,9 @@ _BACKEND = _BuildMetaBackend() get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist -get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable +get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel +prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable build_wheel = _BACKEND.build_wheel build_sdist = _BACKEND.build_sdist build_editable = _BACKEND.build_editable diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index c827efa3..d44d216f 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -4,18 +4,12 @@ Create a wheel that, when installed, will make the source package 'editable' 'setup.py develop'. Based on the setuptools develop command. """ -# TODO doesn't behave when called outside the hook - import os -import time -from pathlib import Path - +import shutil +import sys from distutils.core import Command -from distutils.errors import DistutilsError - -import pkg_resources - -SOURCE_EPOCH_ZIP = 499162860 +from pathlib import Path +from tempfile import TemporaryDirectory class editable_wheel(Command): @@ -25,133 +19,79 @@ class editable_wheel(Command): user_options = [ ("dist-dir=", "d", "directory to put final built distributions in"), + ("dist-info-dir=", "I", "path to a pre-build .dist-info directory"), ] boolean_options = [] - def run(self): - self.build_editable_wheel() - def initialize_options(self): self.dist_dir = None + self.dist_info_dir = None + self.project_dir = None def finalize_options(self): - # is this part of the 'develop' command needed? - ei = self.get_finalized_command("egg_info") - if ei.broken_egg_info: - template = "Please rename %r to %r before using 'develop'" - args = ei.egg_info, ei.broken_egg_info - raise DistutilsError(template % args) - self.args = [ei.egg_name] - - # the .pth file should point to target - self.egg_base = ei.egg_base - self.target = pkg_resources.normalize_path(self.egg_base) - self.dist_info_dir = Path( - (ei.egg_info[: -len(".egg-info")] + ".dist-info").rpartition("/")[-1] - ) - - def build_editable_wheel(self): - if getattr(self.distribution, "use_2to3", False): - raise NotImplementedError("2to3 not supported") - - di = self.get_finalized_command("dist_info") - di.egg_base = self.dist_dir - di.finalize_options() - self.run_command("dist_info") + dist = self.distribution + self.project_dir = dist.src_root or os.curdir + self.dist_dir = Path(self.dist_dir or os.path.join(self.project_dir, "dist")) + self.dist_dir.mkdir(exist_ok=True) + + @property + def target(self): + package_dir = self.distribution.package_dir or {} + return package_dir.get("") or self.project_dir + + def run(self): + self._ensure_dist_info() + + # Add missing dist_info files + bdist_wheel = self.reinitialize_command("bdist_wheel") + bdist_wheel.write_wheelfile(self.dist_info_dir) # Build extensions in-place self.reinitialize_command("build_ext", inplace=1) self.run_command("build_ext") - # now build the wheel - # with the dist-info directory and .pth from 'editables' library - # ... - - import zipfile - import editables # could we use 'develop' command's .pth file + self._create_wheel_file(bdist_wheel) + + def _ensure_dist_info(self): + if self.dist_info_dir is None: + dist_info = self.reinitialize_command("dist_info") + dist_info.output_dir = self.dist_dir + dist_info.finalize_options() + dist_info.run() + self.dist_info_dir = dist_info.dist_info_dir + else: + assert str(self.dist_info_dir).endswith(".dist-info") + assert Path(self.dist_info_dir, "METADATA").exists() + + def _create_wheel_file(self, bdist_wheel): + from wheel.wheelfile import WheelFile + + dist_info = self.get_finalized_command("dist_info") + tag = "-".join(bdist_wheel.get_tag()) + editable_name = dist_info.name + build_tag = "0.editable" # According to PEP 427 needs to start with digit + archive_name = f"{editable_name}-{build_tag}-{tag}.whl" + wheel_path = Path(self.dist_dir, archive_name) + if wheel_path.exists(): + wheel_path.unlink() - project = editables.EditableProject( - self.distribution.metadata.name, self.target - ) - project.add_to_path(self.target) + # Currently the wheel API receives a directory and dump all its contents + # inside of a wheel. So let's use a temporary directory. + with TemporaryDirectory(suffix=archive_name) as tmp: + tmp_dist_info = Path(tmp, Path(self.dist_info_dir).name) + shutil.copytree(self.dist_info_dir, tmp_dist_info) + pth = Path(tmp, f"_editable.{editable_name}.pth") + pth.write_text(f"{_normalize_path(self.target)}\n", encoding="utf-8") - dist_dir = Path(self.dist_dir) - dist_info_dir = self.dist_info_dir - fullname = self.distribution.metadata.get_fullname() - # superfluous 'ed' tag is only a hint to the user, - # and guarantees we can't overwrite the normal wheel - wheel_name = f"{fullname}-ed.py3-none-any.whl" - wheel_path = dist_dir / wheel_name + with WheelFile(wheel_path, "w") as wf: + wf.write_files(tmp) - wheelmeta_builder(dist_dir / dist_info_dir / "WHEEL") + return wheel_path - if wheel_path.exists(): - wheel_path.unlink() - with zipfile.ZipFile( - wheel_path, "a", compression=zipfile.ZIP_DEFLATED - ) as archive: - - # copy .pth file - for f, data in project.files(): - archive.writestr( - zipfile.ZipInfo(f, time.gmtime(SOURCE_EPOCH_ZIP)[:6]), data - ) - - # copy .dist-info directory - for f in sorted(os.listdir(dist_dir / dist_info_dir)): - with (dist_dir / dist_info_dir / f).open() as metadata: - archive.writestr( - zipfile.ZipInfo( - str(dist_info_dir / f), time.gmtime(SOURCE_EPOCH_ZIP)[:6] - ), - metadata.read(), - ) - - add_manifest(archive, dist_info_dir) - - -import base64 - - -def urlsafe_b64encode(data): - """urlsafe_b64encode without padding""" - return base64.urlsafe_b64encode(data).rstrip(b"=") - - -# standalone wheel helpers based on enscons -def add_manifest(archive, dist_info_dir): - """ - Add the wheel manifest. - """ - import hashlib - import zipfile - - lines = [] - for f in archive.namelist(): - data = archive.read(f) - size = len(data) - digest = hashlib.sha256(data).digest() - digest = "sha256=" + (urlsafe_b64encode(digest).decode("ascii")) - lines.append("%s,%s,%s" % (f.replace(",", ",,"), digest, size)) - - record_path = dist_info_dir / "RECORD" - lines.append(str(record_path) + ",,") - RECORD = "\n".join(lines) - archive.writestr( - zipfile.ZipInfo(str(record_path), time.gmtime(SOURCE_EPOCH_ZIP)[:6]), RECORD - ) - archive.close() - - -def wheelmeta_builder(target): - with open(target, "w+") as f: - f.write( - """Wheel-Version: 1.0 -Generator: setuptools_pep660 (0.1) -Root-Is-Purelib: false -Tag: py3-none-any -Tag: ed-none-any -""" - ) +def _normalize_path(filename): + """Normalize a file/dir name for comparison purposes""" + # See pkg_resources.normalize_path + file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename + return os.path.normcase(os.path.realpath(os.path.normpath(file))) diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py index aac4f5ee..0d4980d3 100644 --- a/setuptools/tests/test_editable_install.py +++ b/setuptools/tests/test_editable_install.py @@ -3,6 +3,7 @@ from textwrap import dedent import pytest import jaraco.envs +import jaraco.path import path @@ -85,18 +86,17 @@ EXAMPLE = { SETUP_SCRIPT_STUB = "__import__('setuptools').setup()" -MISSING_SETUP_SCRIPT = pytest.param( - None, - marks=pytest.mark.xfail( - reason="Editable install is currently only supported with `setup.py`" - ) -) -@pytest.mark.parametrize("setup_script", [SETUP_SCRIPT_STUB, MISSING_SETUP_SCRIPT]) -def test_editable_with_pyproject(tmp_path, venv, setup_script): +@pytest.mark.parametrize( + "files", + [ + {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB}, + EXAMPLE, # No setup.py script + ] +) +def test_editable_with_pyproject(tmp_path, venv, files): project = tmp_path / "mypkg" - files = {**EXAMPLE, "setup.py": setup_script} project.mkdir() jaraco.path.build(files, prefix=project) |
