summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnderson Bravalheri <andersonbravalheri@gmail.com>2022-06-15 15:44:57 +0100
committerAnderson Bravalheri <andersonbravalheri@gmail.com>2022-06-15 16:43:46 +0100
commit0bf1d4c26261fdd128709075787d26182f9a12ca (patch)
treeb2383cfe3bcd2a961da18452d94e3436beb345d0
parentc64252975a77ede171bc1f6b9aa4f1ce197643d8 (diff)
parent33f24313116e38d6cc30009fcad55871b5d9a6ee (diff)
downloadpython-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.txt1
-rw-r--r--setuptools/build_meta.py18
-rw-r--r--setuptools/command/editable_wheel.py184
-rw-r--r--setuptools/tests/test_editable_install.py18
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)