|
21 | 21 | import traceback |
22 | 22 | from glob import glob |
23 | 23 | from importlib import import_module |
24 | | -from importlib.util import module_from_spec |
25 | 24 | from importlib.machinery import PathFinder |
26 | 25 | from os.path import join as pjoin |
27 | 26 |
|
@@ -65,40 +64,41 @@ def _build_backend(): |
65 | 64 |
|
66 | 65 | if backend_path: |
67 | 66 | extra_pathitems = backend_path.split(os.pathsep) |
68 | | - obj = _load_module_from_path(mod_path, extra_pathitems) |
69 | | - else: |
70 | | - try: |
71 | | - obj = import_module(mod_path) |
72 | | - except ImportError: |
73 | | - msg = f"Cannot import {mod_path!r}" |
74 | | - raise BackendUnavailable(msg, traceback.format_exc()) |
| 67 | + sys.meta_path.insert(0, _BackendPathFinder(extra_pathitems, mod_path)) |
| 68 | + |
| 69 | + try: |
| 70 | + obj = import_module(mod_path) |
| 71 | + except ImportError: |
| 72 | + msg = f"Cannot import {mod_path!r}" |
| 73 | + raise BackendUnavailable(msg, traceback.format_exc()) |
75 | 74 |
|
76 | 75 | if obj_path: |
77 | 76 | for path_part in obj_path.split("."): |
78 | 77 | obj = getattr(obj, path_part) |
79 | 78 | return obj |
80 | 79 |
|
81 | 80 |
|
82 | | -def _load_module_from_path(fullname, pathitems): |
83 | | - """Given a set of sys.path-like entries, load a module from it""" |
84 | | - sys.path[:0] = pathitems # Still required for other imports. |
85 | | - parts = fullname.split(".") |
86 | | - # Parent packages need to be imported to ensure everything comes from pathitems. |
87 | | - for i in range(len(parts)): |
88 | | - module_name = ".".join(parts[: i + 1]) |
89 | | - spec = _find_spec_in_path(module_name, pathitems) |
90 | | - module = module_from_spec(spec) |
91 | | - sys.modules[module_name] = module |
92 | | - spec.loader.exec_module(module) |
93 | | - return module |
94 | | - |
95 | | - |
96 | | -def _find_spec_in_path(fullname, pathitems): |
97 | | - """Given sys.path-like entries, find a module spec or raise an exception""" |
98 | | - spec = PathFinder.find_spec(fullname, path=pathitems) |
99 | | - if not spec: |
100 | | - raise BackendUnavailable(f"Cannot find module {fullname!r} in {pathitems!r}") |
101 | | - return spec |
| 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 | + |
| 93 | + def find_spec(self, fullname, _path, _target=None): |
| 94 | + # Ignore other items in _path or sys.path and use backend_path instead: |
| 95 | + spec = PathFinder.find_spec(fullname, path=self.backend_path) |
| 96 | + if spec is None and fullname == self.backend_module: |
| 97 | + # According to the spec, the backend MUST be loaded from backend-path. |
| 98 | + # Therefore, we can halt the import machinery and raise a clean error. |
| 99 | + msg = f"Cannot find module {self.backend_module!r} in {self.backend_path!r}" |
| 100 | + raise BackendUnavailable(msg) |
| 101 | + return spec |
102 | 102 |
|
103 | 103 |
|
104 | 104 | def _supported_features(): |
|
0 commit comments