Skip to content

Commit e97bd1d

Browse files
authored
Merge pull request #119 from sbidoul/add-pep660-sbi
Add PEP 660 support
2 parents caec5e5 + e5725c2 commit e97bd1d

16 files changed

+276
-8
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ __pycache__/
55
.pytest_cache
66
doc/_build/
77
*.egg-info/
8+
.coverage
9+
htmlcov/

README.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ API to call PEP 517 hooks
44
`PEP 517 <https://www.python.org/dev/peps/pep-0517/>`_ specifies a standard
55
API for systems which build Python packages.
66

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

1013
- A mechanism to call the hooks in a subprocess, so they are isolated from
1114
the current process.

doc/callhooks.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@ Calling the build system
1212

1313
.. automethod:: get_requires_for_build_wheel
1414

15+
.. automethod:: get_requires_for_build_editable
16+
1517
.. automethod:: prepare_metadata_for_build_wheel
1618

19+
.. automethod:: prepare_metadata_for_build_editable
20+
1721
.. automethod:: build_sdist
1822

1923
.. automethod:: build_wheel
2024

25+
.. automethod:: build_editable
26+
2127
.. automethod:: subprocess_runner
2228

2329
Subprocess runners

doc/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Changelog
44
0.11
55
----
66

7+
- Support editable hooks (`PEP 660 <https://www.python.org/dev/peps/pep-0660/>`_).
78
- Use the TOML 1.0 compliant ``tomli`` parser module on Python 3.6 and above.
89
- Ensure TOML files are always read as UTF-8.
910
- Switch CI to Github actions.

pep517/in_process/_in_process.py

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def __init__(self, message):
6363

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

6770

6871
def contained_in(filename, directory):
@@ -114,6 +117,20 @@ def get_requires_for_build_wheel(config_settings):
114117
return hook(config_settings)
115118

116119

120+
def get_requires_for_build_editable(config_settings):
121+
"""Invoke the optional get_requires_for_build_editable hook
122+
123+
Returns [] if the hook is not defined.
124+
"""
125+
backend = _build_backend()
126+
try:
127+
hook = backend.get_requires_for_build_editable
128+
except AttributeError:
129+
return []
130+
else:
131+
return hook(config_settings)
132+
133+
117134
def prepare_metadata_for_build_wheel(
118135
metadata_directory, config_settings, _allow_fallback):
119136
"""Invoke optional prepare_metadata_for_build_wheel
@@ -127,12 +144,40 @@ def prepare_metadata_for_build_wheel(
127144
except AttributeError:
128145
if not _allow_fallback:
129146
raise HookMissing()
130-
return _get_wheel_metadata_from_wheel(backend, metadata_directory,
147+
whl_basename = backend.build_wheel(metadata_directory, config_settings)
148+
return _get_wheel_metadata_from_wheel(whl_basename, metadata_directory,
131149
config_settings)
132150
else:
133151
return hook(metadata_directory, config_settings)
134152

135153

154+
def prepare_metadata_for_build_editable(
155+
metadata_directory, config_settings, _allow_fallback):
156+
"""Invoke optional prepare_metadata_for_build_editable
157+
158+
Implements a fallback by building an editable wheel if the hook isn't
159+
defined, unless _allow_fallback is False in which case HookMissing is
160+
raised.
161+
"""
162+
backend = _build_backend()
163+
try:
164+
hook = backend.prepare_metadata_for_build_editable
165+
except AttributeError:
166+
if not _allow_fallback:
167+
raise HookMissing()
168+
try:
169+
build_hook = backend.build_editable
170+
except AttributeError:
171+
raise HookMissing(hook_name='build_editable')
172+
else:
173+
whl_basename = build_hook(metadata_directory, config_settings)
174+
return _get_wheel_metadata_from_wheel(whl_basename,
175+
metadata_directory,
176+
config_settings)
177+
else:
178+
return hook(metadata_directory, config_settings)
179+
180+
136181
WHEEL_BUILT_MARKER = 'PEP517_ALREADY_BUILT_WHEEL'
137182

138183

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

150195

151196
def _get_wheel_metadata_from_wheel(
152-
backend, metadata_directory, config_settings):
153-
"""Build a wheel and extract the metadata from it.
197+
whl_basename, metadata_directory, config_settings):
198+
"""Extract the metadata from a wheel.
154199
155200
Fallback for when the build backend does not
156201
define the 'get_wheel_metadata' hook.
157202
"""
158203
from zipfile import ZipFile
159-
whl_basename = backend.build_wheel(metadata_directory, config_settings)
160204
with open(os.path.join(metadata_directory, WHEEL_BUILT_MARKER), 'wb'):
161205
pass # Touch marker file
162206

@@ -205,6 +249,27 @@ def build_wheel(wheel_directory, config_settings, metadata_directory=None):
205249
metadata_directory)
206250

207251

252+
def build_editable(wheel_directory, config_settings, metadata_directory=None):
253+
"""Invoke the optional build_editable hook.
254+
255+
If a wheel was already built in the
256+
prepare_metadata_for_build_editable fallback, this
257+
will copy it rather than rebuilding the wheel.
258+
"""
259+
backend = _build_backend()
260+
try:
261+
hook = backend.build_editable
262+
except AttributeError:
263+
raise HookMissing()
264+
else:
265+
prebuilt_whl = _find_already_built_wheel(metadata_directory)
266+
if prebuilt_whl:
267+
shutil.copy2(prebuilt_whl, wheel_directory)
268+
return os.path.basename(prebuilt_whl)
269+
270+
return hook(wheel_directory, config_settings, metadata_directory)
271+
272+
208273
def get_requires_for_build_sdist(config_settings):
209274
"""Invoke the optional get_requires_for_build_wheel hook
210275
@@ -242,6 +307,9 @@ def build_sdist(sdist_directory, config_settings):
242307
'get_requires_for_build_wheel',
243308
'prepare_metadata_for_build_wheel',
244309
'build_wheel',
310+
'get_requires_for_build_editable',
311+
'prepare_metadata_for_build_editable',
312+
'build_editable',
245313
'get_requires_for_build_sdist',
246314
'build_sdist',
247315
}
@@ -270,8 +338,9 @@ def main():
270338
except GotUnsupportedOperation as e:
271339
json_out['unsupported'] = True
272340
json_out['traceback'] = e.traceback
273-
except HookMissing:
341+
except HookMissing as e:
274342
json_out['hook_missing'] = True
343+
json_out['missing_hook_name'] = e.hook_name or hook_name
275344

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

pep517/wrappers.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,59 @@ def build_wheel(
207207
'metadata_directory': metadata_directory,
208208
})
209209

210+
def get_requires_for_build_editable(self, config_settings=None):
211+
"""Identify packages required for building an editable wheel
212+
213+
Returns a list of dependency specifications, e.g.::
214+
215+
["wheel >= 0.25", "setuptools"]
216+
217+
This does not include requirements specified in pyproject.toml.
218+
It returns the result of calling the equivalently named hook in a
219+
subprocess.
220+
"""
221+
return self._call_hook('get_requires_for_build_editable', {
222+
'config_settings': config_settings
223+
})
224+
225+
def prepare_metadata_for_build_editable(
226+
self, metadata_directory, config_settings=None,
227+
_allow_fallback=True):
228+
"""Prepare a ``*.dist-info`` folder with metadata for this project.
229+
230+
Returns the name of the newly created folder.
231+
232+
If the build backend defines a hook with this name, it will be called
233+
in a subprocess. If not, the backend will be asked to build an editable
234+
wheel, and the dist-info extracted from that (unless _allow_fallback is
235+
False).
236+
"""
237+
return self._call_hook('prepare_metadata_for_build_editable', {
238+
'metadata_directory': abspath(metadata_directory),
239+
'config_settings': config_settings,
240+
'_allow_fallback': _allow_fallback,
241+
})
242+
243+
def build_editable(
244+
self, wheel_directory, config_settings=None,
245+
metadata_directory=None):
246+
"""Build an editable wheel from this project.
247+
248+
Returns the name of the newly created file.
249+
250+
In general, this will call the 'build_editable' hook in the backend.
251+
However, if that was previously called by
252+
'prepare_metadata_for_build_editable', and the same metadata_directory
253+
is used, the previously built wheel will be copied to wheel_directory.
254+
"""
255+
if metadata_directory is not None:
256+
metadata_directory = abspath(metadata_directory)
257+
return self._call_hook('build_editable', {
258+
'wheel_directory': abspath(wheel_directory),
259+
'config_settings': config_settings,
260+
'metadata_directory': metadata_directory,
261+
})
262+
210263
def get_requires_for_build_sdist(self, config_settings=None):
211264
"""Identify packages required for building a wheel
212265
@@ -280,7 +333,7 @@ def _call_hook(self, hook_name, kwargs):
280333
message=data.get('backend_error', '')
281334
)
282335
if data.get('hook_missing'):
283-
raise HookMissing(hook_name)
336+
raise HookMissing(data.get('missing_hook_name') or hook_name)
284337
return data['return_val']
285338

286339

tests/samples/buildsys_pkgs/buildsys.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@ def get_requires_for_build_wheel(config_settings):
1414
return ['wheelwright']
1515

1616

17+
def get_requires_for_build_editable(config_settings):
18+
return ['wheelwright', 'editables']
19+
20+
1721
def prepare_metadata_for_build_wheel(metadata_directory, config_settings):
1822
for distinfo in glob('*.dist-info'):
1923
shutil.copytree(distinfo, pjoin(metadata_directory, distinfo))
2024

2125

26+
prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel
27+
28+
2229
def prepare_build_wheel_files(build_directory, config_settings):
2330
shutil.copy('pyproject.toml', build_directory)
2431
for pyfile in glob('*.py'):
@@ -37,6 +44,9 @@ def build_wheel(wheel_directory, config_settings, metadata_directory=None):
3744
return whl_file
3845

3946

47+
build_editable = build_wheel
48+
49+
4050
def get_requires_for_build_sdist(config_settings):
4151
return ['frog']
4252

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from buildsys_minimal import build_sdist, build_wheel # noqa
2+
3+
4+
build_editable = build_wheel
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 Thomas Kluyver
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Metadata-Version: 1.2
2+
Name: pkg3
3+
Version: 0.5
4+
Summary: Sample package for tests
5+
Home-page: https://github.com/takluyver/pkg3
6+
License: UNKNOWN
7+
Author: Thomas Kluyver
8+
Author-email: [email protected]
9+
Classifier: License :: OSI Approved :: MIT License
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pkg3.py,sha256=ZawKBtrxtdGEheOCWvwzGZsO8Q1OSzEzecGNsRz-ekc,52
2+
pkg3-0.5.dist-info/LICENSE,sha256=GyKwSbUmfW38I6Z79KhNjsBLn9-xpR02DkK0NCyLQVQ,1081
3+
pkg3-0.5.dist-info/WHEEL,sha256=jxKvNaDKHDacpaLi69-vnLKkBSynwBzmMS82pipt1T0,100
4+
pkg3-0.5.dist-info/METADATA,sha256=4zQxJqc4Rvnlf5Y-seXnRx8g-1FK-sjTuS0A1KP0ajk,251
5+
pkg3-0.5.dist-info/RECORD,,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Wheel-Version: 1.0
2+
Generator: flit 0.11.3
3+
Root-Is-Purelib: true
4+
Tag: py2-none-any
5+
Tag: py3-none-any

tests/samples/pkg3/pkg3.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Sample package for tests"""
2+
3+
__version__ = '0.5'

tests/samples/pkg3/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = []
3+
build-backend = "buildsys_minimal_editable"

tests/test_call_hooks.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ def test_get_requires_for_build_wheel():
5050
assert res == ['wheelwright']
5151

5252

53+
def test_get_requires_for_build_editable():
54+
hooks = get_hooks('pkg1')
55+
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
56+
res = hooks.get_requires_for_build_editable({})
57+
assert res == ['wheelwright', 'editables']
58+
59+
5360
def test_get_requires_for_build_sdist():
5461
hooks = get_hooks('pkg1')
5562
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
@@ -66,6 +73,15 @@ def test_prepare_metadata_for_build_wheel():
6673
assert_isfile(pjoin(metadatadir, 'pkg1-0.5.dist-info', 'METADATA'))
6774

6875

76+
def test_prepare_metadata_for_build_editable():
77+
hooks = get_hooks('pkg1')
78+
with TemporaryDirectory() as metadatadir:
79+
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
80+
hooks.prepare_metadata_for_build_editable(metadatadir, {})
81+
82+
assert_isfile(pjoin(metadatadir, 'pkg1-0.5.dist-info', 'METADATA'))
83+
84+
6985
def test_build_wheel():
7086
hooks = get_hooks('pkg1')
7187
with TemporaryDirectory() as builddir:
@@ -80,6 +96,20 @@ def test_build_wheel():
8096
assert zipfile.is_zipfile(whl_file)
8197

8298

99+
def test_build_editable():
100+
hooks = get_hooks('pkg1')
101+
with TemporaryDirectory() as builddir:
102+
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
103+
whl_file = hooks.build_editable(builddir, {})
104+
105+
assert whl_file.endswith('.whl')
106+
assert os.sep not in whl_file
107+
108+
whl_file = pjoin(builddir, whl_file)
109+
assert_isfile(whl_file)
110+
assert zipfile.is_zipfile(whl_file)
111+
112+
83113
def test_build_wheel_relpath():
84114
hooks = get_hooks('pkg1')
85115
with TemporaryWorkingDirectory() as builddir:

0 commit comments

Comments
 (0)