mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-04 05:15:10 +00:00
Initial commit
This commit is contained in:
7
unilabos/utils/__init__.py
Normal file
7
unilabos/utils/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from unilabos.utils.log import logger
|
||||
|
||||
# 确保日志配置在导入utils包时自动应用
|
||||
# 这样任何导入utils包或其子模块的代码都会自动配置好日志
|
||||
|
||||
# 导出logger,使其可以通过from unilabos.utils import logger直接导入
|
||||
__all__ = ['logger']
|
||||
21
unilabos/utils/async_util.py
Normal file
21
unilabos/utils/async_util.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
from asyncio import get_event_loop
|
||||
|
||||
from unilabos.utils.log import error
|
||||
|
||||
|
||||
def run_async_func(func, *, loop=None, **kwargs):
|
||||
if loop is None:
|
||||
loop = get_event_loop()
|
||||
|
||||
def _handle_future_exception(fut):
|
||||
try:
|
||||
fut.result()
|
||||
except Exception as e:
|
||||
error(f"异步任务 {func.__name__} 报错了")
|
||||
error(traceback.format_exc())
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(func(**kwargs), loop)
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
return future
|
||||
183
unilabos/utils/banner_print.py
Normal file
183
unilabos/utils/banner_print.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
横幅和UI打印工具
|
||||
|
||||
提供用于显示彩色横幅、状态信息和其他UI元素的工具函数。
|
||||
"""
|
||||
|
||||
import os
|
||||
import platform
|
||||
from datetime import datetime
|
||||
import importlib.metadata
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
# ANSI颜色代码
|
||||
class Colors:
|
||||
"""ANSI颜色代码集合"""
|
||||
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
ITALIC = "\033[3m"
|
||||
UNDERLINE = "\033[4m"
|
||||
|
||||
BLACK = "\033[30m"
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
BLUE = "\033[34m"
|
||||
MAGENTA = "\033[35m"
|
||||
CYAN = "\033[36m"
|
||||
WHITE = "\033[37m"
|
||||
COLOR_HEAD = "\033[38;5;214m"
|
||||
COLOR_DECO = "\033[38;5;242m"
|
||||
COLOR_TAIL = "\033[38;5;220m"
|
||||
|
||||
BRIGHT_BLACK = "\033[90m"
|
||||
BRIGHT_RED = "\033[91m"
|
||||
BRIGHT_GREEN = "\033[92m"
|
||||
BRIGHT_YELLOW = "\033[93m"
|
||||
BRIGHT_BLUE = "\033[94m"
|
||||
BRIGHT_MAGENTA = "\033[95m"
|
||||
BRIGHT_CYAN = "\033[96m"
|
||||
BRIGHT_WHITE = "\033[97m"
|
||||
|
||||
BG_BLACK = "\033[40m"
|
||||
BG_RED = "\033[41m"
|
||||
BG_GREEN = "\033[42m"
|
||||
BG_YELLOW = "\033[43m"
|
||||
BG_BLUE = "\033[44m"
|
||||
BG_MAGENTA = "\033[45m"
|
||||
BG_CYAN = "\033[46m"
|
||||
BG_WHITE = "\033[47m"
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
"""
|
||||
获取ilabos的版本号
|
||||
|
||||
通过importlib.metadata尝试获取包版本。
|
||||
如果失败,返回开发版本号。
|
||||
|
||||
Returns:
|
||||
版本号字符串
|
||||
"""
|
||||
try:
|
||||
return importlib.metadata.version("unilabos")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
return "dev-0.1.0" # 开发版本
|
||||
|
||||
|
||||
def print_unilab_banner(args_dict: Dict[str, Any], show_config: bool = True) -> None:
|
||||
"""
|
||||
打印UNI LAB启动横幅
|
||||
|
||||
Args:
|
||||
args_dict: 命令行参数字典
|
||||
show_config: 是否显示配置信息
|
||||
"""
|
||||
# 检测终端是否支持ANSI颜色
|
||||
if platform.system() == "Windows":
|
||||
os.system("") # 启用Windows终端中的ANSI支持
|
||||
|
||||
# 获取当前时间
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 获取版本号
|
||||
version = get_version()
|
||||
|
||||
# 构建UNI LAB字符艺术
|
||||
banner = f"""{Colors.COLOR_HEAD}
|
||||
██╗ ██╗ ███╗ ██╗ ██╗ ██╗ █████╗ ██████╗ {Colors.COLOR_TAIL}
|
||||
██║ ██║ ████╗ ██║ ██║ ██║ ██╔══██╗ ██╔══██╗
|
||||
██║ ██║ ██╔██╗ ██║ ██║ ██║ ███████║ ██████╔╝
|
||||
██║ ██║ ██║╚██╗██║ ██║ ██║ ██╔══██║ ██╔══██╗
|
||||
╚██████╔╝ ██║ ╚████║ ██║ ██████╗ ██║ ██║ ██████╔╝{Colors.COLOR_DECO}
|
||||
╚════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ {Colors.RESET}"""
|
||||
|
||||
# 显示版本信息
|
||||
system_info = f"""
|
||||
{Colors.YELLOW}Version:{Colors.RESET} {Colors.BRIGHT_GREEN}{version}{Colors.RESET}
|
||||
{Colors.YELLOW}System:{Colors.RESET} {Colors.WHITE}{platform.system()} {platform.release()}{Colors.RESET}
|
||||
{Colors.YELLOW}Python:{Colors.RESET} {Colors.WHITE}{platform.python_version()}{Colors.RESET}
|
||||
{Colors.YELLOW}Time:{Colors.RESET} {Colors.WHITE}{current_time}{Colors.RESET}
|
||||
{Colors.BRIGHT_WHITE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.RESET}"""
|
||||
|
||||
# 打印横幅和系统信息
|
||||
print(banner + system_info)
|
||||
|
||||
# 如果需要,显示配置信息
|
||||
if show_config and args_dict:
|
||||
print_config(args_dict)
|
||||
|
||||
|
||||
def print_config(args_dict: Dict[str, Any]) -> None:
|
||||
"""
|
||||
打印配置信息
|
||||
|
||||
Args:
|
||||
args_dict: 命令行参数字典
|
||||
"""
|
||||
config_info = f"{Colors.BRIGHT_BLUE}{Colors.BOLD}Configuration:{Colors.RESET}\n"
|
||||
|
||||
# 后端信息
|
||||
if "backend" in args_dict:
|
||||
config_info += f"{Colors.CYAN}• Backend:{Colors.RESET} "
|
||||
config_info += f"{Colors.WHITE}{args_dict['backend']}{Colors.RESET}\n"
|
||||
|
||||
# 桥接信息
|
||||
if "app_bridges" in args_dict:
|
||||
config_info += f"{Colors.CYAN}• Bridges:{Colors.RESET} "
|
||||
config_info += f"{Colors.WHITE}{', '.join(args_dict['app_bridges'])}{Colors.RESET}\n"
|
||||
|
||||
# 主机模式
|
||||
if "without_host" in args_dict:
|
||||
mode = "Slave" if args_dict["without_host"] else "Master"
|
||||
config_info += f"{Colors.CYAN}• Host Mode:{Colors.RESET} {Colors.WHITE}{mode}{Colors.RESET}\n"
|
||||
|
||||
# 如果有图或设备信息,显示它们
|
||||
if "graph" in args_dict and args_dict["graph"] is not None:
|
||||
config_info += f"{Colors.CYAN}• Graph:{Colors.RESET} "
|
||||
config_info += f"{Colors.WHITE}{args_dict['graph']}{Colors.RESET}\n"
|
||||
elif "devices" in args_dict and args_dict["devices"] is not None:
|
||||
config_info += f"{Colors.CYAN}• Devices:{Colors.RESET} "
|
||||
config_info += f"{Colors.WHITE}{args_dict['devices']}{Colors.RESET}\n"
|
||||
if "resources" in args_dict and args_dict["resources"] is not None:
|
||||
config_info += f"{Colors.CYAN}• Resources:{Colors.RESET} "
|
||||
config_info += f"{Colors.WHITE}{args_dict['resources']}{Colors.RESET}\n"
|
||||
|
||||
# 控制器配置
|
||||
if "controllers" in args_dict and args_dict["controllers"] is not None:
|
||||
config_info += f"{Colors.CYAN}• Controllers:{Colors.RESET} "
|
||||
config_info += f"{Colors.WHITE}{args_dict['controllers']}{Colors.RESET}\n"
|
||||
|
||||
# 打印结束分隔线
|
||||
config_info += f"{Colors.BRIGHT_WHITE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.RESET}\n"
|
||||
|
||||
print(config_info)
|
||||
|
||||
|
||||
def print_status(message: str, status_type: str = "info") -> None:
|
||||
"""
|
||||
打印带颜色的状态消息
|
||||
|
||||
Args:
|
||||
message: 要打印的消息
|
||||
status_type: 状态类型('info', 'success', 'warning', 'error')
|
||||
"""
|
||||
color = Colors.WHITE
|
||||
prefix = ""
|
||||
|
||||
if status_type == "info":
|
||||
color = Colors.BLUE
|
||||
prefix = "INFO"
|
||||
elif status_type == "success":
|
||||
color = Colors.GREEN
|
||||
prefix = "SUCCESS"
|
||||
elif status_type == "warning":
|
||||
color = Colors.YELLOW
|
||||
prefix = "WARNING"
|
||||
elif status_type == "error":
|
||||
color = Colors.RED
|
||||
prefix = "ERROR"
|
||||
|
||||
print(f"{color}[{prefix}]{Colors.RESET} {message}")
|
||||
138
unilabos/utils/cls_creator.py
Normal file
138
unilabos/utils/cls_creator.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
类实例创建工具
|
||||
|
||||
此模块提供了通过字典配置创建类实例的功能,支持嵌套实例的递归创建。
|
||||
类似于Hydra和Weights & Biases的配置系统。
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import traceback
|
||||
from typing import Any, Dict, TypeVar
|
||||
from unilabos.utils import import_manager, logger
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# 定义实例创建规范的关键字
|
||||
INSTANCE_TYPE_KEY = "_cls" # 类的完整路径
|
||||
INSTANCE_PARAMS_KEY = "_params" # 构造函数参数
|
||||
INSTANCE_ARGS_KEY = "_args" # 位置参数列表(可选)
|
||||
|
||||
|
||||
def is_instance_config(config: Any) -> bool:
|
||||
"""
|
||||
检查配置是否符合实例创建规范
|
||||
|
||||
Args:
|
||||
config: 要检查的配置对象
|
||||
|
||||
Returns:
|
||||
是否符合实例创建规范
|
||||
"""
|
||||
if not isinstance(config, dict):
|
||||
return False
|
||||
|
||||
return INSTANCE_TYPE_KEY in config and INSTANCE_PARAMS_KEY in config
|
||||
|
||||
|
||||
def import_class(class_path: str) -> type:
|
||||
"""
|
||||
根据类路径导入类
|
||||
|
||||
Args:
|
||||
class_path: 类的完整路径,如"pylabrobot.liquid_handling:LiquidHandler"
|
||||
|
||||
Returns:
|
||||
导入的类
|
||||
|
||||
Raises:
|
||||
ImportError: 如果导入失败
|
||||
AttributeError: 如果找不到指定的类
|
||||
"""
|
||||
try:
|
||||
return import_manager.get_class(class_path)
|
||||
except ValueError as e:
|
||||
raise ImportError(f"无法导入类 {class_path}: {str(e)}")
|
||||
except (ImportError, AttributeError) as e:
|
||||
raise ImportError(f"无法导入类 {class_path}: {str(e)}")
|
||||
|
||||
|
||||
def create_instance_from_config(config: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
从配置字典创建实例,递归处理嵌套的实例配置
|
||||
|
||||
Args:
|
||||
config: 配置字典,必须包含_type和_params键
|
||||
|
||||
Returns:
|
||||
创建的实例
|
||||
|
||||
Raises:
|
||||
ValueError: 如果配置不符合规范
|
||||
ImportError: 如果类导入失败
|
||||
"""
|
||||
if not is_instance_config(config):
|
||||
raise ValueError(f"配置不符合实例创建规范: {config}")
|
||||
|
||||
class_path = config[INSTANCE_TYPE_KEY]
|
||||
params = config[INSTANCE_PARAMS_KEY]
|
||||
args = config.get(INSTANCE_ARGS_KEY, [])
|
||||
|
||||
# 递归处理嵌套的实例配置
|
||||
processed_args = []
|
||||
for arg in args:
|
||||
if is_instance_config(arg):
|
||||
processed_args.append(create_instance_from_config(arg))
|
||||
else:
|
||||
processed_args.append(arg)
|
||||
|
||||
processed_params = {}
|
||||
for key, value in params.items():
|
||||
if is_instance_config(value):
|
||||
processed_params[key] = create_instance_from_config(value)
|
||||
elif isinstance(value, list):
|
||||
# 处理列表中的实例配置
|
||||
processed_list = []
|
||||
for item in value:
|
||||
if is_instance_config(item):
|
||||
processed_list.append(create_instance_from_config(item))
|
||||
else:
|
||||
processed_list.append(item)
|
||||
processed_params[key] = processed_list
|
||||
elif isinstance(value, dict) and not is_instance_config(value):
|
||||
# 处理普通字典中可能包含的实例配置
|
||||
processed_dict = {}
|
||||
for k, v in value.items():
|
||||
if is_instance_config(v):
|
||||
processed_dict[k] = create_instance_from_config(v)
|
||||
else:
|
||||
processed_dict[k] = v
|
||||
processed_params[key] = processed_dict
|
||||
else:
|
||||
processed_params[key] = value
|
||||
|
||||
# 导入类并创建实例
|
||||
cls = import_class(class_path)
|
||||
return cls(*processed_args, **processed_params)
|
||||
|
||||
|
||||
def create_config_from_instance(instance: Any, include_args: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
从实例创建配置字典(序列化)
|
||||
|
||||
Args:
|
||||
instance: 要序列化的实例
|
||||
include_args: 是否包含位置参数(通常无法获取)
|
||||
|
||||
Returns:
|
||||
配置字典
|
||||
"""
|
||||
if instance is None:
|
||||
return {}
|
||||
|
||||
# 获取实例的类路径
|
||||
cls = instance.__class__
|
||||
class_path = f"{cls.__module__}.{cls.__name__}"
|
||||
|
||||
# 无法获取实例的构造参数,这里返回空字典
|
||||
# 实际使用时可能需要手动指定或通过其他方式获取
|
||||
return {INSTANCE_TYPE_KEY: class_path, INSTANCE_PARAMS_KEY: {}, INSTANCE_ARGS_KEY: [] if include_args else None}
|
||||
14
unilabos/utils/decorator.py
Normal file
14
unilabos/utils/decorator.py
Normal file
@@ -0,0 +1,14 @@
|
||||
def singleton(cls):
|
||||
"""
|
||||
单例装饰器
|
||||
确保被装饰的类只有一个实例
|
||||
"""
|
||||
instances = {}
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
if cls not in instances:
|
||||
instances[cls] = cls(*args, **kwargs)
|
||||
return instances[cls]
|
||||
|
||||
return get_instance
|
||||
|
||||
0
unilabos/utils/fastapi/__init__.py
Normal file
0
unilabos/utils/fastapi/__init__.py
Normal file
104
unilabos/utils/fastapi/log_adapter.py
Normal file
104
unilabos/utils/fastapi/log_adapter.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
日志适配器模块
|
||||
|
||||
用于将各种框架的日志(如Uvicorn、FastAPI等)统一适配到ilabos的日志系统
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from unilabos.utils.log import debug, info, warning, error, critical
|
||||
|
||||
|
||||
class UvicornLogAdapter:
|
||||
"""Uvicorn日志适配器,将Uvicorn的日志重定向到我们的日志系统"""
|
||||
|
||||
@staticmethod
|
||||
def configure():
|
||||
"""配置Uvicorn的日志系统,使用我们自定义的日志格式"""
|
||||
# 获取uvicorn相关的日志记录器
|
||||
uvicorn_loggers = [
|
||||
logging.getLogger("uvicorn"),
|
||||
logging.getLogger("uvicorn.access"),
|
||||
logging.getLogger("uvicorn.error"),
|
||||
logging.getLogger("fastapi"),
|
||||
]
|
||||
|
||||
# 清除现有处理器
|
||||
for logger_instance in uvicorn_loggers:
|
||||
for handler in logger_instance.handlers[:]:
|
||||
logger_instance.removeHandler(handler)
|
||||
|
||||
# 添加自定义处理器
|
||||
adapter_handler = UvicornToIlabosHandler()
|
||||
|
||||
# 为所有uvicorn日志记录器添加处理器
|
||||
for logger_instance in uvicorn_loggers:
|
||||
logger_instance.addHandler(adapter_handler)
|
||||
# 设置日志级别
|
||||
logger_instance.setLevel(logging.INFO)
|
||||
# 禁止传播到根日志记录器,避免重复输出
|
||||
logger_instance.propagate = False
|
||||
|
||||
|
||||
class UvicornToIlabosHandler(logging.Handler):
|
||||
"""将Uvicorn日志处理为ilabos日志格式的处理器"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.level_map = {
|
||||
logging.DEBUG: debug,
|
||||
logging.INFO: info,
|
||||
logging.WARNING: warning,
|
||||
logging.ERROR: error,
|
||||
logging.CRITICAL: critical,
|
||||
}
|
||||
|
||||
def emit(self, record):
|
||||
"""发送日志记录到ilabos日志系统"""
|
||||
try:
|
||||
msg = self.format(record)
|
||||
log_func = self.level_map.get(record.levelno, info)
|
||||
# 根据日志源添加前缀
|
||||
if record.name.startswith("uvicorn"):
|
||||
prefix = "[Uvicorn] "
|
||||
if record.name == "uvicorn.access":
|
||||
prefix = "[Uvicorn.HTTP] "
|
||||
msg = f"{prefix}{msg}"
|
||||
elif record.name.startswith("fastapi"):
|
||||
msg = f"[FastAPI] {msg}"
|
||||
else:
|
||||
msg = f"{record.name} {msg}"
|
||||
log_func(msg, stack_level=5)
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
def setup_fastapi_logging():
|
||||
"""设置FastAPI/Uvicorn的日志系统"""
|
||||
# 配置Uvicorn的日志
|
||||
UvicornLogAdapter.configure()
|
||||
|
||||
# 返回适合uvicorn.run()的日志配置
|
||||
return {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": "%(message)s",
|
||||
"use_colors": True,
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "unilabos.utils.fastapi.log_adapter.UvicornToIlabosHandler",
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"uvicorn": {"handlers": ["default"], "level": "INFO"},
|
||||
"uvicorn.error": {"handlers": ["default"], "level": "INFO"},
|
||||
"uvicorn.access": {"handlers": ["default"], "level": "INFO"},
|
||||
"fastapi": {"handlers": ["default"], "level": "INFO"},
|
||||
},
|
||||
}
|
||||
195
unilabos/utils/import_manager.py
Normal file
195
unilabos/utils/import_manager.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
导入管理器
|
||||
|
||||
该模块提供了一个动态导入和管理模块的系统,避免误删未使用的导入。
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import importlib
|
||||
import inspect
|
||||
import traceback
|
||||
from typing import Dict, List, Any, Optional, Callable, Type
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ImportManager",
|
||||
"default_manager",
|
||||
"load_module",
|
||||
"get_class",
|
||||
"get_module",
|
||||
"init_from_list",
|
||||
]
|
||||
|
||||
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)
|
||||
# 如果cls_name是builtins中的关键字,则返回对应类
|
||||
if cls_name in builtins.__dict__:
|
||||
return builtins.__dict__[cls_name]
|
||||
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
|
||||
|
||||
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
|
||||
"""
|
||||
# 首先在已索引的类中查找
|
||||
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
|
||||
|
||||
|
||||
# 全局实例,便于直接使用
|
||||
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)
|
||||
315
unilabos/utils/log.py
Normal file
315
unilabos/utils/log.py
Normal file
@@ -0,0 +1,315 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
from datetime import datetime
|
||||
import ctypes
|
||||
import atexit
|
||||
import inspect
|
||||
from typing import Tuple, cast
|
||||
|
||||
|
||||
class CustomRecord:
|
||||
custom_stack_info: Tuple[str, int, str, str]
|
||||
|
||||
|
||||
# Windows颜色支持
|
||||
if platform.system() == "Windows":
|
||||
# 尝试启用Windows终端的ANSI支持
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
# 获取STD_OUTPUT_HANDLE
|
||||
STD_OUTPUT_HANDLE = -11
|
||||
# 启用ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
||||
# 获取当前控制台模式
|
||||
handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
|
||||
mode = ctypes.c_ulong()
|
||||
kernel32.GetConsoleMode(handle, ctypes.byref(mode))
|
||||
# 启用ANSI处理
|
||||
kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
||||
|
||||
# 程序退出时恢复控制台设置
|
||||
@atexit.register
|
||||
def reset_console():
|
||||
kernel32.SetConsoleMode(handle, mode.value)
|
||||
|
||||
|
||||
# 定义不同日志级别的颜色
|
||||
class ColoredFormatter(logging.Formatter):
|
||||
"""自定义日志格式化器,支持颜色输出"""
|
||||
|
||||
# ANSI 颜色代码
|
||||
COLORS = {
|
||||
"RESET": "\033[0m", # 重置
|
||||
"BOLD": "\033[1m", # 加粗
|
||||
"GRAY": "\033[37m", # 灰色
|
||||
"WHITE": "\033[97m", # 白色
|
||||
"BLACK": "\033[30m", # 黑色
|
||||
"DEBUG_LEVEL": "\033[1;36m", # 加粗青色
|
||||
"INFO_LEVEL": "\033[1;32m", # 加粗绿色
|
||||
"WARNING_LEVEL": "\033[1;33m", # 加粗黄色
|
||||
"ERROR_LEVEL": "\033[1;31m", # 加粗红色
|
||||
"CRITICAL_LEVEL": "\033[1;35m", # 加粗紫色
|
||||
"DEBUG_TEXT": "\033[37m", # 灰色
|
||||
"INFO_TEXT": "\033[97m", # 白色
|
||||
"WARNING_TEXT": "\033[33m", # 黄色
|
||||
"ERROR_TEXT": "\033[31m", # 红色
|
||||
"CRITICAL_TEXT": "\033[35m", # 紫色
|
||||
"DATE": "\033[37m", # 日期始终使用灰色
|
||||
}
|
||||
|
||||
def __init__(self, use_colors=True):
|
||||
super().__init__()
|
||||
# 强制启用颜色
|
||||
self.use_colors = use_colors
|
||||
|
||||
def format(self, record):
|
||||
# 检查是否有自定义堆栈信息
|
||||
if hasattr(record, "custom_stack_info") and record.custom_stack_info: # type: ignore
|
||||
r = cast(CustomRecord, record)
|
||||
frame_info = r.custom_stack_info
|
||||
record.filename = frame_info[0]
|
||||
record.lineno = frame_info[1]
|
||||
record.funcName = frame_info[2]
|
||||
if len(frame_info) > 3:
|
||||
record.name = frame_info[3]
|
||||
if not self.use_colors:
|
||||
return self._format_basic(record)
|
||||
|
||||
level_color = self.COLORS.get(f"{record.levelname}_LEVEL", self.COLORS["WHITE"])
|
||||
text_color = self.COLORS.get(f"{record.levelname}_TEXT", self.COLORS["WHITE"])
|
||||
date_color = self.COLORS["DATE"]
|
||||
reset = self.COLORS["RESET"]
|
||||
|
||||
# 日期格式
|
||||
datetime_str = datetime.fromtimestamp(record.created).strftime("%y-%m-%d [%H:%M:%S,%f")[:-3] + "]"
|
||||
|
||||
# 模块和函数信息
|
||||
filename = record.filename.replace(".py", "").split("\\")[-1] # 提取文件名(不含路径和扩展名)
|
||||
if "/" in filename:
|
||||
filename = filename.split("/")[-1]
|
||||
module_path = f"{record.name}.{filename}"
|
||||
func_line = f"{record.funcName}:{record.lineno}"
|
||||
right_info = f" [{func_line}] [{module_path}]"
|
||||
|
||||
# 主要消息
|
||||
main_msg = record.getMessage()
|
||||
|
||||
# 构建基本消息格式
|
||||
formatted_message = (
|
||||
f"{date_color}{datetime_str}{reset} "
|
||||
f"{level_color}[{record.levelname}]{reset} "
|
||||
f"{text_color}{main_msg}"
|
||||
f"{date_color}{right_info}{reset}"
|
||||
)
|
||||
|
||||
# 处理异常信息
|
||||
if record.exc_info:
|
||||
exc_text = self.formatException(record.exc_info)
|
||||
if formatted_message[-1:] != "\n":
|
||||
formatted_message = formatted_message + "\n"
|
||||
formatted_message = formatted_message + text_color + exc_text + reset
|
||||
elif record.stack_info:
|
||||
if formatted_message[-1:] != "\n":
|
||||
formatted_message = formatted_message + "\n"
|
||||
formatted_message = formatted_message + text_color + self.formatStack(record.stack_info) + reset
|
||||
|
||||
return formatted_message
|
||||
|
||||
def _format_basic(self, record):
|
||||
"""基本格式化,不包含颜色"""
|
||||
datetime_str = datetime.fromtimestamp(record.created).strftime("%y-%m-%d [%H:%M:%S,%f")[:-3] + "]"
|
||||
filename = os.path.basename(record.filename).rsplit(".", 1)[0] # 提取文件名(不含路径和扩展名)
|
||||
module_path = f"{record.name}.{filename}"
|
||||
func_line = f"{record.funcName}:{record.lineno}"
|
||||
|
||||
formatted_message = f"{datetime_str} [{record.levelname}] [{module_path}] [{func_line}]: {record.getMessage()}"
|
||||
|
||||
if record.exc_info:
|
||||
exc_text = self.formatException(record.exc_info)
|
||||
if formatted_message[-1:] != "\n":
|
||||
formatted_message = formatted_message + "\n"
|
||||
formatted_message = formatted_message + exc_text
|
||||
elif record.stack_info:
|
||||
if formatted_message[-1:] != "\n":
|
||||
formatted_message = formatted_message + "\n"
|
||||
formatted_message = formatted_message + self.formatStack(record.stack_info)
|
||||
|
||||
return formatted_message
|
||||
|
||||
def formatException(self, exc_info):
|
||||
"""重写异常格式化,确保异常信息保持正确的格式和颜色"""
|
||||
# 获取标准的异常格式化文本
|
||||
formatted_exc = super().formatException(exc_info)
|
||||
return formatted_exc
|
||||
|
||||
|
||||
# 配置日志处理器
|
||||
def configure_logger():
|
||||
"""配置日志记录器"""
|
||||
# 获取根日志记录器
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG) # 修改为DEBUG以显示所有级别
|
||||
|
||||
# 移除已存在的处理器
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
# 创建控制台处理器
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.DEBUG) # 修改为DEBUG以显示所有级别
|
||||
|
||||
# 使用自定义的颜色格式化器
|
||||
color_formatter = ColoredFormatter()
|
||||
console_handler.setFormatter(color_formatter)
|
||||
|
||||
# 添加处理器到根日志记录器
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
|
||||
# 配置日志系统
|
||||
configure_logger()
|
||||
|
||||
# 获取日志记录器
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 获取调用栈信息的工具函数
|
||||
def _get_caller_info(stack_level=0) -> Tuple[str, int, str, str]:
|
||||
"""
|
||||
获取调用者的信息
|
||||
|
||||
Args:
|
||||
stack_level: 堆栈回溯的级别,0表示当前函数,1表示调用者,依此类推
|
||||
|
||||
Returns:
|
||||
(filename, line_number, function_name, module_name) 元组
|
||||
"""
|
||||
# 堆栈级别需要加3:
|
||||
# +1 因为这个函数本身占一层
|
||||
# +1 因为日志函数(debug, info等)占一层
|
||||
# +1 因为下面调用 inspect.stack() 也占一层
|
||||
frame = inspect.currentframe()
|
||||
try:
|
||||
# 跳过适当的堆栈帧
|
||||
for _ in range(stack_level + 3):
|
||||
if frame and frame.f_back:
|
||||
frame = frame.f_back
|
||||
else:
|
||||
break
|
||||
|
||||
if frame:
|
||||
filename = frame.f_code.co_filename if frame.f_code else "unknown"
|
||||
line_number = frame.f_lineno if hasattr(frame, "f_lineno") else 0
|
||||
function_name = frame.f_code.co_name if frame.f_code else "unknown"
|
||||
|
||||
# 获取模块名称
|
||||
module_name = "unknown"
|
||||
if frame.f_globals and "__name__" in frame.f_globals:
|
||||
module_name = frame.f_globals["__name__"].rsplit(".", 1)[0]
|
||||
|
||||
return (filename, line_number, function_name, module_name)
|
||||
return ("unknown", 0, "unknown", "unknown")
|
||||
finally:
|
||||
del frame # 避免循环引用
|
||||
|
||||
|
||||
# 便捷日志记录函数
|
||||
def debug(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录DEBUG级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.debug的其他参数
|
||||
"""
|
||||
# 获取调用者信息
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.debug(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def info(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录INFO级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.info的其他参数
|
||||
"""
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.info(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def warning(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录WARNING级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.warning的其他参数
|
||||
"""
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.warning(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def error(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录ERROR级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.error的其他参数
|
||||
"""
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.error(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def critical(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录CRITICAL级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.critical的其他参数
|
||||
"""
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.critical(msg, *args, **kwargs)
|
||||
|
||||
|
||||
# 测试日志输出(如果直接运行此文件)
|
||||
if __name__ == "__main__":
|
||||
print("测试不同日志级别的颜色输出:")
|
||||
debug("这是一条调试日志 (DEBUG级别显示为蓝色,其他文本为灰色)")
|
||||
info("这是一条信息日志 (INFO级别显示为绿色,其他文本为白色)")
|
||||
warning("这是一条警告日志 (WARNING级别显示为黄色,其他文本也为黄色)")
|
||||
error("这是一条错误日志 (ERROR级别显示为红色,其他文本也为红色)")
|
||||
critical("这是一条严重错误日志 (CRITICAL级别显示为紫色,其他文本也为紫色)")
|
||||
# 测试异常输出
|
||||
try:
|
||||
1 / 0
|
||||
except Exception as e:
|
||||
error(f"发生错误: {e}", exc_info=True)
|
||||
161
unilabos/utils/pywinauto_util.py
Normal file
161
unilabos/utils/pywinauto_util.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import psutil
|
||||
import pywinauto
|
||||
from pywinauto_recorder import UIApplication
|
||||
from pywinauto_recorder.player import UIPath, click, focus_on_application, exists, find, get_wrapper_path
|
||||
from pywinauto.controls.uiawrapper import UIAWrapper
|
||||
from pywinauto.application import WindowSpecification
|
||||
from pywinauto import findbestmatch
|
||||
import sys
|
||||
import codecs
|
||||
import os
|
||||
import locale
|
||||
|
||||
|
||||
def connect_application(backend="uia", **kwargs):
|
||||
app = pywinauto.Application(backend=backend)
|
||||
app.connect(**kwargs)
|
||||
top_window = app.top_window().wrapper_object()
|
||||
native_window_handle = top_window.handle
|
||||
return UIApplication(app, native_window_handle)
|
||||
|
||||
def get_ui_path_with_window_specification(obj):
|
||||
return UIPath(get_wrapper_path(obj))
|
||||
|
||||
def get_process_pid_by_name(process_name: str, min_memory_mb: float = 0) -> tuple[bool, int]:
|
||||
"""
|
||||
通过进程名称和最小内存要求获取进程PID
|
||||
|
||||
Args:
|
||||
process_name: 进程名称
|
||||
min_memory_mb: 最小内存要求(MB),默认为0表示不检查内存
|
||||
|
||||
Returns:
|
||||
tuple[bool, int]: (是否找到进程, 进程PID)
|
||||
"""
|
||||
process_found = False
|
||||
process_pid = None
|
||||
min_memory_bytes = min_memory_mb * 1024 * 1024 # 转换为字节
|
||||
|
||||
try:
|
||||
for proc in psutil.process_iter(['name', 'pid', 'memory_info']):
|
||||
try:
|
||||
# 获取进程信息
|
||||
proc_info = proc.info
|
||||
if process_name in proc_info['name']:
|
||||
# 如果设置了内存限制,则检查内存
|
||||
if min_memory_mb > 0:
|
||||
memory_info = proc_info.get('memory_info')
|
||||
if memory_info and memory_info.rss > min_memory_bytes:
|
||||
process_found = True
|
||||
process_pid = proc_info['pid']
|
||||
break
|
||||
else:
|
||||
# 不检查内存,直接返回找到的进程
|
||||
process_found = True
|
||||
process_pid = proc_info['pid']
|
||||
break
|
||||
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取进程信息时发生错误: {str(e)}")
|
||||
|
||||
return process_found, process_pid
|
||||
|
||||
def print_wrapper_identifiers(wrapper_object, depth=None, filename=None):
|
||||
"""
|
||||
打印控件及其子控件的标识信息
|
||||
|
||||
Args:
|
||||
wrapper_object: UIAWrapper对象
|
||||
depth: 打印的最大深度,None表示打印全部
|
||||
filename: 输出文件名,None表示打印到控制台
|
||||
"""
|
||||
if depth is None:
|
||||
depth = sys.maxsize
|
||||
|
||||
# 创建所有控件的列表(当前控件及其所有子代)
|
||||
all_ctrls = [wrapper_object, ] + wrapper_object.descendants()
|
||||
|
||||
# 创建所有可见文本控件的列表
|
||||
txt_ctrls = [ctrl for ctrl in all_ctrls if ctrl.can_be_label and ctrl.is_visible() and ctrl.window_text()]
|
||||
|
||||
# 构建唯一的控件名称字典
|
||||
name_ctrl_id_map = findbestmatch.UniqueDict()
|
||||
for index, ctrl in enumerate(all_ctrls):
|
||||
ctrl_names = findbestmatch.get_control_names(ctrl, all_ctrls, txt_ctrls)
|
||||
for name in ctrl_names:
|
||||
name_ctrl_id_map[name] = index
|
||||
|
||||
# 反转映射关系(控件索引到名称列表)
|
||||
ctrl_id_name_map = {}
|
||||
for name, index in name_ctrl_id_map.items():
|
||||
ctrl_id_name_map.setdefault(index, []).append(name)
|
||||
|
||||
def print_identifiers(ctrls, current_depth=1, log_func=print):
|
||||
"""递归打印控件及其子代的标识信息"""
|
||||
if len(ctrls) == 0 or current_depth > depth:
|
||||
return
|
||||
|
||||
indent = (current_depth - 1) * u" | "
|
||||
for ctrl in ctrls:
|
||||
try:
|
||||
ctrl_id = all_ctrls.index(ctrl)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
ctrl_text = ctrl.window_text()
|
||||
if ctrl_text:
|
||||
# 将多行文本转换为单行
|
||||
ctrl_text = ctrl_text.replace('\n', r'\n').replace('\r', r'\r')
|
||||
|
||||
output = indent + u'\n'
|
||||
output += indent + u"{class_name} - '{text}' {rect}\n"\
|
||||
"".format(class_name=ctrl.friendly_class_name(),
|
||||
text=ctrl_text,
|
||||
rect=ctrl.rectangle())
|
||||
output += indent + u'{}'.format(ctrl_id_name_map[ctrl_id])
|
||||
|
||||
title = ctrl_text
|
||||
class_name = ctrl.class_name()
|
||||
auto_id = None
|
||||
control_type = None
|
||||
if hasattr(ctrl.element_info, 'automation_id'):
|
||||
auto_id = ctrl.element_info.automation_id
|
||||
if hasattr(ctrl.element_info, 'control_type'):
|
||||
control_type = ctrl.element_info.control_type
|
||||
if control_type:
|
||||
class_name = None # 如果有control_type就不需要class_name
|
||||
else:
|
||||
control_type = None # 如果control_type为空,仍使用class_name
|
||||
|
||||
criteria_texts = []
|
||||
recorder_texts = []
|
||||
if title:
|
||||
criteria_texts.append(u'title="{}"'.format(title))
|
||||
recorder_texts.append(f"{title}")
|
||||
if class_name:
|
||||
criteria_texts.append(u'class_name="{}"'.format(class_name))
|
||||
if auto_id:
|
||||
criteria_texts.append(u'auto_id="{}"'.format(auto_id))
|
||||
if control_type:
|
||||
criteria_texts.append(u'control_type="{}"'.format(control_type))
|
||||
recorder_texts.append(f"||{control_type}")
|
||||
if title or class_name or auto_id:
|
||||
output += u'\n' + indent + u'child_window(' + u', '.join(criteria_texts) + u')' + " / " + "".join(recorder_texts)
|
||||
|
||||
log_func(output)
|
||||
print_identifiers(ctrl.children(), current_depth + 1, log_func)
|
||||
|
||||
if filename is None:
|
||||
print("Control Identifiers:")
|
||||
print_identifiers([wrapper_object, ])
|
||||
else:
|
||||
log_file = codecs.open(filename, "w", locale.getpreferredencoding())
|
||||
def log_func(msg):
|
||||
log_file.write(str(msg) + os.linesep)
|
||||
log_func("Control Identifiers:")
|
||||
print_identifiers([wrapper_object, ], log_func=log_func)
|
||||
log_file.close()
|
||||
|
||||
4
unilabos/utils/tools.py
Normal file
4
unilabos/utils/tools.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# 辅助函数:将UUID数组转换为字符串
|
||||
def uuid_to_str(uuid_array) -> str:
|
||||
"""将UUID字节数组转换为十六进制字符串"""
|
||||
return "".join(format(byte, "02x") for byte in uuid_array)
|
||||
23
unilabos/utils/type_check.py
Normal file
23
unilabos/utils/type_check.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import collections
|
||||
import json
|
||||
from typing import get_origin, get_args
|
||||
|
||||
|
||||
def get_type_class(type_hint):
|
||||
origin = get_origin(type_hint)
|
||||
if origin is not None and issubclass(origin, collections.abc.Sequence):
|
||||
final_type = [get_args(type_hint)[0]] # 默认sequence中类型都一样
|
||||
else:
|
||||
final_type = type_hint
|
||||
return final_type
|
||||
|
||||
|
||||
class TypeEncoder(json.JSONEncoder):
|
||||
"""自定义JSON编码器处理特殊类型"""
|
||||
|
||||
def default(self, obj):
|
||||
# 优先处理类型对象
|
||||
if isinstance(obj, type):
|
||||
return str(obj)[8:-2]
|
||||
return super().default(obj)
|
||||
|
||||
Reference in New Issue
Block a user