fix(compat): preserve legacy plugin imports

This commit is contained in:
jxxghp
2026-08-15 07:15:03 +08:00
parent 8a11214a43
commit a2117bafc8
13 changed files with 831 additions and 17 deletions
+37 -8
View File
@@ -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 = ""
+92
View File
@@ -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)
+142 -3
View File
@@ -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")
},
}