Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ __pycache__/
.pytest_cache
doc/_build/
*.egg-info/
.coverage
htmlcov/
7 changes: 5 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ API to call PEP 517 hooks
`PEP 517 <https://www.python.org/dev/peps/pep-0517/>`_ specifies a standard
API for systems which build Python packages.

This package contains wrappers around the hooks specified by PEP 517. It
provides:
`PEP 660 <https://www.python.org/dev/peps/pep-0660/>`_ extends it with a build
mode that leads to editable installs.

This package contains wrappers around the hooks specified by PEP 517 and
PEP 660. It provides:

- A mechanism to call the hooks in a subprocess, so they are isolated from
the current process.
Expand Down
6 changes: 6 additions & 0 deletions doc/callhooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ Calling the build system

.. automethod:: get_requires_for_build_wheel

.. automethod:: get_requires_for_build_editable

.. automethod:: prepare_metadata_for_build_wheel

.. automethod:: prepare_metadata_for_build_editable

.. automethod:: build_sdist

.. automethod:: build_wheel

.. automethod:: build_editable

.. automethod:: subprocess_runner

Subprocess runners
Expand Down
1 change: 1 addition & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
0.11
----

- Support editable hooks (`PEP 660 <https://www.python.org/dev/peps/pep-0660/>`_).
- Use the TOML 1.0 compliant ``tomli`` parser module on Python 3.6 and above.
- Ensure TOML files are always read as UTF-8.
- Switch CI to Github actions.
Expand Down
79 changes: 74 additions & 5 deletions pep517/in_process/_in_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ def __init__(self, message):

class HookMissing(Exception):
"""Raised if a hook is missing and we are not executing the fallback"""
def __init__(self, hook_name=None):
super(HookMissing, self).__init__(hook_name)
self.hook_name = hook_name


def contained_in(filename, directory):
Expand Down Expand Up @@ -114,6 +117,20 @@ def get_requires_for_build_wheel(config_settings):
return hook(config_settings)


def get_requires_for_build_editable(config_settings):
"""Invoke the optional get_requires_for_build_editable hook

Returns [] if the hook is not defined.
"""
backend = _build_backend()
try:
hook = backend.get_requires_for_build_editable
except AttributeError:
return []
else:
return hook(config_settings)


def prepare_metadata_for_build_wheel(
metadata_directory, config_settings, _allow_fallback):
"""Invoke optional prepare_metadata_for_build_wheel
Expand All @@ -127,12 +144,40 @@ def prepare_metadata_for_build_wheel(
except AttributeError:
if not _allow_fallback:
raise HookMissing()
return _get_wheel_metadata_from_wheel(backend, metadata_directory,
whl_basename = backend.build_wheel(metadata_directory, config_settings)
return _get_wheel_metadata_from_wheel(whl_basename, metadata_directory,
config_settings)
else:
return hook(metadata_directory, config_settings)


def prepare_metadata_for_build_editable(
metadata_directory, config_settings, _allow_fallback):
"""Invoke optional prepare_metadata_for_build_editable

Implements a fallback by building an editable wheel if the hook isn't
defined, unless _allow_fallback is False in which case HookMissing is
raised.
"""
backend = _build_backend()
try:
hook = backend.prepare_metadata_for_build_editable
except AttributeError:
if not _allow_fallback:
raise HookMissing()
try:
build_hook = backend.build_editable
except AttributeError:
raise HookMissing(hook_name='build_editable')
else:
whl_basename = build_hook(metadata_directory, config_settings)
return _get_wheel_metadata_from_wheel(whl_basename,
metadata_directory,
config_settings)
else:
return hook(metadata_directory, config_settings)


WHEEL_BUILT_MARKER = 'PEP517_ALREADY_BUILT_WHEEL'


Expand All @@ -149,14 +194,13 @@ def _dist_info_files(whl_zip):


def _get_wheel_metadata_from_wheel(
backend, metadata_directory, config_settings):
"""Build a wheel and extract the metadata from it.
whl_basename, metadata_directory, config_settings):
"""Extract the metadata from a wheel.

Fallback for when the build backend does not
define the 'get_wheel_metadata' hook.
"""
from zipfile import ZipFile
whl_basename = backend.build_wheel(metadata_directory, config_settings)
with open(os.path.join(metadata_directory, WHEEL_BUILT_MARKER), 'wb'):
pass # Touch marker file

Expand Down Expand Up @@ -205,6 +249,27 @@ def build_wheel(wheel_directory, config_settings, metadata_directory=None):
metadata_directory)


def build_editable(wheel_directory, config_settings, metadata_directory=None):
"""Invoke the optional build_editable hook.

If a wheel was already built in the
prepare_metadata_for_build_editable fallback, this
will copy it rather than rebuilding the wheel.
"""
backend = _build_backend()
try:
hook = backend.build_editable
except AttributeError:
raise HookMissing()
else:
prebuilt_whl = _find_already_built_wheel(metadata_directory)
if prebuilt_whl:
shutil.copy2(prebuilt_whl, wheel_directory)
return os.path.basename(prebuilt_whl)

return hook(wheel_directory, config_settings, metadata_directory)


def get_requires_for_build_sdist(config_settings):
"""Invoke the optional get_requires_for_build_wheel hook

Expand Down Expand Up @@ -242,6 +307,9 @@ def build_sdist(sdist_directory, config_settings):
'get_requires_for_build_wheel',
'prepare_metadata_for_build_wheel',
'build_wheel',
'get_requires_for_build_editable',
'prepare_metadata_for_build_editable',
'build_editable',
'get_requires_for_build_sdist',
'build_sdist',
}
Expand Down Expand Up @@ -270,8 +338,9 @@ def main():
except GotUnsupportedOperation as e:
json_out['unsupported'] = True
json_out['traceback'] = e.traceback
except HookMissing:
except HookMissing as e:
json_out['hook_missing'] = True
json_out['missing_hook_name'] = e.hook_name or hook_name

write_json(json_out, pjoin(control_dir, 'output.json'), indent=2)

Expand Down
55 changes: 54 additions & 1 deletion pep517/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,59 @@ def build_wheel(
'metadata_directory': metadata_directory,
})

def get_requires_for_build_editable(self, config_settings=None):
"""Identify packages required for building an editable wheel

Returns a list of dependency specifications, e.g.::

["wheel >= 0.25", "setuptools"]

This does not include requirements specified in pyproject.toml.
It returns the result of calling the equivalently named hook in a
subprocess.
"""
return self._call_hook('get_requires_for_build_editable', {
'config_settings': config_settings
})

def prepare_metadata_for_build_editable(
self, metadata_directory, config_settings=None,
_allow_fallback=True):
"""Prepare a ``*.dist-info`` folder with metadata for this project.

Returns the name of the newly created folder.

If the build backend defines a hook with this name, it will be called
in a subprocess. If not, the backend will be asked to build an editable
wheel, and the dist-info extracted from that (unless _allow_fallback is
False).
"""
return self._call_hook('prepare_metadata_for_build_editable', {
'metadata_directory': abspath(metadata_directory),
'config_settings': config_settings,
'_allow_fallback': _allow_fallback,
})

def build_editable(
self, wheel_directory, config_settings=None,
metadata_directory=None):
"""Build an editable wheel from this project.

Returns the name of the newly created file.

In general, this will call the 'build_editable' hook in the backend.
However, if that was previously called by
'prepare_metadata_for_build_editable', and the same metadata_directory
is used, the previously built wheel will be copied to wheel_directory.
"""
if metadata_directory is not None:
metadata_directory = abspath(metadata_directory)
return self._call_hook('build_editable', {
'wheel_directory': abspath(wheel_directory),
'config_settings': config_settings,
'metadata_directory': metadata_directory,
})

def get_requires_for_build_sdist(self, config_settings=None):
"""Identify packages required for building a wheel

Expand Down Expand Up @@ -280,7 +333,7 @@ def _call_hook(self, hook_name, kwargs):
message=data.get('backend_error', '')
)
if data.get('hook_missing'):
raise HookMissing(hook_name)
raise HookMissing(data.get('missing_hook_name') or hook_name)
return data['return_val']


Expand Down
10 changes: 10 additions & 0 deletions tests/samples/buildsys_pkgs/buildsys.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ def get_requires_for_build_wheel(config_settings):
return ['wheelwright']


def get_requires_for_build_editable(config_settings):
return ['wheelwright', 'editables']


def prepare_metadata_for_build_wheel(metadata_directory, config_settings):
for distinfo in glob('*.dist-info'):
shutil.copytree(distinfo, pjoin(metadata_directory, distinfo))


prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel


def prepare_build_wheel_files(build_directory, config_settings):
shutil.copy('pyproject.toml', build_directory)
for pyfile in glob('*.py'):
Expand All @@ -37,6 +44,9 @@ def build_wheel(wheel_directory, config_settings, metadata_directory=None):
return whl_file


build_editable = build_wheel


def get_requires_for_build_sdist(config_settings):
return ['frog']

Expand Down
4 changes: 4 additions & 0 deletions tests/samples/buildsys_pkgs/buildsys_minimal_editable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from buildsys_minimal import build_sdist, build_wheel # noqa


build_editable = build_wheel
21 changes: 21 additions & 0 deletions tests/samples/pkg3/pkg3-0.5.dist-info/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2017 Thomas Kluyver

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
9 changes: 9 additions & 0 deletions tests/samples/pkg3/pkg3-0.5.dist-info/METADATA
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Metadata-Version: 1.2
Name: pkg3
Version: 0.5
Summary: Sample package for tests
Home-page: https://github.com/takluyver/pkg3
License: UNKNOWN
Author: Thomas Kluyver
Author-email: [email protected]
Classifier: License :: OSI Approved :: MIT License
5 changes: 5 additions & 0 deletions tests/samples/pkg3/pkg3-0.5.dist-info/RECORD
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pkg3.py,sha256=ZawKBtrxtdGEheOCWvwzGZsO8Q1OSzEzecGNsRz-ekc,52
pkg3-0.5.dist-info/LICENSE,sha256=GyKwSbUmfW38I6Z79KhNjsBLn9-xpR02DkK0NCyLQVQ,1081
pkg3-0.5.dist-info/WHEEL,sha256=jxKvNaDKHDacpaLi69-vnLKkBSynwBzmMS82pipt1T0,100
pkg3-0.5.dist-info/METADATA,sha256=4zQxJqc4Rvnlf5Y-seXnRx8g-1FK-sjTuS0A1KP0ajk,251
pkg3-0.5.dist-info/RECORD,,
5 changes: 5 additions & 0 deletions tests/samples/pkg3/pkg3-0.5.dist-info/WHEEL
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: flit 0.11.3
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any
3 changes: 3 additions & 0 deletions tests/samples/pkg3/pkg3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Sample package for tests"""

__version__ = '0.5'
3 changes: 3 additions & 0 deletions tests/samples/pkg3/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = []
build-backend = "buildsys_minimal_editable"
30 changes: 30 additions & 0 deletions tests/test_call_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ def test_get_requires_for_build_wheel():
assert res == ['wheelwright']


def test_get_requires_for_build_editable():
hooks = get_hooks('pkg1')
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
res = hooks.get_requires_for_build_editable({})
assert res == ['wheelwright', 'editables']


def test_get_requires_for_build_sdist():
hooks = get_hooks('pkg1')
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
Expand All @@ -66,6 +73,15 @@ def test_prepare_metadata_for_build_wheel():
assert_isfile(pjoin(metadatadir, 'pkg1-0.5.dist-info', 'METADATA'))


def test_prepare_metadata_for_build_editable():
hooks = get_hooks('pkg1')
with TemporaryDirectory() as metadatadir:
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
hooks.prepare_metadata_for_build_editable(metadatadir, {})

assert_isfile(pjoin(metadatadir, 'pkg1-0.5.dist-info', 'METADATA'))


def test_build_wheel():
hooks = get_hooks('pkg1')
with TemporaryDirectory() as builddir:
Expand All @@ -80,6 +96,20 @@ def test_build_wheel():
assert zipfile.is_zipfile(whl_file)


def test_build_editable():
hooks = get_hooks('pkg1')
with TemporaryDirectory() as builddir:
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
whl_file = hooks.build_editable(builddir, {})

assert whl_file.endswith('.whl')
assert os.sep not in whl_file

whl_file = pjoin(builddir, whl_file)
assert_isfile(whl_file)
assert zipfile.is_zipfile(whl_file)


def test_build_wheel_relpath():
hooks = get_hooks('pkg1')
with TemporaryWorkingDirectory() as builddir:
Expand Down
Loading