mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
* Cleanup registry to be easy-understanding (#76) * delete deprecated mock devices * rename categories * combine chromatographic devices * rename rviz simulation nodes * organic virtual devices * parse vessel_id * run registry completion before merge --------- Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com> * fix: workstation handlers and vessel_id parsing * fix: working dir error when input config path feat: report publish topic when error * modify default discovery_interval to 15s * feat: add trace log level * feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79) * fix: drop_tips not using auto resource select * fix: discard_tips error * fix: discard_tips * fix: prcxi_res * add: prcxi res fix: startup slow * feat: workstation example * fix pumps and liquid_handler handle * feat: 优化protocol node节点运行日志 * fix all protocol_compilers and remove deprecated devices * feat: 新增use_remote_resource参数 * fix and remove redundant info * bugfixes on organic protocols * fix filter protocol * fix protocol node * 临时兼容错误的driver写法 * fix: prcxi import error * use call_async in all service to avoid deadlock * fix: figure_resource * Update recipe.yaml * add workstation template and battery example * feat: add sk & ak * update workstation base * Create workstation_architecture.md * refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode * refactor: ProtocolNode→WorkstationNode * Add:msgs.action (#83) * update: Workstation dev 将版本号从 0.10.3 更新为 0.10.4 (#84) * Add:msgs.action * update: 将版本号从 0.10.3 更新为 0.10.4 * simplify resource system * uncompleted refactor * example for use WorkstationBase * feat: websocket * feat: websocket test * feat: workstation example * feat: action status * fix: station自己的方法注册错误 * fix: 还原protocol node处理方法 * fix: build * fix: missing job_id key * ws test version 1 * ws test version 2 * ws protocol * 增加物料关系上传日志 * 增加物料关系上传日志 * 修正物料关系上传 * 修复工站的tracker实例追踪失效问题 * 增加handle检测,增加material edge关系上传 * 修复event loop错误 * 修复edge上报错误 * 修复async错误 * 更新schema的title字段 * 主机节点信息等支持自动刷新 * 注册表编辑器 * 修复status密集发送时,消息出错 * 增加addr参数 * fix: addr param * fix: addr param * 取消labid 和 强制config输入 * Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup - Created LiquidHandlerSetGroup.action with fields for group name, wells, and volumes. - Created LiquidHandlerTransferGroup.action with fields for source and target group names and unit volume. - Both actions include response fields for return information and success status. * Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists * Add set_group and transfer_group methods to PRCXI9300Handler and update liquid_handler.yaml * result_info改为字典类型 * 新增uat的地址替换 * runze multiple pump support (cherry picked from commit49354fcf39) * remove runze multiple software obtainer (cherry picked from commit8bcc92a394) * support multiple backbone (cherry picked from commit4771ff2347) * Update runze pump format * Correct runze multiple backbone * Update runze_multiple_backbone * Correct runze pump multiple receive method. * Correct runze pump multiple receive method. * 对于PRCXI9320的transfer_group,一对多和多对多 * 移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5 * fix import error * fix dupe upload registry * refactor ws client * add server timeout * Fix: run-column with correct vessel id (#86) * fix run_column * Update run_column_protocol.py (cherry picked from commite5aa4d940a) * resource_update use resource_add * 新增版位推荐功能 * 重新规定了版位推荐的入参 * update registry with nested obj * fix protocol node log_message, added create_resource return value * fix protocol node log_message, added create_resource return value * try fix add protocol * fix resource_add * 修复移液站错误的aspirate注册表 * Feature/xprbalance-zhida (#80) * feat(devices): add Zhida GC/MS pretreatment automation workstation * feat(devices): add mettler_toledo xpr balance * balance * 重新补全zhida注册表 * PRCXI9320 json * PRCXI9320 json * PRCXI9320 json * fix resource download * remove class for resource * bump version to 0.10.6 * 更新所有注册表 * 修复protocolnode的兼容性 * 修复protocolnode的兼容性 * Update install md * Add Defaultlayout * 更新物料接口 * fix dict to tree/nested-dict converter * coin_cell_station draft * refactor: rename "station_resource" to "deck" * add standardized BIOYOND resources: bottle_carrier, bottle * refactor and add BIOYOND resources tests * add BIOYOND deck assignment and pass all tests * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92) * feat: 新威电池测试系统驱动与注册文件 * feat: bring neware driver & battery.json into workstation_dev_YB2 * add bioyond studio draft * bioyond station with communication init and resource sync * fix bioyond station and registry * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * frontend_docs * create/update resources with POST/PUT for big amount/ small amount data * create/update resources with POST/PUT for big amount/ small amount data * refactor: add itemized_carrier instead of carrier consists of ResourceHolder * create warehouse by factory func * update bioyond launch json * add child_size for itemized_carrier * fix bioyond resource io * Workstation templates: Resources and its CRUD, and workstation tasks (#95) * coin_cell_station draft * refactor: rename "station_resource" to "deck" * add standardized BIOYOND resources: bottle_carrier, bottle * refactor and add BIOYOND resources tests * add BIOYOND deck assignment and pass all tests * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92) * feat: 新威电池测试系统驱动与注册文件 * feat: bring neware driver & battery.json into workstation_dev_YB2 * add bioyond studio draft * bioyond station with communication init and resource sync * fix bioyond station and registry * create/update resources with POST/PUT for big amount/ small amount data * refactor: add itemized_carrier instead of carrier consists of ResourceHolder * create warehouse by factory func * update bioyond launch json * add child_size for itemized_carrier * fix bioyond resource io --------- Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com> Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com> * 更新物料接口 * Workstation dev yb2 (#100) * Refactor and extend reaction station action messages * Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities - Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability. - Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input. - Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input. - Refined `create_diamine_solution_task` to include additional parameters for better task configuration. - Enhanced schema descriptions and default values for improved user guidance. * 修复to_plr_resources * add update remove * 支持选择器注册表自动生成 支持转运物料 * 修复资源添加 * 修复transfer_resource_to_another生成 * 更新transfer_resource_to_another参数,支持spot入参 * 新增test_resource动作 * fix host_node error * fix host_node test_resource error * fix host_node test_resource error * 过滤本地动作 * 移动内部action以兼容host node * 修复同步任务报错不显示的bug * feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了 * update todo * modify bioyond/plr converter, bioyond resource registry, and tests * pass the tests * update todo * add conda-pack-build.yml * add auto install script for conda-pack-build.yml (cherry picked from commit172599adcf) * update conda-pack-build.yml * update conda-pack-build.yml * update conda-pack-build.yml * update conda-pack-build.yml * update conda-pack-build.yml * Add version in __init__.py Update conda-pack-build.yml Add create_zip_archive.py * Update conda-pack-build.yml * Update conda-pack-build.yml (with mamba) * Update conda-pack-build.yml * Fix FileNotFoundError * Try fix 'charmap' codec can't encode characters in position 16-23: character maps to <undefined> * Fix unilabos msgs search error * Fix environment_check.py * Update recipe.yaml * Update registry. Update uuid loop figure method. Update install docs. * Fix nested conda pack * Fix one-key installation path error * Bump version to 0.10.7 * Workshop bj (#99) * Add LaiYu Liquid device integration and tests Introduce LaiYu Liquid device implementation, including backend, controllers, drivers, configuration, and resource files. Add hardware connection, tip pickup, and simplified test scripts, as well as experiment and registry configuration for LaiYu Liquid. Documentation and .gitignore for the device are also included. * feat(LaiYu_Liquid): 重构设备模块结构并添加硬件文档 refactor: 重新组织LaiYu_Liquid模块目录结构 docs: 添加SOPA移液器和步进电机控制指令文档 fix: 修正设备配置中的最大体积默认值 test: 新增工作台配置测试用例 chore: 删除过时的测试脚本和配置文件 * add * 重构: 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py 并更新所有导入引用 - 使用 git mv 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py - 更新所有相关文件中的导入引用 - 保持代码功能不变,仅改善命名一致性 - 测试确认所有导入正常工作 * 修复: 在 core/__init__.py 中添加 LaiYuLiquidBackend 导出 - 添加 LaiYuLiquidBackend 到导入列表 - 添加 LaiYuLiquidBackend 到 __all__ 导出列表 - 确保所有主要类都可以正确导入 * 修复大小写文件夹名字 * 电池装配工站二次开发教程(带目录)上传至dev (#94) * 电池装配工站二次开发教程 * Update intro.md * 物料教程 * 更新物料教程,json格式注释 * Update prcxi driver & fix transfer_liquid mix_times (#90) * Update prcxi driver & fix transfer_liquid mix_times * fix: correct mix_times type * Update liquid_handler registry * test: prcxi.py * Update registry from pr * fix ony-key script not exist * clean files --------- Co-authored-by: Junhan Chang <changjh@dp.tech> Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com> Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com> Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com> Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com> Co-authored-by: LccLink <1951855008@qq.com> Co-authored-by: lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com> Co-authored-by: shiyubo0410 <shiyubo@dp.tech>
780 lines
29 KiB
Python
780 lines
29 KiB
Python
"""
|
||
导入管理器
|
||
|
||
该模块提供了一个动态导入和管理模块的系统,避免误删未使用的导入。
|
||
"""
|
||
|
||
import builtins
|
||
import importlib
|
||
import inspect
|
||
import sys
|
||
import traceback
|
||
import ast
|
||
import os
|
||
from pathlib import Path
|
||
from typing import Dict, List, Any, Optional, Callable, Type, Union, Tuple
|
||
|
||
__all__ = [
|
||
"ImportManager",
|
||
"default_manager",
|
||
"load_module",
|
||
"get_class",
|
||
"get_module",
|
||
"init_from_list",
|
||
"get_class_info_static",
|
||
"get_registry_class_info",
|
||
]
|
||
|
||
from ast import Constant
|
||
|
||
from unilabos.utils import logger
|
||
|
||
|
||
class ImportManager:
|
||
"""导入管理器类,用于动态加载和管理模块"""
|
||
|
||
def __init__(self, module_list: Optional[List[str]] = None):
|
||
"""
|
||
初始化导入管理器
|
||
|
||
Args:
|
||
module_list: 要预加载的模块路径列表
|
||
"""
|
||
self._modules: Dict[str, Any] = {}
|
||
self._classes: Dict[str, Type] = {}
|
||
self._functions: Dict[str, Callable] = {}
|
||
|
||
if module_list:
|
||
for module_path in module_list:
|
||
self.load_module(module_path)
|
||
|
||
def load_module(self, module_path: str) -> Any:
|
||
"""
|
||
加载指定路径的模块
|
||
|
||
Args:
|
||
module_path: 模块路径
|
||
|
||
Returns:
|
||
加载的模块对象
|
||
|
||
Raises:
|
||
ImportError: 如果模块导入失败
|
||
"""
|
||
try:
|
||
if module_path in self._modules:
|
||
return self._modules[module_path]
|
||
|
||
module = importlib.import_module(module_path)
|
||
self._modules[module_path] = module
|
||
|
||
# 索引模块中的类和函数
|
||
for name, obj in inspect.getmembers(module):
|
||
if inspect.isclass(obj):
|
||
full_name = f"{module_path}.{name}"
|
||
self._classes[name] = obj
|
||
self._classes[full_name] = obj
|
||
elif inspect.isfunction(obj):
|
||
full_name = f"{module_path}.{name}"
|
||
self._functions[name] = obj
|
||
self._functions[full_name] = obj
|
||
|
||
return module
|
||
except Exception as e:
|
||
logger.error(f"导入模块 '{module_path}' 时发生错误:{str(e)}")
|
||
logger.warning(traceback.format_exc())
|
||
raise ImportError(f"无法导入模块 {module_path}: {str(e)}")
|
||
|
||
def get_module(self, module_path: str) -> Any:
|
||
"""
|
||
获取已加载的模块
|
||
|
||
Args:
|
||
module_path: 模块路径
|
||
|
||
Returns:
|
||
模块对象
|
||
|
||
Raises:
|
||
KeyError: 如果模块未加载
|
||
"""
|
||
if module_path not in self._modules:
|
||
return self.load_module(module_path)
|
||
return self._modules[module_path]
|
||
|
||
def get_class(self, class_name: str) -> Type:
|
||
"""
|
||
获取类对象
|
||
|
||
Args:
|
||
class_name: 类名或完整类路径
|
||
|
||
Returns:
|
||
类对象
|
||
|
||
Raises:
|
||
KeyError: 如果找不到类
|
||
"""
|
||
if class_name in self._classes:
|
||
return self._classes[class_name]
|
||
|
||
# 尝试动态导入
|
||
if ":" in class_name:
|
||
module_path, cls_name = class_name.rsplit(":", 1)
|
||
module = self.load_module(module_path)
|
||
if hasattr(module, cls_name):
|
||
cls = getattr(module, cls_name)
|
||
self._classes[class_name] = cls
|
||
self._classes[cls_name] = cls
|
||
return cls
|
||
else:
|
||
# 如果cls_name是builtins中的关键字,则返回对应类
|
||
if class_name in builtins.__dict__:
|
||
return builtins.__dict__[class_name]
|
||
|
||
raise KeyError(f"找不到类: {class_name}")
|
||
|
||
def list_modules(self) -> List[str]:
|
||
"""列出所有已加载的模块路径"""
|
||
return list(self._modules.keys())
|
||
|
||
def list_classes(self) -> List[str]:
|
||
"""列出所有已索引的类名"""
|
||
return list(self._classes.keys())
|
||
|
||
def list_functions(self) -> List[str]:
|
||
"""列出所有已索引的函数名"""
|
||
return list(self._functions.keys())
|
||
|
||
def search_class(self, class_name: str, search_lower=False) -> Optional[Type]:
|
||
"""
|
||
在所有已加载的模块中搜索特定类名
|
||
|
||
Args:
|
||
class_name: 要搜索的类名
|
||
search_lower: 以小写搜索
|
||
|
||
Returns:
|
||
找到的类对象,如果未找到则返回None
|
||
"""
|
||
# 如果cls_name是builtins中的关键字,则返回对应类
|
||
if class_name in builtins.__dict__:
|
||
return builtins.__dict__[class_name]
|
||
# 首先在已索引的类中查找
|
||
if class_name in self._classes:
|
||
return self._classes[class_name]
|
||
|
||
if search_lower:
|
||
classes = {name.lower(): obj for name, obj in self._classes.items()}
|
||
if class_name in classes:
|
||
return classes[class_name]
|
||
|
||
# 遍历所有已加载的模块进行搜索
|
||
for module_path, module in self._modules.items():
|
||
for name, obj in inspect.getmembers(module):
|
||
if inspect.isclass(obj) and (
|
||
(name.lower() == class_name.lower()) if search_lower else (name == class_name)
|
||
):
|
||
# 将找到的类添加到索引中
|
||
self._classes[name] = obj
|
||
self._classes[f"{module_path}:{name}"] = obj
|
||
return obj
|
||
|
||
return None
|
||
|
||
def get_enhanced_class_info(self, module_path: str, use_dynamic: bool = True) -> Dict[str, Any]:
|
||
"""
|
||
获取增强的类信息,支持动态导入和静态分析
|
||
|
||
Args:
|
||
module_path: 模块路径,格式为 "module.path" 或 "module.path:ClassName"
|
||
use_dynamic: 是否优先使用动态导入
|
||
|
||
Returns:
|
||
包含详细类信息的字典
|
||
"""
|
||
result = {
|
||
"module_path": module_path,
|
||
"dynamic_import_success": False,
|
||
"static_analysis_success": False,
|
||
"init_params": {},
|
||
"status_methods": {}, # get_ 开头和 @property 方法
|
||
"action_methods": {}, # set_ 开头和其他非_开头方法
|
||
}
|
||
|
||
# 尝试动态导入
|
||
dynamic_info = None
|
||
static_info = None
|
||
if use_dynamic:
|
||
try:
|
||
dynamic_info = self._get_dynamic_class_info(module_path)
|
||
result["dynamic_import_success"] = True
|
||
logger.debug(f"[ImportManager] 动态导入类 {module_path} 成功")
|
||
except Exception as e:
|
||
logger.warning(
|
||
f"[UniLab Registry] 在补充注册表时,动态导入类 "
|
||
f"{module_path} 失败(将使用静态分析,"
|
||
f"建议修复导入错误,以实现更好的注册表识别效果!): {e}"
|
||
)
|
||
use_dynamic = False
|
||
if not use_dynamic:
|
||
# 尝试静态分析
|
||
try:
|
||
static_info = self._get_static_class_info(module_path)
|
||
result["static_analysis_success"] = True
|
||
logger.debug(f"[ImportManager] 静态分析类 {module_path} 成功")
|
||
except Exception as e:
|
||
logger.warning(f"[ImportManager] 静态分析类 {module_path} 失败: {e}")
|
||
|
||
# 合并信息(优先使用动态导入的信息)
|
||
if dynamic_info:
|
||
result.update(dynamic_info)
|
||
elif static_info:
|
||
result.update(static_info)
|
||
|
||
return result
|
||
|
||
def _get_dynamic_class_info(self, class_path: str) -> Dict[str, Any]:
|
||
"""使用inspect模块动态获取类信息"""
|
||
cls = get_class(class_path)
|
||
class_name = cls.__name__
|
||
|
||
result = {"class_name": class_name, "init_params": self._analyze_method_signature(cls.__init__)["args"],
|
||
"status_methods": {}, "action_methods": {}}
|
||
# 分析类的所有成员
|
||
for name, method in cls.__dict__.items():
|
||
if name.startswith("_"):
|
||
continue
|
||
|
||
# 检查是否是property
|
||
if isinstance(method, property):
|
||
# @property 装饰的方法
|
||
# noinspection PyTypeChecker
|
||
return_type = self._get_return_type_from_method(method.fget) if method.fget else "Any"
|
||
prop_info = {
|
||
"name": name,
|
||
"return_type": return_type,
|
||
}
|
||
result["status_methods"][name] = prop_info
|
||
|
||
# 检查是否有对应的setter
|
||
if method.fset:
|
||
setter_info = self._analyze_method_signature(method.fset)
|
||
result["action_methods"][name] = setter_info
|
||
|
||
elif inspect.ismethod(method) or inspect.isfunction(method):
|
||
if name.startswith("get_"):
|
||
actual_name = name[4:] # 去掉get_前缀
|
||
if actual_name in result["status_methods"]:
|
||
continue
|
||
# get_ 开头的方法归类为status
|
||
method_info = self._analyze_method_signature(method)
|
||
result["status_methods"][actual_name] = method_info
|
||
elif not name.startswith("_"):
|
||
# 其他非_开头的方法归类为action
|
||
method_info = self._analyze_method_signature(method)
|
||
result["action_methods"][name] = method_info
|
||
|
||
return result
|
||
|
||
def _get_static_class_info(self, module_path: str) -> Dict[str, Any]:
|
||
"""使用AST静态分析获取类信息"""
|
||
module_name, class_name = module_path.rsplit(":", 1)
|
||
# 将模块路径转换为文件路径
|
||
file_path = self._module_path_to_file_path(module_name)
|
||
if not file_path or not os.path.exists(file_path):
|
||
raise FileNotFoundError(f"找不到模块文件: {module_name} -> {file_path}")
|
||
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
source_code = f.read()
|
||
|
||
tree = ast.parse(source_code)
|
||
|
||
# 查找目标类
|
||
target_class = None
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.ClassDef):
|
||
if node.name == class_name:
|
||
target_class = node
|
||
break
|
||
|
||
if target_class is None:
|
||
raise AttributeError(f"在文件 {file_path} 中找不到类 {class_name}")
|
||
|
||
result = {
|
||
"class_name": class_name,
|
||
"init_params": {},
|
||
"status_methods": {},
|
||
"action_methods": {},
|
||
}
|
||
|
||
# 分析类的方法
|
||
for node in target_class.body:
|
||
if isinstance(node, ast.FunctionDef):
|
||
method_info = self._analyze_method_node(node)
|
||
method_name = node.name
|
||
if method_name == "__init__":
|
||
result["init_params"] = method_info["args"]
|
||
elif method_name.startswith("_"):
|
||
continue
|
||
elif self._is_property_method(node):
|
||
# @property 装饰的方法
|
||
result["status_methods"][method_name] = method_info
|
||
elif method_name.startswith("get_"):
|
||
# get_ 开头的方法归类为status
|
||
actual_name = method_name[4:] # 去掉get_前缀
|
||
if actual_name not in result["status_methods"]:
|
||
result["status_methods"][actual_name] = method_info
|
||
else:
|
||
# 其他非_开头的方法归类为action
|
||
result["action_methods"][method_name] = method_info
|
||
return result
|
||
|
||
def _analyze_method_signature(self, method) -> Dict[str, Any]:
|
||
"""
|
||
分析方法签名,提取具体的命名参数信息
|
||
|
||
注意:此方法会跳过*args和**kwargs,只提取具体的命名参数
|
||
这样可以确保通过**dict方式传参时的准确性
|
||
|
||
示例用法:
|
||
method_info = self._analyze_method_signature(some_method)
|
||
params = {"param1": "value1", "param2": "value2"}
|
||
result = some_method(**params) # 安全的参数传递
|
||
"""
|
||
signature = inspect.signature(method)
|
||
args = []
|
||
num_required = 0
|
||
|
||
for param_name, param in signature.parameters.items():
|
||
# 跳过self参数
|
||
if param_name == "self":
|
||
continue
|
||
|
||
# 跳过*args和**kwargs参数
|
||
if param.kind == param.VAR_POSITIONAL: # *args
|
||
continue
|
||
if param.kind == param.VAR_KEYWORD: # **kwargs
|
||
continue
|
||
|
||
is_required = param.default == inspect.Parameter.empty
|
||
if is_required:
|
||
num_required += 1
|
||
|
||
args.append(
|
||
{
|
||
"name": param_name,
|
||
"type": self._get_type_string(param.annotation),
|
||
"required": is_required,
|
||
"default": None if param.default == inspect.Parameter.empty else param.default,
|
||
}
|
||
)
|
||
|
||
return {
|
||
"name": method.__name__,
|
||
"args": args,
|
||
"return_type": self._get_type_string(signature.return_annotation),
|
||
"is_async": inspect.iscoroutinefunction(method),
|
||
}
|
||
|
||
def _get_return_type_from_method(self, method) -> str:
|
||
"""从方法中获取返回类型"""
|
||
signature = inspect.signature(method)
|
||
return self._get_type_string(signature.return_annotation)
|
||
|
||
def _get_type_string(self, annotation) -> Union[str, Tuple[str, Any]]:
|
||
"""将类型注解转换为Class Library中可搜索的类名"""
|
||
if annotation == inspect.Parameter.empty:
|
||
return "Any" # 如果没有注解,返回Any
|
||
if annotation is None:
|
||
return "None" # 明确的None类型
|
||
if hasattr(annotation, "__origin__"):
|
||
# 处理typing模块的类型
|
||
origin = annotation.__origin__
|
||
if origin in (list, set, tuple):
|
||
if hasattr(annotation, "__args__") and annotation.__args__:
|
||
if len(annotation.__args__):
|
||
arg0 = annotation.__args__[0]
|
||
if isinstance(arg0, int):
|
||
return "Int64MultiArray"
|
||
elif isinstance(arg0, float):
|
||
return "Float64MultiArray"
|
||
return "list", self._get_type_string(arg0)
|
||
elif origin is dict:
|
||
return "dict"
|
||
elif origin is Optional:
|
||
return "Unknown"
|
||
return f"Unknown"
|
||
annotation_str = str(annotation)
|
||
# 处理typing模块的复杂类型
|
||
if "typing." in annotation_str:
|
||
# 简化typing类型显示
|
||
return (
|
||
annotation_str.replace("typing.", "")
|
||
if getattr(annotation, "_name", None) is None
|
||
else annotation._name.lower()
|
||
)
|
||
# 如果是类型对象
|
||
if hasattr(annotation, "__name__"):
|
||
# 如果是内置类型
|
||
if annotation.__module__ == "builtins":
|
||
return annotation.__name__
|
||
else:
|
||
# 如果是自定义类,返回完整路径
|
||
return f"{annotation.__module__}:{annotation.__name__}"
|
||
# 如果是typing模块的类型
|
||
elif hasattr(annotation, "_name"):
|
||
return annotation._name
|
||
# 如果是字符串形式的类型注解
|
||
elif isinstance(annotation, str):
|
||
return annotation
|
||
else:
|
||
return annotation_str
|
||
|
||
def _is_property_method(self, node: ast.FunctionDef) -> bool:
|
||
"""检查是否是@property装饰的方法"""
|
||
for decorator in node.decorator_list:
|
||
if isinstance(decorator, ast.Name) and decorator.id == "property":
|
||
return True
|
||
return False
|
||
|
||
def _is_setter_method(self, node: ast.FunctionDef) -> bool:
|
||
"""检查是否是@xxx.setter装饰的方法"""
|
||
for decorator in node.decorator_list:
|
||
if isinstance(decorator, ast.Attribute) and decorator.attr == "setter":
|
||
return True
|
||
return False
|
||
|
||
def _get_property_name_from_setter(self, node: ast.FunctionDef) -> str:
|
||
"""从setter装饰器中获取属性名"""
|
||
for decorator in node.decorator_list:
|
||
if isinstance(decorator, ast.Attribute) and decorator.attr == "setter":
|
||
if isinstance(decorator.value, ast.Name):
|
||
return decorator.value.id
|
||
return node.name
|
||
|
||
def get_class_info_static(self, module_class_path: str) -> Dict[str, Any]:
|
||
"""
|
||
静态分析获取类的方法信息,不需要实际导入模块
|
||
|
||
Args:
|
||
module_class_path: 格式为 "module.path:ClassName" 的字符串
|
||
|
||
Returns:
|
||
包含类方法信息的字典
|
||
"""
|
||
try:
|
||
if ":" not in module_class_path:
|
||
raise ValueError("module_class_path必须是 'module.path:ClassName' 格式")
|
||
|
||
module_path, class_name = module_class_path.rsplit(":", 1)
|
||
|
||
# 将模块路径转换为文件路径
|
||
file_path = self._module_path_to_file_path(module_path)
|
||
if not file_path or not os.path.exists(file_path):
|
||
logger.warning(f"找不到模块文件: {module_path} -> {file_path}")
|
||
return {}
|
||
|
||
# 解析源码
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
source_code = f.read()
|
||
|
||
tree = ast.parse(source_code)
|
||
|
||
# 查找目标类
|
||
class_node = None
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
||
class_node = node
|
||
break
|
||
|
||
if not class_node:
|
||
logger.warning(f"在模块 {module_path} 中找不到类 {class_name}")
|
||
return {}
|
||
|
||
# 分析类的方法
|
||
methods_info = {}
|
||
for node in class_node.body:
|
||
if isinstance(node, ast.FunctionDef):
|
||
method_info = self._analyze_method_node(node)
|
||
methods_info[node.name] = method_info
|
||
|
||
return {
|
||
"class_name": class_name,
|
||
"module_path": module_path,
|
||
"file_path": file_path,
|
||
"methods": methods_info,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"静态分析类 {module_class_path} 时出错: {str(e)}")
|
||
return {}
|
||
|
||
def _module_path_to_file_path(self, module_path: str) -> Optional[str]:
|
||
for path in sys.path:
|
||
potential_path = Path(path) / module_path.replace(".", "/")
|
||
|
||
# 检查是否为包
|
||
if (potential_path / "__init__.py").exists():
|
||
return str(potential_path / "__init__.py")
|
||
|
||
# 检查是否为模块文件
|
||
if (potential_path.parent / f"{potential_path.name}.py").exists():
|
||
return str(potential_path.parent / f"{potential_path.name}.py")
|
||
|
||
return None
|
||
|
||
def _analyze_method_node(self, node: ast.FunctionDef) -> Dict[str, Any]:
|
||
"""分析方法节点,提取参数和返回类型信息"""
|
||
method_info = {
|
||
"name": node.name,
|
||
"args": [],
|
||
"return_type": None,
|
||
"is_async": isinstance(node, ast.AsyncFunctionDef),
|
||
}
|
||
# 获取默认值列表
|
||
defaults = node.args.defaults
|
||
num_defaults = len(defaults)
|
||
|
||
# 计算必需参数数量
|
||
total_args = len(node.args.args)
|
||
num_required = total_args - num_defaults
|
||
|
||
# 提取参数信息
|
||
for i, arg in enumerate(node.args.args):
|
||
if arg.arg == "self":
|
||
continue
|
||
arg_info = {
|
||
"name": arg.arg,
|
||
"type": None,
|
||
"default": None,
|
||
"required": i < num_required,
|
||
}
|
||
|
||
# 提取类型注解
|
||
if arg.annotation:
|
||
arg_info["type"] = ast.unparse(arg.annotation) if hasattr(ast, "unparse") else str(arg.annotation)
|
||
|
||
# 提取默认值并推断类型
|
||
if i >= num_required:
|
||
default_index = i - num_required
|
||
if default_index < len(defaults):
|
||
default_value: Constant = defaults[default_index] # type: ignore
|
||
assert isinstance(default_value, Constant), "暂不支持对非常量类型进行推断,可反馈开源仓库"
|
||
arg_info["default"] = default_value.value
|
||
# 如果没有类型注解,尝试从默认值推断类型
|
||
if not arg_info["type"]:
|
||
arg_info["type"] = self._get_type_string(type(arg_info["default"]))
|
||
method_info["args"].append(arg_info)
|
||
|
||
# 提取返回类型
|
||
if node.returns:
|
||
method_info["return_type"] = ast.unparse(node.returns) if hasattr(ast, "unparse") else str(node.returns)
|
||
|
||
return method_info
|
||
|
||
def _infer_type_from_default(self, node: ast.AST) -> Optional[str]:
|
||
"""从默认值推断参数类型"""
|
||
if isinstance(node, ast.Constant):
|
||
value = node.value
|
||
if isinstance(value, bool):
|
||
return "bool"
|
||
elif isinstance(value, int):
|
||
return "int"
|
||
elif isinstance(value, float):
|
||
return "float"
|
||
elif isinstance(value, str):
|
||
return "str"
|
||
elif value is None:
|
||
return "Optional[Any]"
|
||
elif isinstance(node, ast.List):
|
||
return "List"
|
||
elif isinstance(node, ast.Dict):
|
||
return "Dict"
|
||
elif isinstance(node, ast.Tuple):
|
||
return "Tuple"
|
||
elif isinstance(node, ast.Set):
|
||
return "Set"
|
||
elif isinstance(node, ast.Name):
|
||
# 常见的默认值模式
|
||
if node.id in ["None"]:
|
||
return "Optional[Any]"
|
||
elif node.id in ["True", "False"]:
|
||
return "bool"
|
||
|
||
return None
|
||
|
||
def _infer_types_from_docstring(self, method_info: Dict[str, Any]) -> None:
|
||
"""从docstring中推断参数类型"""
|
||
docstring = method_info.get("docstring", "")
|
||
if not docstring:
|
||
return
|
||
|
||
lines = docstring.split("\n")
|
||
in_args_section = False
|
||
|
||
for line in lines:
|
||
line = line.strip()
|
||
|
||
# 检测Args或Arguments段落
|
||
if line.lower().startswith(("args:", "arguments:")):
|
||
in_args_section = True
|
||
continue
|
||
elif line.startswith(("returns:", "return:", "yields:", "raises:")):
|
||
in_args_section = False
|
||
continue
|
||
elif not line or not in_args_section:
|
||
continue
|
||
|
||
# 解析参数行,格式通常是: param_name (type): description 或 param_name: description
|
||
if ":" in line:
|
||
parts = line.split(":", 1)
|
||
param_part = parts[0].strip()
|
||
|
||
# 提取参数名和类型
|
||
param_name = None
|
||
param_type = None
|
||
|
||
if "(" in param_part and ")" in param_part:
|
||
# 格式: param_name (type)
|
||
param_name = param_part.split("(")[0].strip()
|
||
type_part = param_part.split("(")[1].split(")")[0].strip()
|
||
param_type = type_part
|
||
else:
|
||
# 格式: param_name
|
||
param_name = param_part
|
||
|
||
# 更新对应参数的类型信息
|
||
if param_name:
|
||
for arg_info in method_info["args"]:
|
||
if arg_info["name"] == param_name and not arg_info["type"]:
|
||
if param_type:
|
||
arg_info["inferred_type"] = param_type
|
||
elif not arg_info["inferred_type"]:
|
||
# 从描述中推断类型
|
||
description = parts[1].strip().lower()
|
||
if any(word in description for word in ["path", "file", "directory", "filename"]):
|
||
arg_info["inferred_type"] = "str"
|
||
elif any(
|
||
word in description for word in ["port", "number", "count", "size", "length"]
|
||
):
|
||
arg_info["inferred_type"] = "int"
|
||
elif any(
|
||
word in description for word in ["rate", "ratio", "percentage", "temperature"]
|
||
):
|
||
arg_info["inferred_type"] = "float"
|
||
elif any(word in description for word in ["flag", "enable", "disable", "option"]):
|
||
arg_info["inferred_type"] = "bool"
|
||
|
||
def get_registry_class_info(self, module_class_path: str) -> Dict[str, Any]:
|
||
"""
|
||
获取适用于注册表的类信息,包含完整的类型推断
|
||
|
||
Args:
|
||
module_class_path: 格式为 "module.path:ClassName" 的字符串
|
||
|
||
Returns:
|
||
适用于注册表的类信息字典
|
||
"""
|
||
class_info = self.get_class_info_static(module_class_path)
|
||
if not class_info:
|
||
return {}
|
||
|
||
registry_info = {
|
||
"class_name": class_info["class_name"],
|
||
"module_path": class_info["module_path"],
|
||
"file_path": class_info["file_path"],
|
||
"methods": {},
|
||
"properties": [],
|
||
"init_params": {},
|
||
"action_methods": {},
|
||
}
|
||
|
||
for method_name, method_info in class_info["methods"].items():
|
||
# 分类处理不同类型的方法
|
||
if method_info["is_property"]:
|
||
registry_info["properties"].append(
|
||
{
|
||
"name": method_name,
|
||
"return_type": method_info.get("return_type"),
|
||
"docstring": method_info.get("docstring"),
|
||
}
|
||
)
|
||
elif method_name == "__init__":
|
||
# 处理初始化参数
|
||
init_params = {}
|
||
for arg in method_info["args"]:
|
||
if arg["name"] != "self":
|
||
param_info = {
|
||
"name": arg["name"],
|
||
"type": arg.get("type") or arg.get("inferred_type"),
|
||
"required": arg.get("is_required", True),
|
||
"default": arg.get("default"),
|
||
}
|
||
init_params[arg["name"]] = param_info
|
||
registry_info["init_params"] = init_params
|
||
elif not method_name.startswith("_"):
|
||
# 处理公共方法(可能的action方法)
|
||
action_info = {
|
||
"name": method_name,
|
||
"params": {},
|
||
"return_type": method_info.get("return_type"),
|
||
"docstring": method_info.get("docstring"),
|
||
"num_required": method_info.get("num_required", 0) - 1, # 减去self
|
||
"num_defaults": method_info.get("num_defaults", 0),
|
||
}
|
||
|
||
for arg in method_info["args"]:
|
||
if arg["name"] != "self":
|
||
param_info = {
|
||
"name": arg["name"],
|
||
"type": arg.get("type") or arg.get("inferred_type"),
|
||
"required": arg.get("is_required", True),
|
||
"default": arg.get("default"),
|
||
}
|
||
action_info["params"][arg["name"]] = param_info
|
||
|
||
registry_info["action_methods"][method_name] = action_info
|
||
|
||
return registry_info
|
||
|
||
|
||
# 全局实例,便于直接使用
|
||
default_manager = ImportManager()
|
||
|
||
|
||
def load_module(module_path: str) -> Any:
|
||
"""加载模块的便捷函数"""
|
||
return default_manager.load_module(module_path)
|
||
|
||
|
||
def get_class(class_name: str) -> Type:
|
||
"""获取类的便捷函数"""
|
||
return default_manager.get_class(class_name)
|
||
|
||
|
||
def get_module(module_path: str) -> Any:
|
||
"""获取模块的便捷函数"""
|
||
return default_manager.get_module(module_path)
|
||
|
||
|
||
def init_from_list(module_list: List[str]) -> None:
|
||
"""从模块列表初始化默认管理器"""
|
||
global default_manager
|
||
default_manager = ImportManager(module_list)
|
||
|
||
|
||
def get_class_info_static(module_class_path: str) -> Dict[str, Any]:
|
||
"""静态分析获取类信息的便捷函数"""
|
||
return default_manager.get_class_info_static(module_class_path)
|
||
|
||
|
||
def get_registry_class_info(module_class_path: str) -> Dict[str, Any]:
|
||
"""获取适用于注册表的类信息的便捷函数"""
|
||
return default_manager.get_registry_class_info(module_class_path)
|
||
|
||
|
||
def get_enhanced_class_info(module_path: str, use_dynamic: bool = True) -> Dict[str, Any]:
|
||
"""获取增强的类信息的便捷函数"""
|
||
return default_manager.get_enhanced_class_info(module_path, use_dynamic)
|