Coverage for src/symphra_modules/loader/directory.py: 49.26%
98 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-26 18:16 +0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-26 18:16 +0800
1"""目录加载器实现."""
3import importlib
4import importlib.util
5import sys
6from pathlib import Path
7from typing import Any
9from symphra_modules.abc import ModuleInterface
10from symphra_modules.exceptions import ModuleLoadError
11from symphra_modules.loader.base import ModuleLoader
12from symphra_modules.utils import get_logger
14logger = get_logger()
17class DirectoryLoader(ModuleLoader):
18 """从目录加载模块."""
20 def __init__(self, base_path: Path | None = None) -> None:
21 """初始化目录加载器.
23 Args:
24 base_path: 基础路径,默认为当前工作目录
25 """
26 self.base_path = base_path or Path.cwd()
28 def _to_module_name(self, path: Path) -> str | None:
29 """将文件或包路径转换为模块名(相对 base_path).
31 Args:
32 path: 文件或包路径
34 Returns:
35 模块名,如果无法转换则返回 None
36 """
37 try:
38 relative = path.relative_to(self.base_path)
39 except ValueError:
40 # 无法转换为相对路径时返回 None
41 return None
43 parts = list(relative.parts)
44 if parts and parts[-1].endswith(".py"):
45 parts[-1] = parts[-1][:-3]
47 return ".".join(parts)
49 def load(self, source: str) -> dict[str, type[ModuleInterface]]:
50 """从指定目录加载所有模块.
52 Args:
53 source: 目录路径(相对于 base_path)
55 Returns:
56 模块名到模块类的映射字典
58 Raises:
59 ModuleLoadError: 目录不存在时抛出
60 """
61 modules: dict[str, type[ModuleInterface]] = {}
62 module_dir = Path(self.base_path) / source
64 if not module_dir.exists():
65 raise ModuleLoadError(f"模块目录不存在: {module_dir}")
67 # 查找所有 Python 文件
68 for py_file in module_dir.glob("*.py"):
69 if py_file.name.startswith("_"): 69 ↛ 72line 69 didn't jump to line 72 because the condition on line 69 was always true
70 continue
72 try:
73 module_classes = self._load_from_file(py_file)
74 modules.update(module_classes)
75 except Exception as e:
76 logger.warning(f"无法从 {py_file} 加载模块: {e}")
78 # 查找包
79 for package_dir in module_dir.iterdir():
80 if package_dir.is_dir() and (package_dir / "__init__.py").exists(): 80 ↛ 81line 80 didn't jump to line 81 because the condition on line 80 was never true
81 try:
82 module_classes = self._load_from_package(package_dir)
83 modules.update(module_classes)
84 except Exception as e:
85 logger.error(f"无法从 {package_dir} 加载模块: {e}")
87 return modules
89 def discover(self, source: str) -> list[str]:
90 """发现目录中的模块.
92 Args:
93 source: 目录路径
95 Returns:
96 模块名称列表
97 """
98 module_names: list[str] = []
99 module_dir = Path(self.base_path) / source
101 if not module_dir.exists():
102 return module_names
104 # Python 文件
105 for py_file in module_dir.glob("*.py"):
106 if not py_file.name.startswith("_"):
107 module_names.append(py_file.stem)
109 # 包
110 for package_dir in module_dir.iterdir():
111 if package_dir.is_dir() and (package_dir / "__init__.py").exists():
112 module_names.append(package_dir.name)
114 return module_names
116 def _load_from_file(self, file_path: Path, package: str | None = None) -> dict[str, type[ModuleInterface]]:
117 """从 Python 文件加载模块.
119 Args:
120 file_path: Python 文件路径
121 package: 包名(可选)
123 Returns:
124 模块类字典
126 Raises:
127 ModuleLoadError: 加载失败时抛出
128 """
129 module_name: str
130 if package is not None:
131 module_name = f"{package}.{file_path.stem}"
132 else:
133 inferred = self._to_module_name(file_path)
134 module_name = inferred or file_path.stem
136 module: Any = None
137 if "." in module_name:
138 try:
139 module = importlib.import_module(module_name)
140 except ImportError:
141 module = None
143 if module is None:
144 spec = importlib.util.spec_from_file_location(module_name, file_path)
145 if not spec or not spec.loader:
146 raise ModuleLoadError(f"无法从 {file_path} 创建模块规范")
148 module = importlib.util.module_from_spec(spec)
149 sys.modules[module_name] = module
150 spec.loader.exec_module(module)
152 return self._find_module_classes(module)
154 def _load_from_package(self, package_dir: Path) -> dict[str, type[ModuleInterface]]:
155 """从包加载模块.
157 Args:
158 package_dir: 包目录路径
160 Returns:
161 模块类字典
163 Raises:
164 ModuleLoadError: 加载失败时抛出
165 """
166 inferred_package = self._to_module_name(package_dir)
167 package_name = inferred_package or package_dir.name
169 # 将包路径添加到 sys.path,便于定位依赖
170 parent_path = str(package_dir.parent)
171 if parent_path not in sys.path:
172 sys.path.insert(0, parent_path)
174 modules: dict[str, type[ModuleInterface]] = {}
176 # 优先加载显式的 module.py,再加载其他非私有 Python 文件
177 candidate_files: list[Path] = []
178 module_file = package_dir / "module.py"
179 if module_file.exists():
180 candidate_files.append(module_file)
182 if not candidate_files:
183 # 回退:若没有独立文件,则尝试加载包本身
184 try:
185 module = importlib.import_module(package_name)
186 return self._find_module_classes(module)
187 except ImportError as e:
188 raise ModuleLoadError(f"无法从 {package_name} 加载模块: {e}") from e
190 for py_file in candidate_files:
191 try:
192 module_classes = self._load_from_file(py_file, package=package_name)
193 modules.update(module_classes)
194 except Exception as e:
195 logger.warning(f"无法从 {package_name} 的 {py_file.name} 加载模块: {e}")
197 return modules