mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
fix(compat): preserve legacy plugin imports
This commit is contained in:
@@ -3,9 +3,15 @@ import inspect
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, List, Optional, Set, Tuple
|
||||
from typing import Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from app.runtime.compat.manifest import MODULE_ALIASES, PACKAGE_ALIASES, ModuleAlias
|
||||
from app.runtime.compat.manifest import (
|
||||
MODULE_ALIASES,
|
||||
PACKAGE_ALIASES,
|
||||
SYMBOL_ALIASES,
|
||||
ModuleAlias,
|
||||
SymbolAlias,
|
||||
)
|
||||
|
||||
|
||||
WarningEmitter = Callable[[str], object]
|
||||
@@ -47,7 +53,23 @@ def _find_import_consumer() -> str:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _format_warning(usage: LegacyImportUsage, alias: ModuleAlias) -> str:
|
||||
def _find_alias(legacy_path: str) -> Optional[Union[ModuleAlias, SymbolAlias]]:
|
||||
"""查找旧模块或旧符号对应的精确兼容规则。"""
|
||||
module_alias = MODULE_ALIASES.get(legacy_path) or PACKAGE_ALIASES.get(
|
||||
legacy_path
|
||||
)
|
||||
if module_alias:
|
||||
return module_alias
|
||||
module_name, separator, symbol_name = legacy_path.rpartition(".")
|
||||
if not separator:
|
||||
return None
|
||||
return SYMBOL_ALIASES.get(module_name, {}).get(symbol_name)
|
||||
|
||||
|
||||
def _format_warning(
|
||||
usage: LegacyImportUsage,
|
||||
alias: Union[ModuleAlias, SymbolAlias],
|
||||
) -> str:
|
||||
"""生成包含实际目标和推荐 SDK 路径的单行兼容警告。"""
|
||||
consumer = usage.consumer
|
||||
if consumer.startswith("app.plugins."):
|
||||
@@ -56,17 +78,20 @@ def _format_warning(usage: LegacyImportUsage, alias: ModuleAlias) -> str:
|
||||
else:
|
||||
source = f"模块 {consumer}"
|
||||
origin = f"({usage.origin})" if usage.origin else ""
|
||||
target = (
|
||||
alias.target
|
||||
if isinstance(alias, ModuleAlias)
|
||||
else f"{alias.target_module}.{alias.target_name}"
|
||||
)
|
||||
return (
|
||||
f"[兼容导入] {source}{origin} 使用旧路径 {usage.legacy_module},"
|
||||
f"已映射到 {alias.target};请迁移到 {alias.replacement}"
|
||||
f"已映射到 {target};请迁移到 {alias.replacement}"
|
||||
)
|
||||
|
||||
|
||||
def _emit_usage(usage: LegacyImportUsage) -> None:
|
||||
"""按调用方和旧路径去重后输出兼容警告。"""
|
||||
alias = MODULE_ALIASES.get(usage.legacy_module) or PACKAGE_ALIASES.get(
|
||||
usage.legacy_module
|
||||
)
|
||||
alias = _find_alias(usage.legacy_module)
|
||||
if not alias:
|
||||
return
|
||||
key = (usage.consumer, usage.legacy_module)
|
||||
@@ -129,7 +154,11 @@ def _extract_legacy_imports(tree: ast.AST) -> Tuple[Tuple[str, int], ...]:
|
||||
matches.add((node.module, node.lineno))
|
||||
for imported in node.names:
|
||||
candidate = f"{node.module}.{imported.name}"
|
||||
if candidate in MODULE_ALIASES or candidate in PACKAGE_ALIASES:
|
||||
if (
|
||||
candidate in MODULE_ALIASES
|
||||
or candidate in PACKAGE_ALIASES
|
||||
or imported.name in SYMBOL_ALIASES.get(node.module, {})
|
||||
):
|
||||
matches.add((candidate, node.lineno))
|
||||
elif isinstance(node, ast.Call) and node.args:
|
||||
function_name = ""
|
||||
|
||||
@@ -12,6 +12,7 @@ from app.runtime.compat.manifest import (
|
||||
MODULE_ALIASES,
|
||||
PACKAGE_ALIASES,
|
||||
PACKAGE_EXPORTS,
|
||||
SYMBOL_ALIASES,
|
||||
VIRTUAL_PACKAGES,
|
||||
ModuleAlias,
|
||||
)
|
||||
@@ -115,6 +116,91 @@ class VirtualLegacyPackageLoader(importlib.abc.Loader):
|
||||
record_legacy_import(self.package_name)
|
||||
|
||||
|
||||
class LegacySymbolOverlayLoader(importlib.abc.Loader):
|
||||
"""在标准物理模块执行后叠加旧符号的惰性解析,不修改 canonical 源码。"""
|
||||
|
||||
_STATE_KEY = "__legacy_symbol_overlay_state__"
|
||||
|
||||
def __init__(self, module_name: str, original_loader: importlib.abc.Loader):
|
||||
"""保存物理模块名称和 PathFinder 已选择的原始 Loader。"""
|
||||
self.module_name = module_name
|
||||
self.original_loader = original_loader
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
"""把资源读取等非核心 Loader 能力转交给原始 Loader。"""
|
||||
return getattr(self.original_loader, name)
|
||||
|
||||
def create_module(self, spec):
|
||||
"""沿用原始 Loader 的模块创建逻辑。"""
|
||||
creator = getattr(self.original_loader, "create_module", None)
|
||||
return creator(spec) if creator else None
|
||||
|
||||
@classmethod
|
||||
def _restore_previous_overlay(cls, module: ModuleType) -> None:
|
||||
"""reload 前恢复物理模块原有的动态属性和 __all__ 状态。"""
|
||||
state = module.__dict__.pop(cls._STATE_KEY, None)
|
||||
if not state:
|
||||
return
|
||||
for name in ("__getattr__", "__dir__"):
|
||||
previous = state.get(name)
|
||||
if previous is None:
|
||||
module.__dict__.pop(name, None)
|
||||
else:
|
||||
module.__dict__[name] = previous
|
||||
if state.get("had_all"):
|
||||
module.__dict__["__all__"] = state.get("all")
|
||||
else:
|
||||
module.__dict__.pop("__all__", None)
|
||||
|
||||
def exec_module(self, module: ModuleType) -> None:
|
||||
"""执行真实模块后安装只对已登记旧符号生效的 __getattr__。"""
|
||||
self._restore_previous_overlay(module)
|
||||
executor = getattr(self.original_loader, "exec_module", None)
|
||||
if not executor:
|
||||
raise ImportError(f"模块 {self.module_name} 的原始 Loader 不支持 exec_module")
|
||||
executor(module)
|
||||
|
||||
exports = SYMBOL_ALIASES[self.module_name]
|
||||
previous_getattr = module.__dict__.get("__getattr__")
|
||||
previous_dir = module.__dict__.get("__dir__")
|
||||
had_all = "__all__" in module.__dict__
|
||||
previous_all = module.__dict__.get("__all__")
|
||||
|
||||
def resolve_export(name: str):
|
||||
"""惰性解析物理模块中已经迁走的旧符号。"""
|
||||
symbol = exports.get(name)
|
||||
if symbol:
|
||||
record_legacy_import(f"{self.module_name}.{name}")
|
||||
target = importlib.import_module(symbol.target_module)
|
||||
return getattr(target, symbol.target_name)
|
||||
if previous_getattr:
|
||||
return previous_getattr(name)
|
||||
raise AttributeError(
|
||||
f"module {self.module_name!r} has no attribute {name!r}"
|
||||
)
|
||||
|
||||
def list_exports():
|
||||
"""返回物理模块原有名称与兼容符号的并集。"""
|
||||
names = set(module.__dict__) | set(exports)
|
||||
if previous_dir:
|
||||
names.update(previous_dir())
|
||||
return sorted(names)
|
||||
|
||||
module.__getattr__ = resolve_export
|
||||
module.__dir__ = list_exports
|
||||
public_names = {
|
||||
name for name in module.__dict__ if not name.startswith("_")
|
||||
}
|
||||
declared_exports = set(previous_all or ()) if had_all else public_names
|
||||
module.__all__ = sorted(declared_exports | set(exports))
|
||||
module.__dict__[self._STATE_KEY] = {
|
||||
"__getattr__": previous_getattr,
|
||||
"__dir__": previous_dir,
|
||||
"had_all": had_all,
|
||||
"all": previous_all,
|
||||
}
|
||||
|
||||
|
||||
class BlockedLegacyModuleLoader(importlib.abc.Loader):
|
||||
"""阻止合成旧包从其他 Finder 泄漏未登记的新内部模块。"""
|
||||
|
||||
@@ -163,6 +249,12 @@ class LegacyImportFinder(importlib.abc.MetaPathFinder):
|
||||
is_package=alias.is_package,
|
||||
)
|
||||
|
||||
if fullname in SYMBOL_ALIASES:
|
||||
spec = importlib.machinery.PathFinder.find_spec(fullname, path, target)
|
||||
if spec and spec.loader:
|
||||
spec.loader = LegacySymbolOverlayLoader(fullname, spec.loader)
|
||||
return spec
|
||||
|
||||
virtual_packages = self._virtual_package_names()
|
||||
if fullname in virtual_packages:
|
||||
loader = VirtualLegacyPackageLoader(fullname)
|
||||
|
||||
@@ -30,6 +30,108 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = {
|
||||
introduced="v3.0.0",
|
||||
owner="sdk",
|
||||
),
|
||||
"app.db.agentchat_oper": ModuleAlias(
|
||||
target="app.db.oper.agentchat",
|
||||
replacement="app.db.oper.agentchat",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.agenttask_oper": ModuleAlias(
|
||||
target="app.db.oper.agenttask",
|
||||
replacement="app.db.oper.agenttask",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.downloadfailure_oper": ModuleAlias(
|
||||
target="app.db.oper.downloadfailure",
|
||||
replacement="app.db.oper.downloadfailure",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.downloadhistory_oper": ModuleAlias(
|
||||
target="app.db.oper.downloadhistory",
|
||||
replacement="app.db.oper.downloadhistory",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.init": ModuleAlias(
|
||||
target="app.startup.database_initializer",
|
||||
replacement="app.startup.database_initializer",
|
||||
introduced="v3.0.0",
|
||||
owner="startup",
|
||||
),
|
||||
"app.db.mediaserver_oper": ModuleAlias(
|
||||
target="app.db.oper.mediaserver",
|
||||
replacement="app.db.oper.mediaserver",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.message_oper": ModuleAlias(
|
||||
target="app.db.oper.message",
|
||||
replacement="app.db.oper.message",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.plugindata_oper": ModuleAlias(
|
||||
target="app.db.oper.plugindata",
|
||||
replacement="app.db.oper.plugindata",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.site_oper": ModuleAlias(
|
||||
target="app.db.oper.site",
|
||||
replacement="app.db.oper.site",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.subscribe_oper": ModuleAlias(
|
||||
target="app.sdk._legacy.subscribe",
|
||||
replacement="app.application.subscribe.add_subscribe",
|
||||
introduced="v3.0.0",
|
||||
owner="sdk",
|
||||
),
|
||||
"app.db.subscribehistory_oper": ModuleAlias(
|
||||
target="app.db.oper.subscribehistory",
|
||||
replacement="app.db.oper.subscribehistory",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.systemconfig_oper": ModuleAlias(
|
||||
target="app.db.oper.systemconfig",
|
||||
replacement="app.db.oper.systemconfig",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.transferhistory_oper": ModuleAlias(
|
||||
target="app.sdk._legacy.history",
|
||||
replacement="app.application.history",
|
||||
introduced="v3.0.0",
|
||||
owner="sdk",
|
||||
),
|
||||
"app.db.transferpending_oper": ModuleAlias(
|
||||
target="app.db.oper.transferpending",
|
||||
replacement="app.db.oper.transferpending",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.user_oper": ModuleAlias(
|
||||
target="app.sdk._legacy.user",
|
||||
replacement="app.db.oper.user 或 app.api.deps",
|
||||
introduced="v3.0.0",
|
||||
owner="sdk",
|
||||
),
|
||||
"app.db.userconfig_oper": ModuleAlias(
|
||||
target="app.db.oper.userconfig",
|
||||
replacement="app.db.oper.userconfig",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.db.workflow_oper": ModuleAlias(
|
||||
target="app.db.oper.workflow",
|
||||
replacement="app.db.oper.workflow",
|
||||
introduced="v3.0.0",
|
||||
owner="db",
|
||||
),
|
||||
"app.utils.crypto": ModuleAlias(
|
||||
target="app.foundation.crypto",
|
||||
replacement="app.sdk.utilities",
|
||||
@@ -151,10 +253,10 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = {
|
||||
owner="runtime",
|
||||
),
|
||||
"app.utils.media": ModuleAlias(
|
||||
target="app.domain.media",
|
||||
replacement="app.domain.media",
|
||||
target="app.sdk.media",
|
||||
replacement="app.sdk.media",
|
||||
introduced="v3.0.0",
|
||||
owner="domain",
|
||||
owner="sdk",
|
||||
),
|
||||
"app.utils.mixins": ModuleAlias(
|
||||
target="app.runtime.reload",
|
||||
@@ -562,3 +664,40 @@ PACKAGE_EXPORTS: Dict[str, Dict[str, SymbolAlias]] = {
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
# 物理模块仍存在、仅部分公开符号迁走时,由导入器在标准 Loader 执行后叠加惰性符号路由。
|
||||
# canonical 源码不反向依赖兼容层,目标符号也只在旧调用方真正取用时加载。
|
||||
SYMBOL_ALIASES: Dict[str, Dict[str, SymbolAlias]] = {
|
||||
"app.domain.media": {
|
||||
name: SymbolAlias(
|
||||
target_module="app.schemas.media",
|
||||
target_name=name,
|
||||
replacement=f"app.schemas.media.{name}",
|
||||
)
|
||||
for name in (
|
||||
"MEDIA_SOURCE_ALIASES",
|
||||
"MEDIA_SOURCE_PREFIXES",
|
||||
"normalize_media_source",
|
||||
"parse_media_key",
|
||||
"resolve_media_identity",
|
||||
"normalize_media_identity_payload",
|
||||
"build_media_key",
|
||||
)
|
||||
},
|
||||
"app.schemas": {
|
||||
name: SymbolAlias(
|
||||
target_module="app.sdk._legacy.transfer",
|
||||
target_name=name,
|
||||
replacement=f"app.application.transfer.{name}",
|
||||
)
|
||||
for name in ("TransferTask", "TransferQueue")
|
||||
},
|
||||
"app.schemas.transfer": {
|
||||
name: SymbolAlias(
|
||||
target_module="app.sdk._legacy.transfer",
|
||||
target_name=name,
|
||||
replacement=f"app.application.transfer.{name}",
|
||||
)
|
||||
for name in ("TransferTask", "TransferQueue")
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user