|
| 1 | +import functools |
| 2 | +import importlib.abc |
| 3 | +import os |
| 4 | +import subprocess |
| 5 | +import sys |
| 6 | + |
| 7 | +from types import ModuleType |
| 8 | +from typing import Optional, Union |
| 9 | + |
| 10 | +from mesonpy._compat import Collection, Sequence |
| 11 | + |
| 12 | + |
| 13 | +class MesonpyFinder(importlib.abc.MetaPathFinder): |
| 14 | + """Custom loader that whose purpose is to detect when the import system is |
| 15 | + trying to load our modules, and trigger a rebuild. After triggering a |
| 16 | + rebuild, we return None in find_spec, letting the normal finders pick up the |
| 17 | + modules. |
| 18 | + """ |
| 19 | + |
| 20 | + def __init__( |
| 21 | + self, |
| 22 | + project_path: str, |
| 23 | + top_level_modules: Collection[str], |
| 24 | + rebuild_commands: Sequence[Sequence[str]], |
| 25 | + ) -> None: |
| 26 | + if not os.path.isabs(project_path): |
| 27 | + raise ImportError('Project path must be absolute') |
| 28 | + self._project_path = project_path |
| 29 | + self._top_level_modules = top_level_modules |
| 30 | + self._rebuild_commands = rebuild_commands |
| 31 | + |
| 32 | + def __repr__(self) -> str: |
| 33 | + return f'{self.__class__}({self._project_path})' |
| 34 | + |
| 35 | + def __hash__(self) -> int: |
| 36 | + return hash((self._project_path, self._top_level_modules, self._rebuild_commands)) |
| 37 | + |
| 38 | + @functools.lru_cache(maxsize=1) |
| 39 | + def rebuild(self) -> None: |
| 40 | + for command in self._rebuild_commands: |
| 41 | + subprocess.check_call(command) |
| 42 | + |
| 43 | + def find_spec( |
| 44 | + self, |
| 45 | + fullname: str, |
| 46 | + path: Optional[Sequence[Union[str, bytes]]], |
| 47 | + target: Optional[ModuleType] = None, |
| 48 | + ) -> None: |
| 49 | + # if it's one of our modules, trigger a rebuild |
| 50 | + if fullname.split('.', maxsplit=1)[0] in self._top_level_modules: |
| 51 | + self.rebuild() |
| 52 | + # return none (meaning we "didn't find" the module) and let the normal |
| 53 | + # finders find/import it |
| 54 | + return None |
| 55 | + |
| 56 | + @classmethod |
| 57 | + def install( |
| 58 | + cls, |
| 59 | + project_path: str, |
| 60 | + top_level_modules: Collection[str], |
| 61 | + rebuild_commands: Sequence[Sequence[str]], |
| 62 | + ) -> None: |
| 63 | + finder = cls(project_path, top_level_modules, rebuild_commands) |
| 64 | + if finder not in sys.meta_path: |
| 65 | + # prepend our finder to sys.meta_path, so that it is queried before |
| 66 | + # the normal finders, and can trigger a project rebuild |
| 67 | + sys.meta_path.insert(0, finder) |
| 68 | + # add the project path to sys.path, so that the normal finder can |
| 69 | + # find our modules |
| 70 | + sys.path.append(project_path) |
0 commit comments