2021-02-19 14:58:26 +08:00
|
|
|
import sys
|
|
|
|
import pkgutil
|
|
|
|
import importlib
|
2021-06-19 15:15:00 +08:00
|
|
|
from pathlib import Path
|
2021-02-19 14:58:26 +08:00
|
|
|
from types import ModuleType
|
|
|
|
from collections import Counter
|
|
|
|
from importlib.abc import MetaPathFinder
|
2021-03-31 20:38:00 +08:00
|
|
|
from importlib.machinery import PathFinder, SourceFileLoader
|
2021-10-21 00:08:37 +08:00
|
|
|
from typing import Set, List, Union, Iterable, Optional, Sequence
|
2021-03-31 20:38:00 +08:00
|
|
|
|
2021-11-08 01:02:35 +08:00
|
|
|
from .export import Export
|
|
|
|
from . import _current_plugin
|
|
|
|
from .plugin import Plugin, _new_plugin
|
2021-02-19 14:58:26 +08:00
|
|
|
|
2021-11-09 00:57:59 +08:00
|
|
|
_manager_stack: List["PluginManager"] = []
|
|
|
|
|
2021-02-19 14:58:26 +08:00
|
|
|
|
2021-11-08 01:02:35 +08:00
|
|
|
# TODO
|
2021-02-19 14:58:26 +08:00
|
|
|
class PluginManager:
|
|
|
|
|
2021-11-09 00:57:59 +08:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
plugins: Optional[Iterable[str]] = None,
|
|
|
|
search_path: Optional[Iterable[str]] = None,
|
|
|
|
):
|
2021-02-19 14:58:26 +08:00
|
|
|
|
|
|
|
# simple plugin not in search path
|
|
|
|
self.plugins: Set[str] = set(plugins or [])
|
|
|
|
self.search_path: Set[str] = set(search_path or [])
|
|
|
|
# ensure can be loaded
|
|
|
|
self.list_plugins()
|
|
|
|
|
|
|
|
def search_plugins(self) -> List[str]:
|
|
|
|
return [
|
|
|
|
module_info.name
|
|
|
|
for module_info in pkgutil.iter_modules(self.search_path)
|
|
|
|
]
|
|
|
|
|
|
|
|
def list_plugins(self) -> Set[str]:
|
|
|
|
_pre_managers: List[PluginManager]
|
|
|
|
if self in _manager_stack:
|
|
|
|
_pre_managers = _manager_stack[:_manager_stack.index(self)]
|
|
|
|
else:
|
|
|
|
_pre_managers = _manager_stack[:]
|
|
|
|
|
|
|
|
_search_path: Set[str] = set()
|
|
|
|
for manager in _pre_managers:
|
|
|
|
_search_path |= manager.search_path
|
|
|
|
if _search_path & self.search_path:
|
|
|
|
raise RuntimeError("Duplicate plugin search path!")
|
|
|
|
|
|
|
|
_search_plugins = self.search_plugins()
|
|
|
|
c = Counter([*_search_plugins, *self.plugins])
|
|
|
|
conflict = [name for name, num in c.items() if num > 1]
|
|
|
|
if conflict:
|
|
|
|
raise RuntimeError(
|
|
|
|
f"More than one plugin named {' / '.join(conflict)}!")
|
|
|
|
return set(_search_plugins) | self.plugins
|
|
|
|
|
|
|
|
def load_plugin(self, name) -> ModuleType:
|
|
|
|
if name in self.plugins:
|
2021-11-09 00:57:59 +08:00
|
|
|
return importlib.import_module(name)
|
2021-02-19 14:58:26 +08:00
|
|
|
|
|
|
|
if "." in name:
|
|
|
|
raise ValueError("Plugin name cannot contain '.'")
|
2021-03-13 18:21:56 +08:00
|
|
|
|
2021-11-09 00:57:59 +08:00
|
|
|
return importlib.import_module(f"{self.namespace}.{name}")
|
2021-02-19 14:58:26 +08:00
|
|
|
|
|
|
|
def load_all_plugins(self) -> List[ModuleType]:
|
|
|
|
return [self.load_plugin(name) for name in self.list_plugins()]
|
|
|
|
|
2021-05-10 18:39:59 +08:00
|
|
|
def _rewrite_module_name(self, module_name: str) -> Optional[str]:
|
2021-03-22 01:15:15 +08:00
|
|
|
prefix = f"{self.internal_module.__name__}."
|
2021-05-10 18:39:59 +08:00
|
|
|
raw_name = module_name[len(self.namespace) +
|
|
|
|
1:] if module_name.startswith(self.namespace +
|
|
|
|
".") else None
|
|
|
|
# dir plugins
|
|
|
|
if raw_name and raw_name.split(".")[0] in self.search_plugins():
|
|
|
|
return f"{prefix}{raw_name}"
|
|
|
|
# third party plugin or renamed dir plugins
|
2021-03-22 01:15:15 +08:00
|
|
|
elif module_name in self.plugins or module_name.startswith(prefix):
|
|
|
|
return module_name
|
2021-05-10 18:39:59 +08:00
|
|
|
# dir plugins
|
2021-03-31 20:38:00 +08:00
|
|
|
elif module_name in self.search_plugins():
|
|
|
|
return f"{prefix}{module_name}"
|
2021-02-19 14:58:26 +08:00
|
|
|
return None
|
|
|
|
|
2021-06-19 15:15:00 +08:00
|
|
|
def _check_absolute_import(self, origin_path: str) -> Optional[str]:
|
|
|
|
if not self.search_path:
|
|
|
|
return
|
|
|
|
paths = set([
|
|
|
|
*self.search_path,
|
|
|
|
*(str(Path(path).resolve()) for path in self.search_path)
|
|
|
|
])
|
|
|
|
for path in paths:
|
|
|
|
try:
|
|
|
|
rel_path = Path(origin_path).relative_to(path)
|
2021-08-06 15:13:36 +08:00
|
|
|
if rel_path.stem == "__init__":
|
|
|
|
return f"{self.internal_module.__name__}." + ".".join(
|
|
|
|
rel_path.parts[:-1])
|
|
|
|
return f"{self.internal_module.__name__}." + ".".join(
|
|
|
|
rel_path.parts[:-1] + (rel_path.stem,))
|
2021-06-19 15:15:00 +08:00
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
|
2021-02-19 14:58:26 +08:00
|
|
|
|
|
|
|
class PluginFinder(MetaPathFinder):
|
|
|
|
|
2021-10-21 00:08:37 +08:00
|
|
|
def find_spec(self,
|
|
|
|
fullname: str,
|
|
|
|
path: Optional[Sequence[Union[bytes, str]]],
|
|
|
|
target: Optional[ModuleType] = None):
|
2021-02-19 14:58:26 +08:00
|
|
|
if _manager_stack:
|
|
|
|
index = -1
|
2021-06-19 15:15:00 +08:00
|
|
|
origin_spec = PathFinder.find_spec(fullname, path, target)
|
2021-02-19 14:58:26 +08:00
|
|
|
while -index <= len(_manager_stack):
|
|
|
|
manager = _manager_stack[index]
|
2021-06-19 15:15:00 +08:00
|
|
|
|
|
|
|
rel_name = None
|
|
|
|
if origin_spec and origin_spec.origin:
|
|
|
|
rel_name = manager._check_absolute_import(
|
|
|
|
origin_spec.origin)
|
|
|
|
|
|
|
|
newname = manager._rewrite_module_name(rel_name or fullname)
|
2021-02-19 14:58:26 +08:00
|
|
|
if newname:
|
2021-03-22 01:15:15 +08:00
|
|
|
spec = PathFinder.find_spec(
|
2021-06-15 01:13:05 +08:00
|
|
|
newname, path or [*manager.search_path, *sys.path],
|
2021-04-02 00:05:27 +08:00
|
|
|
target)
|
2021-02-19 14:58:26 +08:00
|
|
|
if spec:
|
2021-06-15 01:13:05 +08:00
|
|
|
spec.loader = PluginLoader( # type: ignore
|
|
|
|
manager, newname, spec.origin)
|
2021-02-19 14:58:26 +08:00
|
|
|
return spec
|
|
|
|
index -= 1
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2021-03-13 18:21:56 +08:00
|
|
|
class PluginLoader(SourceFileLoader):
|
|
|
|
|
2021-03-19 14:59:59 +08:00
|
|
|
def __init__(self, manager: PluginManager, fullname: str, path) -> None:
|
|
|
|
self.manager = manager
|
2021-03-13 18:21:56 +08:00
|
|
|
self.loaded = False
|
|
|
|
super().__init__(fullname, path)
|
|
|
|
|
|
|
|
def create_module(self, spec) -> Optional[ModuleType]:
|
|
|
|
if self.name in sys.modules:
|
|
|
|
self.loaded = True
|
|
|
|
return sys.modules[self.name]
|
2021-03-19 14:59:59 +08:00
|
|
|
# return None to use default module creation
|
2021-03-13 18:21:56 +08:00
|
|
|
return super().create_module(spec)
|
|
|
|
|
|
|
|
def exec_module(self, module: ModuleType) -> None:
|
|
|
|
if self.loaded:
|
|
|
|
return
|
2021-03-31 20:38:00 +08:00
|
|
|
|
2021-05-10 18:39:59 +08:00
|
|
|
export = Export()
|
|
|
|
_export_token = _export.set(export)
|
|
|
|
|
|
|
|
prefix = self.manager.internal_module.__name__
|
|
|
|
is_dir_plugin = self.name.startswith(prefix + ".")
|
|
|
|
module_name = self.name[len(prefix) +
|
|
|
|
1:] if is_dir_plugin else self.name
|
|
|
|
_plugin_token = _current_plugin.set(module)
|
|
|
|
|
|
|
|
setattr(module, "__export__", export)
|
|
|
|
setattr(module, "__plugin_name__",
|
|
|
|
module_name.split(".")[0] if is_dir_plugin else module_name)
|
|
|
|
setattr(module, "__module_name__", module_name)
|
|
|
|
setattr(module, "__module_prefix__", prefix if is_dir_plugin else "")
|
|
|
|
|
|
|
|
# try:
|
|
|
|
# super().exec_module(module)
|
|
|
|
# except Exception as e:
|
|
|
|
# raise ImportError(
|
|
|
|
# f"Error when executing module {module_name} from {module.__file__}."
|
|
|
|
# ) from e
|
|
|
|
super().exec_module(module)
|
|
|
|
|
|
|
|
_current_plugin.reset(_plugin_token)
|
|
|
|
_export.reset(_export_token)
|
2021-03-19 14:59:59 +08:00
|
|
|
return
|
2021-03-13 18:21:56 +08:00
|
|
|
|
|
|
|
|
2021-02-19 14:58:26 +08:00
|
|
|
sys.meta_path.insert(0, PluginFinder())
|