feat: 增加伴生包加载器、manifest配置,Excel处理优化
This commit is contained in:
+11
-14
@@ -9,7 +9,7 @@ from typing import Any
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
class Config():
|
||||
"""MIL SDK 全局配置
|
||||
|
||||
承载测试工程相关的全局开关与路径信息,是 UI 层与 core 层之间的
|
||||
@@ -23,6 +23,7 @@ class Config:
|
||||
CurrProject: 当前工程名称
|
||||
ItemConfigs: 各模块/用例项的细粒度配置
|
||||
"""
|
||||
path:str
|
||||
DataPath:str = ""
|
||||
FilePath:str = ""
|
||||
AddTimeEn:bool = True
|
||||
@@ -31,6 +32,8 @@ class Config:
|
||||
# dict 是可变类型,必须用 default_factory 显式实例化,避免多个 Config 共享同一对象
|
||||
ItemConfigs: dict = field(default_factory=dict)
|
||||
|
||||
def check_path(self):
|
||||
return os.path.exists(self.path)
|
||||
|
||||
def to_dict(self):
|
||||
"""将 Config 实例序列化为 dict,便于写入 JSON 文件。"""
|
||||
@@ -40,10 +43,10 @@ class Config:
|
||||
"AddTimeEn":self.AddTimeEn,
|
||||
"GeratePath":self.GeratePath,
|
||||
"CurrProject":self.CurrProject,
|
||||
"ItemConfigs":self.ItemConfigs
|
||||
"ItemConfigs":self.ItemConfigs,
|
||||
}
|
||||
|
||||
def load_config(self, config_path: str) -> "Config":
|
||||
def load_config(self) -> "Config":
|
||||
"""从 JSON 文件读取配置并填充到当前实例的各个字段。
|
||||
|
||||
解析规则:
|
||||
@@ -63,11 +66,11 @@ class Config:
|
||||
"""
|
||||
try:
|
||||
# 以 UTF-8 读取 JSON,避免 Windows 默认编码带来的乱码问题
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
with open(self.path, 'r', encoding='utf-8') as f:
|
||||
raw = json.load(f)
|
||||
except FileNotFoundError:
|
||||
# 显式记录并重抛,遵循"错误必须显式处理,禁止静默失败"
|
||||
logger.error(f"配置文件{config_path}未找到")
|
||||
logger.error(f"配置文件{self.path}未找到")
|
||||
raise
|
||||
|
||||
if not isinstance(raw, dict):
|
||||
@@ -81,7 +84,7 @@ class Config:
|
||||
setattr(self, key, value)
|
||||
return self
|
||||
|
||||
def save_config(self,config_path:str):
|
||||
def save_config(self):
|
||||
"""将当前配置以 JSON 格式写入磁盘。
|
||||
|
||||
Args:
|
||||
@@ -91,15 +94,10 @@ class Config:
|
||||
Exception: 写入失败时记录日志并原样抛出异常。
|
||||
"""
|
||||
try:
|
||||
with open(config_path,'w',encoding='utf-8') as f:
|
||||
with open(self.path,'w',encoding='utf-8') as f:
|
||||
json.dump(self.to_dict(), f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"配置文件{config_path}写入失败{e.args}")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
logger.error(f"配置文件{self.path}写入失败{e.args}")
|
||||
|
||||
@dataclass
|
||||
class DataLog:
|
||||
@@ -111,7 +109,6 @@ class DataLog:
|
||||
"""
|
||||
time: float = 0.0
|
||||
value: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class SignalData:
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""伴生包加载器:把纯 Python 伴生包从产物目录挂到 sys.modules。
|
||||
|
||||
设计目标:宿主 exe 一次打包后,mil 的伴生依赖(openpyxl / et_xmlfile / ...)
|
||||
可通过"覆盖 build/<pkg>/ 下的 .pyd 或 mil_runtime/<pkg>/ 下的源码"增量更新,
|
||||
宿主不需要重打。
|
||||
|
||||
加载顺序(命中即停):
|
||||
1. sys.modules 已存在(不动)
|
||||
2. mil_runtime/<pkg>/ 下的纯 Python 源码(数据化部署)
|
||||
3. build/<pkg>/ 下的预编译 .pyd(本轮主路径)
|
||||
4. 都不命中 → 跳过并 warning,由调用方决定是否兜底
|
||||
|
||||
约定:
|
||||
- mil 包的父目录下存在两个候选目录:mil_runtime/(源码)和 build/(.pyd)。
|
||||
- 解析顺序可通过环境变量 MIL_RUNTIME_DIR / MIL_BUILD_DIR 覆盖。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_RUNTIME_DIRNAME = "mil_runtime"
|
||||
DEFAULT_BUILD_DIRNAME = "build"
|
||||
|
||||
|
||||
def _mil_root() -> Path:
|
||||
"""获取 mil 包的父目录(即 mil_runtime/ 与 build/ 所在的工程根)。
|
||||
|
||||
不通过 `import mil` 解析位置,避免触发 mil 包完整初始化
|
||||
(宿主侧必装 PySide6,开发侧不一定有)。
|
||||
"""
|
||||
# 本文件位于 <root>/mil/core/companion_loader.py,往上两级就是 <root>
|
||||
return Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
def _load_from_source(runtime_dir: Path, pkg: str) -> ModuleType | None:
|
||||
"""尝试从 mil_runtime/<pkg>/ 加载纯 Python 源码包。"""
|
||||
pkg_dir = runtime_dir / pkg
|
||||
if not (pkg_dir / "__init__.py").is_file():
|
||||
return None
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
pkg,
|
||||
pkg_dir / "__init__.py",
|
||||
submodule_search_locations=[str(pkg_dir)],
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
logger.warning(f"无法为 {pkg} 构造 spec(runtime 源码),跳过")
|
||||
return None
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[pkg] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _load_from_pyd(build_dir: Path, pkg: str) -> ModuleType | None:
|
||||
"""尝试从 build/<pkg>/<pkg>.cp311-*.pyd 加载预编译包。"""
|
||||
candidates = list(build_dir.glob(f"{pkg}.*.pyd"))
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
pyd_path = candidates[0]
|
||||
spec = importlib.util.spec_from_file_location(pkg, pyd_path)
|
||||
if spec is None or spec.loader is None:
|
||||
logger.warning(f"无法为 {pkg} 构造 spec(pyd={pyd_path}),跳过")
|
||||
return None
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[pkg] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def load_companions(packages: list[str]) -> list[str]:
|
||||
"""按上述顺序把每个伴生包挂到 sys.modules,返回成功挂载的包名列表。"""
|
||||
root = _mil_root()
|
||||
runtime_dir = Path(os.environ.get("MIL_RUNTIME_DIR", root / DEFAULT_RUNTIME_DIRNAME))
|
||||
build_dir = Path(os.environ.get("MIL_BUILD_DIR", root / DEFAULT_BUILD_DIRNAME))
|
||||
|
||||
loaded: list[str] = []
|
||||
for pkg in packages:
|
||||
if pkg in sys.modules:
|
||||
logger.debug(f"伴生包 {pkg} 已在 sys.modules,跳过")
|
||||
loaded.append(pkg)
|
||||
continue
|
||||
|
||||
mod = (
|
||||
_load_from_source(runtime_dir, pkg)
|
||||
or _load_from_pyd(build_dir, pkg)
|
||||
)
|
||||
if mod is not None:
|
||||
loaded.append(pkg)
|
||||
logger.info(f"伴生包已挂载: {pkg}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"伴生包未找到: {pkg}(runtime={runtime_dir} / build={build_dir} 均无)"
|
||||
)
|
||||
|
||||
return loaded
|
||||
@@ -0,0 +1,41 @@
|
||||
"""插件运行期依赖清单。
|
||||
|
||||
这是唯一的事实源:
|
||||
- build_pyd.py 读 COMPANION_PACKAGES_PYD 决定要编哪些伴生包;
|
||||
- build_pyd.py 编译 mil 时用 --include-package 把 COMPANION_PACKAGES_INLINE 编进 mil.pyd;
|
||||
- tools/export_runtime.py 读 COMPANION_C_EXTENSIONS 决定要拷哪些 C 扩展。
|
||||
|
||||
加新伴生包时只动这里,其它脚本自动跟随。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
# 需 Nuitka 编译成独立 .pyd 的伴生包(业务代码需保护)
|
||||
COMPANION_PACKAGES_PYD: list[str] = []
|
||||
|
||||
# 编译 mil 时用 --include-package 编进 mil.pyd 的纯 Python 包
|
||||
# 这些包的 .py 代码编进 mil.pyd,运行时内部 hard-import,不查 sys.path
|
||||
COMPANION_PACKAGES_INLINE: list[str] = [
|
||||
"openpyxl",
|
||||
"et_xmlfile",
|
||||
]
|
||||
|
||||
# 编译 mil 时用 --include-module 编进 mil.pyd 的 stdlib 子模块
|
||||
# Nuitka 默认不编译 stdlib(--nofollow-stdlib),所以必须显式 include。
|
||||
# 这些是 openpyxl/et_xmlfile 间接依赖的纯 Python stdlib,编进 mil.pyd 后
|
||||
# 运行时 hard-import,不查 sys.path。只有 C 扩展(_elementtree)留给运行时。
|
||||
COMPANION_STDLIB_INLINE: list[str] = [
|
||||
"xml.etree",
|
||||
"xml.etree.ElementTree",
|
||||
"xml.etree.ElementPath",
|
||||
"xml.etree.ElementInclude",
|
||||
]
|
||||
|
||||
# 兼容旧接口:所有伴生包
|
||||
COMPANION_PACKAGES: list[str] = COMPANION_PACKAGES_PYD + COMPANION_PACKAGES_INLINE
|
||||
|
||||
# 插件依赖的 C 扩展模块(Nuitka --module 编不进去,运行时从插件目录加载)
|
||||
# key = 所属 stdlib 包名, value = C 扩展模块名列表
|
||||
COMPANION_C_EXTENSIONS: dict[str, list[str]] = {
|
||||
"xml": ["_elementtree"], # xml.etree.ElementTree 内部 from _elementtree import *
|
||||
}
|
||||
Reference in New Issue
Block a user