Skip to content

Implement PEP 660 hooks in setuptools/build_meta.py #2872

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2872.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implemented PEP 660 editable backend hooks.
111 changes: 110 additions & 1 deletion setuptools/build_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
- `prepare_metadata_for_build_wheel`: get the `install_requires`
- `build_sdist`: build an sdist in the folder and return the basename
- `get_requires_for_build_sdist`: get the `setup_requires` to build
- `build_editable`: build a wheel containing a .pth file and dist-info
metadata, and return the basename (PEP 660)
- `get_requires_for_build_wheel`: get the `setup_requires` to build
` `prepare_metadata_for_build_editable`: get the `install_requires`

Again, this is not a formal definition! Just a "taste" of the module.
"""
Expand All @@ -34,17 +38,27 @@
import contextlib
import tempfile
import warnings
import zipfile
import base64
import textwrap
import hashlib
import csv

import setuptools
import setuptools.command.egg_info
import distutils

from pkg_resources import parse_requirements
import pkg_resources
from pkg_resources import parse_requirements, safe_name, safe_version

__all__ = ['get_requires_for_build_sdist',
'get_requires_for_build_wheel',
'get_requires_for_build_editable'
'prepare_metadata_for_build_wheel',
'prepare_metadata_for_build_editable',
'build_wheel',
'build_sdist',
'build_editable',
'__legacy__',
'SetupRequirementsError']

Expand Down Expand Up @@ -76,6 +90,24 @@ def patch(cls):
distutils.core.Distribution = orig


class _egg_info_EditableShim(setuptools.command.egg_info.egg_info):
_captured_instance = None

def finalize_options(self):
super().finalize_options()
self.__class__._captured_instance = self

@classmethod
@contextlib.contextmanager
def patch(cls):
orig = setuptools.command.egg_info.egg_info
setuptools.command.egg_info.egg_info = cls
try:
yield
finally:
setuptools.command.egg_info.egg_info = orig


@contextlib.contextmanager
def no_install_setup_requires():
"""Temporarily disable installing setup_requires
Expand Down Expand Up @@ -126,6 +158,25 @@ def suppress_known_deprecation():
yield


def _urlsafe_b64encode(data):
"""urlsafe_b64encode without padding"""
return base64.urlsafe_b64encode(data).rstrip(b"=")


def _add_wheel_record(archive, dist_info):
"""Add the wheel RECORD manifest."""
buffer = io.StringIO()
writer = csv.writer(buffer, delimiter=',', quotechar='"', lineterminator='\n')
for f in archive.namelist():
data = archive.read(f)
size = len(data)
digest = hashlib.sha256(data).digest()
digest = "sha256=" + (_urlsafe_b64encode(digest).decode("ascii"))
writer.writerow((f, digest, size))
record_path = os.path.join(dist_info, "RECORD")
archive.writestr(record_path, buffer.read())


class _BuildMetaBackend(object):

def _fix_config(self, config_settings):
Expand Down Expand Up @@ -235,6 +286,59 @@ def build_sdist(self, sdist_directory, config_settings=None):
'.tar.gz', sdist_directory,
config_settings)

def build_editable(self, wheel_directory, config_settings=None,
metadata_directory=None):
config_settings = self._fix_config(config_settings)

sys.argv = [*sys.argv[:1], 'dist_info']
with no_install_setup_requires(), _egg_info_EditableShim.patch():
self.run_setup()
# HACK: to get the distribution's location we'll have to capture the
# egg_info instance created by dist_info. It'd be even more difficult
# to statically recalcuate the location (i.e. the proper way) AFAICT.
egg_info = _egg_info_EditableShim._captured_instance
dist_info = egg_info.egg_name + '.dist-info'
dist_info_path = os.path.join(os.getcwd(), egg_info.egg_info)
dist_info_path = dist_info_path[:-len('.egg-info')] + '.dist-info'
location = os.path.join(os.getcwd(), egg_info.egg_base)

sys.argv = [
*sys.argv[:1], 'build_ext', '--inplace',
*config_settings['--global-option']
]
with no_install_setup_requires():
self.run_setup()

metadata = pkg_resources.PathMetadata(location, dist_info_path)
dist = pkg_resources.DistInfoDistribution.from_location(
location, dist_info, metadata=metadata
)
# Seems like the distribution name and version attributes aren't always
# 100% normalized ...
dist_name = safe_name(dist.project_name).replace("-", "_")
version = safe_version(dist.version).replace("-", "_")
wheel_name = f'{dist_name}-{version}-ed.py3-none-any.whl'
wheel_path = os.path.join(wheel_directory, wheel_name)
with zipfile.ZipFile(wheel_path, 'w') as archive:
archive.writestr(f'{dist.project_name}.pth', location)
for file in os.scandir(dist_info_path):
with open(file.path, encoding='utf-8') as metadata:
zip_filename = os.path.relpath(file.path, location)
archive.writestr(zip_filename, metadata.read())

archive.writestr(
os.path.join(dist_info, 'WHEEL'),
textwrap.dedent(f"""\
Wheel-Version: 1.0
Generator: setuptools ({setuptools.__version__})
Root-Is-Purelib: false
Tag: ed.py3-none-any
""")
)
_add_wheel_record(archive, dist_info)

return os.path.basename(wheel_path)


class _BuildMetaLegacyBackend(_BuildMetaBackend):
"""Compatibility backend for setuptools
Expand Down Expand Up @@ -281,9 +385,14 @@ def run_setup(self, setup_script='setup.py'):

get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel
get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist
# Fortunately we can just reuse the wheel hook for editables in this case.
get_requires_for_build_editable = _BACKEND.get_requires_for_build_wheel
prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel
# Ditto reuse of wheel's equivalent.
prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_wheel
build_wheel = _BACKEND.build_wheel
build_sdist = _BACKEND.build_sdist
build_editable = _BACKEND.build_editable


# The legacy backend
Expand Down