21
21
import traceback
22
22
from glob import glob
23
23
from importlib import import_module
24
+ from importlib .machinery import PathFinder
24
25
from os .path import join as pjoin
25
26
26
27
# This file is run as a script, and `import wrappers` is not zip-safe, so we
@@ -40,15 +41,10 @@ def read_json(path):
40
41
class BackendUnavailable (Exception ):
41
42
"""Raised if we cannot import the backend"""
42
43
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 )
51
46
self .message = message
47
+ self .traceback = traceback
52
48
53
49
54
50
class HookMissing (Exception ):
@@ -59,38 +55,58 @@ def __init__(self, hook_name=None):
59
55
self .hook_name = hook_name
60
56
61
57
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
-
69
58
def _build_backend ():
70
59
"""Find and load the build backend"""
71
- # Add in-tree backend directories to the front of sys.path.
72
60
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
+
73
64
if backend_path :
65
+ # Ensure in-tree backend directories have the highest priority when importing.
74
66
extra_pathitems = backend_path .split (os .pathsep )
75
- sys .path [: 0 ] = extra_pathitems
67
+ sys .meta_path . insert ( 0 , _BackendPathFinder ( extra_pathitems , mod_path ))
76
68
77
- ep = os .environ ["_PYPROJECT_HOOKS_BUILD_BACKEND" ]
78
- mod_path , _ , obj_path = ep .partition (":" )
79
69
try :
80
70
obj = import_module (mod_path )
81
71
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 ())
87
74
88
75
if obj_path :
89
76
for path_part in obj_path .split ("." ):
90
77
obj = getattr (obj , path_part )
91
78
return obj
92
79
93
80
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
+
94
110
def _supported_features ():
95
111
"""Return the list of options features supported by the backend.
96
112
@@ -342,8 +358,6 @@ def main():
342
358
except BackendUnavailable as e :
343
359
json_out ["no_backend" ] = True
344
360
json_out ["traceback" ] = e .traceback
345
- except BackendInvalid as e :
346
- json_out ["backend_invalid" ] = True
347
361
json_out ["backend_error" ] = e .message
348
362
except GotUnsupportedOperation as e :
349
363
json_out ["unsupported" ] = True
0 commit comments