Skip to content

Commit ecd511e

Browse files
authored
Merge pull request #46 from pfmoore/backend_path
Support in tree hooks (pyproject.toml backend-path key)
2 parents 2f97e1b + 9e4935d commit ecd511e

File tree

5 files changed

+144
-4
lines changed

5 files changed

+144
-4
lines changed

pep517/_in_process.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
33
It expects:
44
- Command line args: hook_name, control_dir
5-
- Environment variable: PEP517_BUILD_BACKEND=entry.point:spec
5+
- Environment variables:
6+
PEP517_BUILD_BACKEND=entry.point:spec
7+
PEP517_BACKEND_PATH=paths (separated with os.pathsep)
68
- control_dir/input.json:
79
- {"kwargs": {...}}
810
@@ -13,6 +15,7 @@
1315
from glob import glob
1416
from importlib import import_module
1517
import os
18+
import os.path
1619
from os.path import join as pjoin
1720
import re
1821
import shutil
@@ -29,14 +32,41 @@ def __init__(self, traceback):
2932
self.traceback = traceback
3033

3134

35+
class BackendInvalid(Exception):
36+
"""Raised if the backend is invalid"""
37+
def __init__(self, message):
38+
self.message = message
39+
40+
41+
def contained_in(filename, directory):
42+
"""Test if a file is located within the given directory."""
43+
filename = os.path.normcase(os.path.abspath(filename))
44+
directory = os.path.normcase(os.path.abspath(directory))
45+
return os.path.commonprefix([filename, directory]) == directory
46+
47+
3248
def _build_backend():
3349
"""Find and load the build backend"""
50+
# Add in-tree backend directories to the front of sys.path.
51+
backend_path = os.environ.get('PEP517_BACKEND_PATH')
52+
if backend_path:
53+
extra_pathitems = backend_path.split(os.pathsep)
54+
sys.path[:0] = extra_pathitems
55+
3456
ep = os.environ['PEP517_BUILD_BACKEND']
3557
mod_path, _, obj_path = ep.partition(':')
3658
try:
3759
obj = import_module(mod_path)
3860
except ImportError:
3961
raise BackendUnavailable(traceback.format_exc())
62+
63+
if backend_path:
64+
if not any(
65+
contained_in(obj.__file__, path)
66+
for path in extra_pathitems
67+
):
68+
raise BackendInvalid("Backend was not loaded from backend-path")
69+
4070
if obj_path:
4171
for path_part in obj_path.split('.'):
4272
obj = getattr(obj, path_part)
@@ -203,6 +233,9 @@ def main():
203233
except BackendUnavailable as e:
204234
json_out['no_backend'] = True
205235
json_out['traceback'] = e.traceback
236+
except BackendInvalid as e:
237+
json_out['backend_invalid'] = True
238+
json_out['backend_error'] = e.message
206239
except GotUnsupportedOperation as e:
207240
json_out['unsupported'] = True
208241
json_out['traceback'] = e.traceback

pep517/wrappers.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ def __init__(self, traceback):
2626
self.traceback = traceback
2727

2828

29+
class BackendInvalid(Exception):
30+
"""Will be raised if the backend is invalid."""
31+
def __init__(self, backend_name, backend_path, message):
32+
self.backend_name = backend_name
33+
self.backend_path = backend_path
34+
self.message = message
35+
36+
2937
class UnsupportedOperation(Exception):
3038
"""May be raised by build_sdist if the backend indicates that it can't."""
3139
def __init__(self, traceback):
@@ -41,15 +49,45 @@ def default_subprocess_runner(cmd, cwd=None, extra_environ=None):
4149
check_call(cmd, cwd=cwd, env=env)
4250

4351

52+
def norm_and_check(source_tree, requested):
53+
"""Normalise and check a backend path.
54+
55+
Ensure that the requested backend path is specified as a relative path,
56+
and resolves to a location under the given source tree.
57+
58+
Return an absolute version of the requested path.
59+
"""
60+
if os.path.isabs(requested):
61+
raise ValueError("paths must be relative")
62+
63+
abs_source = os.path.abspath(source_tree)
64+
abs_requested = os.path.normpath(os.path.join(abs_source, requested))
65+
# We have to use commonprefix for Python 2.7 compatibility. So we
66+
# normalise case to avoid problems because commonprefix is a character
67+
# based comparison :-(
68+
norm_source = os.path.normcase(abs_source)
69+
norm_requested = os.path.normcase(abs_requested)
70+
if os.path.commonprefix([norm_source, norm_requested]) != norm_source:
71+
raise ValueError("paths must be inside source tree")
72+
73+
return abs_requested
74+
75+
4476
class Pep517HookCaller(object):
4577
"""A wrapper around a source directory to be built with a PEP 517 backend.
4678
4779
source_dir : The path to the source directory, containing pyproject.toml.
4880
backend : The build backend spec, as per PEP 517, from pyproject.toml.
81+
backend_path : The backend path, as per PEP 517, from pyproject.toml.
4982
"""
50-
def __init__(self, source_dir, build_backend):
83+
def __init__(self, source_dir, build_backend, backend_path=None):
5184
self.source_dir = abspath(source_dir)
5285
self.build_backend = build_backend
86+
if backend_path:
87+
backend_path = [
88+
norm_and_check(self.source_dir, p) for p in backend_path
89+
]
90+
self.backend_path = backend_path
5391
self._subprocess_runner = default_subprocess_runner
5492

5593
# TODO: Is this over-engineered? Maybe frontends only need to
@@ -143,25 +181,40 @@ def _call_hook(self, hook_name, kwargs):
143181
# letters, digits and _, . and : characters, and will be used as a
144182
# Python identifier, so non-ASCII content is wrong on Python 2 in
145183
# any case).
184+
# For backend_path, we use sys.getfilesystemencoding.
146185
if sys.version_info[0] == 2:
147186
build_backend = self.build_backend.encode('ASCII')
148187
else:
149188
build_backend = self.build_backend
189+
extra_environ = {'PEP517_BUILD_BACKEND': build_backend}
190+
191+
if self.backend_path:
192+
backend_path = os.pathsep.join(self.backend_path)
193+
if sys.version_info[0] == 2:
194+
backend_path = backend_path.encode(sys.getfilesystemencoding())
195+
extra_environ['PEP517_BACKEND_PATH'] = backend_path
150196

151197
with tempdir() as td:
152-
compat.write_json({'kwargs': kwargs}, pjoin(td, 'input.json'),
198+
hook_input = {'kwargs': kwargs}
199+
compat.write_json(hook_input, pjoin(td, 'input.json'),
153200
indent=2)
154201

155202
# Run the hook in a subprocess
156203
self._subprocess_runner(
157204
[sys.executable, _in_proc_script, hook_name, td],
158205
cwd=self.source_dir,
159-
extra_environ={'PEP517_BUILD_BACKEND': build_backend}
206+
extra_environ=extra_environ
160207
)
161208

162209
data = compat.read_json(pjoin(td, 'output.json'))
163210
if data.get('unsupported'):
164211
raise UnsupportedOperation(data.get('traceback', ''))
165212
if data.get('no_backend'):
166213
raise BackendUnavailable(data.get('traceback', ''))
214+
if data.get('backend_invalid'):
215+
raise BackendInvalid(
216+
backend_name=self.build_backend,
217+
backend_path=self.backend_path,
218+
message=data.get('backend_error', '')
219+
)
167220
return data['return_val']
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def get_requires_for_build_sdist(config_settings):
2+
return ["intree_backend_called"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
build-backend = 'intree_backend'
3+
backend-path = ['backend']

tests/test_inplace_hooks.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from os.path import dirname, abspath, join as pjoin
2+
import pytoml
3+
from testpath import modified_env
4+
import pytest
5+
6+
from pep517.wrappers import Pep517HookCaller, BackendInvalid
7+
8+
SAMPLES_DIR = pjoin(dirname(abspath(__file__)), 'samples')
9+
BUILDSYS_PKGS = pjoin(SAMPLES_DIR, 'buildsys_pkgs')
10+
11+
12+
def get_hooks(pkg, backend=None, path=None):
13+
source_dir = pjoin(SAMPLES_DIR, pkg)
14+
with open(pjoin(source_dir, 'pyproject.toml')) as f:
15+
data = pytoml.load(f)
16+
if backend is None:
17+
backend = data['build-system']['build-backend']
18+
if path is None:
19+
path = data['build-system']['backend-path']
20+
return Pep517HookCaller(source_dir, backend, path)
21+
22+
23+
def test_backend_path_within_tree():
24+
source_dir = pjoin(SAMPLES_DIR, 'pkg1')
25+
assert Pep517HookCaller(source_dir, 'dummy', ['.', 'subdir'])
26+
assert Pep517HookCaller(source_dir, 'dummy', ['../pkg1', 'subdir/..'])
27+
# TODO: Do we want to insist on ValueError, or invent another exception?
28+
with pytest.raises(Exception):
29+
assert Pep517HookCaller(source_dir, 'dummy', [source_dir])
30+
with pytest.raises(Exception):
31+
Pep517HookCaller(source_dir, 'dummy', ['.', '..'])
32+
with pytest.raises(Exception):
33+
Pep517HookCaller(source_dir, 'dummy', ['subdir/../..'])
34+
with pytest.raises(Exception):
35+
Pep517HookCaller(source_dir, 'dummy', ['/'])
36+
37+
38+
def test_intree_backend():
39+
hooks = get_hooks('pkg_intree')
40+
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
41+
res = hooks.get_requires_for_build_sdist({})
42+
assert res == ["intree_backend_called"]
43+
44+
45+
def test_intree_backend_not_in_path():
46+
hooks = get_hooks('pkg_intree', backend='buildsys')
47+
with modified_env({'PYTHONPATH': BUILDSYS_PKGS}):
48+
with pytest.raises(BackendInvalid):
49+
hooks.get_requires_for_build_sdist({})

0 commit comments

Comments
 (0)