Files
MoviePilot/app/runtime/extensions/plugin/loader.py
T

124 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""插件源码发现、导入和模块缓存清理。"""
from __future__ import annotations
import importlib
import sys
import traceback
from collections.abc import Callable
from pathlib import Path
from typing import Any, Optional
PluginImportPreparer = Callable[..., None]
PluginImportScanner = Callable[..., None]
PluginValidator = Callable[[Any], bool]
class PluginLoader:
"""只负责从运行目录发现插件类,并维护对应模块缓存。"""
def __init__(
self,
*,
plugins_root: Path,
import_preparer: PluginImportPreparer,
import_scanner: PluginImportScanner,
log: Any,
) -> None:
"""保存插件目录、导入前置能力和日志端口。"""
self._plugins_root = plugins_root
self._import_preparer = import_preparer
self._import_scanner = import_scanner
self._logger = log
def load(
self,
plugin_id: Optional[str],
installed_plugins: list[str],
validator: PluginValidator,
) -> list[Any]:
"""只导入指定插件或已安装插件,并返回通过契约检查的插件类。"""
if not self._plugins_root.exists():
self._logger.warning(f"插件目录不存在:{self._plugins_root}")
return []
targets = (
[plugin_id.lower()]
if plugin_id
else [item.lower() for item in installed_plugins]
)
if not targets:
self._logger.debug("没有需要加载的插件")
return []
plugins = []
loaded_classes = set()
for plugin_dir in self._plugins_root.iterdir():
if not plugin_dir.is_dir() or plugin_dir.name.startswith("_"):
continue
if plugin_dir.name not in targets:
self._logger.debug(
f"跳过插件目录:{plugin_dir.name}(不在加载列表中)"
)
continue
if not (plugin_dir / "__init__.py").exists():
self._logger.debug(
f"跳过插件目录:{plugin_dir.name}(缺少__init__.py"
)
continue
try:
module_name = f"app.plugins.{plugin_dir.name}"
self._logger.debug(f"正在导入插件模块:{module_name}")
self._import_preparer(
plugin_id=plugin_dir.name,
plugin_dir=plugin_dir,
)
self._import_scanner(
plugin_id=plugin_dir.name,
plugin_dir=plugin_dir,
)
module = importlib.import_module(module_name)
for name, candidate in module.__dict__.items():
if name.startswith("_") or not isinstance(candidate, type):
continue
if name in loaded_classes or not validator(candidate):
continue
loaded_classes.add(name)
plugins.append(candidate)
self._logger.debug(f"找到符合条件的插件类:{name}")
break
except Exception as err:
self._logger.error(
f"加载插件 {plugin_dir.name} 失败:{str(err)} - "
f"{traceback.format_exc()}"
)
return plugins
def clear_modules(self, plugin_id: Optional[str] = None) -> list[str]:
"""清除指定插件或全部插件的 Python 模块缓存。"""
prefix = (
f"app.plugins.{plugin_id.lower()}"
if plugin_id
else "app.plugins"
)
removed = [
module_name
for module_name in list(sys.modules)
if module_name == prefix or module_name.startswith(f"{prefix}.")
]
for module_name in removed:
sys.modules.pop(module_name, None)
self._logger.debug(f"已清除插件模块缓存:{module_name}")
importlib.invalidate_caches()
self._logger.debug("已清除查找器的缓存")
if plugin_id:
if removed:
self._logger.info(
f"插件 {plugin_id} 共清除 {len(removed)} 个模块缓存:{removed}"
)
else:
self._logger.debug(f"插件 {plugin_id} 没有找到需要清除的模块缓存")
return removed