Initial commit

This commit is contained in:
Junhan Chang
2025-04-17 15:19:47 +08:00
parent a47a3f5c3a
commit c78ac482d8
262 changed files with 39871 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
from unilabos.utils.log import logger
# 确保日志配置在导入utils包时自动应用
# 这样任何导入utils包或其子模块的代码都会自动配置好日志
# 导出logger使其可以通过from unilabos.utils import logger直接导入
__all__ = ['logger']

View 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

View 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}")

View 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}

View 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

View File

View 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"},
},
}

View 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
View 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)

View 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
View File

@@ -0,0 +1,4 @@
# 辅助函数将UUID数组转换为字符串
def uuid_to_str(uuid_array) -> str:
"""将UUID字节数组转换为十六进制字符串"""
return "".join(format(byte, "02x") for byte in uuid_array)

View 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)