Skip to content

Commit 47a3347

Browse files
authored
Merge branch 'main' into typing
2 parents 228aef1 + 084b02e commit 47a3347

File tree

11 files changed

+121
-52
lines changed

11 files changed

+121
-52
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
name: CI
22

3+
concurrency:
4+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
5+
cancel-in-progress: true
6+
37
on: [push, pull_request]
48

59
env:

docs/pyproject_hooks.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ Exceptions
5252

5353
Each exception has public attributes with the same name as their constructors.
5454

55-
.. autoexception:: pyproject_hooks.BackendInvalid
5655
.. autoexception:: pyproject_hooks.BackendUnavailable
5756
.. autoexception:: pyproject_hooks.HookMissing
5857
.. autoexception:: pyproject_hooks.UnsupportedOperation

src/pyproject_hooks/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from typing import TYPE_CHECKING
55

66
from ._impl import (
7-
BackendInvalid,
87
BackendUnavailable,
98
BuildBackendHookCaller,
109
HookMissing,
@@ -16,7 +15,6 @@
1615
__version__ = "1.0.0"
1716
__all__ = [
1817
"BackendUnavailable",
19-
"BackendInvalid",
2018
"HookMissing",
2119
"UnsupportedOperation",
2220
"default_subprocess_runner",

src/pyproject_hooks/_impl.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,19 @@ def read_json(path: str) -> Mapping[str, Any]:
3737

3838
class BackendUnavailable(Exception):
3939
"""Will be raised if the backend cannot be imported in the hook process."""
40-
41-
def __init__(self, traceback: str) -> None:
42-
self.traceback = traceback
43-
44-
45-
class BackendInvalid(Exception):
46-
"""Will be raised if the backend is invalid."""
47-
40+
4841
def __init__(
4942
self,
50-
backend_name: str,
51-
backend_path: Optional[Sequence[str]],
52-
message: str,
43+
traceback: str,
44+
message: Optional[str] = None,
45+
backend_name: Optional[str] = None,
46+
backend_path: Optional[Sequence[str]] = None,
5347
) -> None:
54-
super().__init__(message)
48+
# Preserving arg order for the sake of API backward compatibility.
5549
self.backend_name = backend_name
5650
self.backend_path = backend_path
51+
self.traceback = traceback
52+
super().__init__(message or "Error while importing backend")
5753

5854

5955
class HookMissing(Exception):
@@ -403,12 +399,11 @@ def _call_hook(self, hook_name: str, kwargs: Mapping[str, Any]) -> Any:
403399
if data.get("unsupported"):
404400
raise UnsupportedOperation(data.get("traceback", ""))
405401
if data.get("no_backend"):
406-
raise BackendUnavailable(data.get("traceback", ""))
407-
if data.get("backend_invalid"):
408-
raise BackendInvalid(
402+
raise BackendUnavailable(
403+
data.get("traceback", ""),
404+
message=data.get("backend_error", ""),
409405
backend_name=self.build_backend,
410406
backend_path=self.backend_path,
411-
message=data.get("backend_error", ""),
412407
)
413408
if data.get("hook_missing"):
414409
raise HookMissing(data.get("missing_hook_name") or hook_name)

src/pyproject_hooks/_in_process/_in_process.py

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import traceback
2222
from glob import glob
2323
from importlib import import_module
24+
from importlib.machinery import PathFinder
2425
from os.path import join as pjoin
2526

2627
# This file is run as a script, and `import wrappers` is not zip-safe, so we
@@ -40,15 +41,10 @@ def read_json(path):
4041
class BackendUnavailable(Exception):
4142
"""Raised if we cannot import the backend"""
4243

43-
def __init__(self, traceback):
44-
self.traceback = traceback
45-
46-
47-
class BackendInvalid(Exception):
48-
"""Raised if the backend is invalid"""
49-
50-
def __init__(self, message):
44+
def __init__(self, message, traceback=None):
45+
super().__init__(message)
5146
self.message = message
47+
self.traceback = traceback
5248

5349

5450
class HookMissing(Exception):
@@ -59,38 +55,58 @@ def __init__(self, hook_name=None):
5955
self.hook_name = hook_name
6056

6157

62-
def contained_in(filename, directory):
63-
"""Test if a file is located within the given directory."""
64-
filename = os.path.normcase(os.path.abspath(filename))
65-
directory = os.path.normcase(os.path.abspath(directory))
66-
return os.path.commonprefix([filename, directory]) == directory
67-
68-
6958
def _build_backend():
7059
"""Find and load the build backend"""
71-
# Add in-tree backend directories to the front of sys.path.
7260
backend_path = os.environ.get("_PYPROJECT_HOOKS_BACKEND_PATH")
61+
ep = os.environ["_PYPROJECT_HOOKS_BUILD_BACKEND"]
62+
mod_path, _, obj_path = ep.partition(":")
63+
7364
if backend_path:
65+
# Ensure in-tree backend directories have the highest priority when importing.
7466
extra_pathitems = backend_path.split(os.pathsep)
75-
sys.path[:0] = extra_pathitems
67+
sys.meta_path.insert(0, _BackendPathFinder(extra_pathitems, mod_path))
7668

77-
ep = os.environ["_PYPROJECT_HOOKS_BUILD_BACKEND"]
78-
mod_path, _, obj_path = ep.partition(":")
7969
try:
8070
obj = import_module(mod_path)
8171
except ImportError:
82-
raise BackendUnavailable(traceback.format_exc())
83-
84-
if backend_path:
85-
if not any(contained_in(obj.__file__, path) for path in extra_pathitems):
86-
raise BackendInvalid("Backend was not loaded from backend-path")
72+
msg = f"Cannot import {mod_path!r}"
73+
raise BackendUnavailable(msg, traceback.format_exc())
8774

8875
if obj_path:
8976
for path_part in obj_path.split("."):
9077
obj = getattr(obj, path_part)
9178
return obj
9279

9380

81+
class _BackendPathFinder:
82+
"""Implements the MetaPathFinder interface to locate modules in ``backend-path``.
83+
84+
Since the environment provided by the frontend can contain all sorts of
85+
MetaPathFinders, the only way to ensure the backend is loaded from the
86+
right place is to prepend our own.
87+
"""
88+
89+
def __init__(self, backend_path, backend_module):
90+
self.backend_path = backend_path
91+
self.backend_module = backend_module
92+
self.backend_parent, _, _ = backend_module.partition(".")
93+
94+
def find_spec(self, fullname, _path, _target=None):
95+
if "." in fullname:
96+
# Rely on importlib to find nested modules based on parent's path
97+
return None
98+
99+
# Ignore other items in _path or sys.path and use backend_path instead:
100+
spec = PathFinder.find_spec(fullname, path=self.backend_path)
101+
if spec is None and fullname == self.backend_parent:
102+
# According to the spec, the backend MUST be loaded from backend-path.
103+
# Therefore, we can halt the import machinery and raise a clean error.
104+
msg = f"Cannot find module {self.backend_module!r} in {self.backend_path!r}"
105+
raise BackendUnavailable(msg)
106+
107+
return spec
108+
109+
94110
def _supported_features():
95111
"""Return the list of options features supported by the backend.
96112
@@ -342,8 +358,6 @@ def main():
342358
except BackendUnavailable as e:
343359
json_out["no_backend"] = True
344360
json_out["traceback"] = e.traceback
345-
except BackendInvalid as e:
346-
json_out["backend_invalid"] = True
347361
json_out["backend_error"] = e.message
348362
except GotUnsupportedOperation as e:
349363
json_out["unsupported"] = True
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from ..buildsys import * # noqa: F403
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# PathFinder.find_spec only take into consideration the last segment
2+
# of the module name (not the full name).
3+
raise Exception("This isn't the backend you are looking for")
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 = 'nested.intree_backend'
3+
backend-path = ['backend']

tests/test_call_hooks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ def get_hooks(pkg, **kwargs):
3434
def test_missing_backend_gives_exception():
3535
hooks = get_hooks("pkg1")
3636
with modified_env({"PYTHONPATH": ""}):
37-
with pytest.raises(BackendUnavailable):
37+
msg = "Cannot import 'buildsys'"
38+
with pytest.raises(BackendUnavailable, match=msg) as exc:
3839
hooks.get_requires_for_build_wheel({})
40+
assert exc.value.backend_name == "buildsys"
3941

4042

4143
def test_get_requires_for_build_wheel():

0 commit comments

Comments
 (0)