From 697e9157669e467f41c5cd4b542aee861b320712 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Sun, 13 Aug 2023 12:28:01 +0800 Subject: refactor: brandly new edition --- HydroRollCore/__init__.py | 6 +- HydroRollCore/config.py | 76 ++++++++++++++++++ HydroRollCore/core.py | 76 ++++++++++++++++++ HydroRollCore/exception.py | 0 HydroRollCore/exceptions.py | 32 ++++++++ HydroRollCore/home.html | 111 ------------------------- HydroRollCore/log.py | 30 +++++++ HydroRollCore/main.html | 27 ------- HydroRollCore/rule.py | 79 +++++------------- HydroRollCore/typing.py | 0 HydroRollCore/typing_.py | 23 ++++++ HydroRollCore/utils.py | 192 ++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 452 insertions(+), 200 deletions(-) create mode 100644 HydroRollCore/core.py delete mode 100644 HydroRollCore/exception.py create mode 100644 HydroRollCore/exceptions.py delete mode 100644 HydroRollCore/home.html delete mode 100644 HydroRollCore/main.html delete mode 100644 HydroRollCore/typing.py create mode 100644 HydroRollCore/typing_.py (limited to 'HydroRollCore') diff --git a/HydroRollCore/__init__.py b/HydroRollCore/__init__.py index 7133a630..48f904b0 100644 --- a/HydroRollCore/__init__.py +++ b/HydroRollCore/__init__.py @@ -1,5 +1,7 @@ name = "HydroRollCore" -from HydroRollCore.rule import Rule +from HydroRollCore.core import Core +from HydroRollCore.rule import Rule, RuleLoadType +from HydroRollCore.config import ConfigModel -__all__ = ['Rule'] \ No newline at end of file +__all__ = ['Core', 'Rule', 'ConfigModel', 'RuleLoadType'] \ No newline at end of file diff --git a/HydroRollCore/config.py b/HydroRollCore/config.py index e69de29b..4bee29df 100644 --- a/HydroRollCore/config.py +++ b/HydroRollCore/config.py @@ -0,0 +1,76 @@ +"""HydroRollCore 配置。 + +HydroRollCore 使用 [pydantic](https://pydantic-docs.helpmanual.io/) 来读取配置。 +""" +from typing import Set, Union + +from pydantic import Extra, Field, BaseModel, DirectoryPath + +__all__ = [ + "ConfigModel", + "LogConfig", + "CoreConfig", + "RuleConfig", + "MainConfig", +] + + +class ConfigModel(BaseModel): + """HydroRollCore 配置模型。 + + Attributes: + __config_name__: 配置名称。 + """ + + __config_name__: str + + class Config: + extra = Extra.allow + + +class LogConfig(ConfigModel): + """HydroRollCore 日志相关设置。 + + Attributes: + level: 日志级别。 + verbose_exception: 详细的异常记录,设置为 True 时会在日志中添加异常的 Traceback。 + """ + + level: Union[str, int] = "DEBUG" + verbose_exception: bool = False + + +class CoreConfig(ConfigModel): + """Core 配置。 + + Attributes: + rules: 将被加载的规则书列表,将被 `Core` 类的 `load_rules()` 方法加载。 + rule_dirs: 将被加载的规则书目录列表,将被 `Core` 类的 `load_rules_from_dirs()` 方法加载。 + log: HydroRollCore 日志相关设置。 + """ + + rules: Set[str] = Field(default_factory=set) + rule_dirs: Set[DirectoryPath] = Field(default_factory=set) + log: LogConfig = LogConfig() # type: ignore + + +class RuleConfig(ConfigModel): + """规则书配置。""" + + +class DebugConfig(ConfigModel): + """是否打印事件配置。""" + + +class MainConfig(ConfigModel): + """HydroRollCore 配置。 + + Attributes: + core: HydroRollCore 的主要配置。 + """ + + core: CoreConfig = CoreConfig() # type: ignore + rule: RuleConfig = RuleConfig() # type: ignore + + class Config: + extra = Extra.allow \ No newline at end of file diff --git a/HydroRollCore/core.py b/HydroRollCore/core.py new file mode 100644 index 00000000..9016eda8 --- /dev/null +++ b/HydroRollCore/core.py @@ -0,0 +1,76 @@ +"""HydroRollCore 核心程序""" + +import asyncio +from collections import defaultdict +import sys + +from typing import Any, List, Optional, Type, Dict + +from HydroRollCore.config import MainConfig +from HydroRollCore.rule import Rule, RuleLoadType + +__all__ = ['Core'] + +class Core: + """HydroRollCore 核心对象,定义了核心的基本方法。 + 读取并储存配置 `Config`,加载规则包 `Rule`,并进行事件分发。 + + Attributes: + config: 核心配置。 + should_exit: 核心是否应该进入准备退出状态。 + rules: 当前已经加载的规则包的列表。 + rules_priority_dict: 规则包优先级字典。 + rule_state: 规则包状态。 + global_state: 全局状态。 + """ + + config: MainConfig + should_exit: asyncio.Event + rules: List[Rule] + rules_priority_dict: Dict[int, List[Type[Rule]]] + rule_state: Dict[str, Any] + global_state: Dict[Any, Any] + + def __init__( + self, + *, + config_file: Optional[str] = "config.toml", + config_dict: Optional[Dict] = None, + hot_reload: bool = False, + ): + """初始化 iamai ,读取配置文件,创建配置,加载规则包。 + + Args: + config_file: 配置文件,如不指定则使用默认的 `config.toml`。 + 若指定为 None,则不加载配置文件。 + config_dict: 配置字典,默认为 None。 + 若指定字典,则会忽略 config_file 配置,不再读取配置文件。 + hot_reload: 热重载。 + 启用后将自动检查 `rule_dir` 中的规则包文件更新,并在更新时自动重新加载。 + """ + self.config = MainConfig() # type: ignore[assignment] + self.plugins_priority_dict = defaultdict(list) + self.plugin_state = defaultdict(type(None)) # type: ignore[assignment] + self.global_state = {} + + self.adapters = [] + self._restart_flag = False + self._module_path_finder = ModulePathFinder() + self._raw_config_dict = {} + + self._config_file = config_file + self._config_dict = config_dict + self._hot_reload = hot_reload + + self._extend_plugins = [] + self._extend_plugin_dirs = [] + self._extend_adapters = [] + self._bot_run_hooks = [] + self._bot_exit_hooks = [] + self._adapter_startup_hooks = [] + self._adapter_run_hooks = [] + self._adapter_shutdown_hooks = [] + self._event_preprocessor_hooks = [] + self._event_postprocessor_hooks = [] + + sys.meta_path.insert(0, self._module_path_finder) \ No newline at end of file diff --git a/HydroRollCore/exception.py b/HydroRollCore/exception.py deleted file mode 100644 index e69de29b..00000000 diff --git a/HydroRollCore/exceptions.py b/HydroRollCore/exceptions.py new file mode 100644 index 00000000..ae49fed2 --- /dev/null +++ b/HydroRollCore/exceptions.py @@ -0,0 +1,32 @@ +"""HydroRollCore 异常。 + +下列是 HydroRollCore 运行过程中可能会抛出的异常。这些异常大部分不需要用户处理,HydroRollCore 会自动捕获并处理。 +""" + + +class EventException(BaseException): + """事件处理过程中由插件抛出的异常,用于控制事件的传播,会被 iamai 自动捕获并处理。""" + + +class SkipException(EventException): + """跳过当前插件继续当前事件传播。""" + + +class StopException(EventException): + """停止当前事件传播。""" + + +class iamaiException(Exception): + """所有 iamai 发生的异常的基类。""" + + +class GetEventTimeout(iamaiException): + """当 get 方法超时使被抛出。""" + + +class AdapterException(iamaiException): + """由适配器抛出的异常基类,所有适配器抛出的异常都应该继承自此类。""" + + +class LoadModuleError(iamaiException): + """加载模块错误,在指定模块中找不到特定类型的类或模块中存在多个符合条件的类时抛出。""" \ No newline at end of file diff --git a/HydroRollCore/home.html b/HydroRollCore/home.html deleted file mode 100644 index 79b18ba8..00000000 --- a/HydroRollCore/home.html +++ /dev/null @@ -1,111 +0,0 @@ - - -{% extends "main.html" %} - - -{% block tabs %} - {{ super() }} - - - - - -
-
-
- - -
- -
- - -
-

HydroRollCore

- HydroRollCore 是一个规则包读取与处理分发用工具,通过读取一个一个规则包实现水系骰子掷骰规则,同时支持模板创建与Wiki站点辅助设计规则书内容。
- 它是世界主入门水系的关键。
- - 快速开始 - - - 了解通用规则包标准 - -
-
-
-
-{% endblock %} - - -{% block content %} - -{% endblock %} - - -{% block footer %} - -{% endblock %} \ No newline at end of file diff --git a/HydroRollCore/log.py b/HydroRollCore/log.py index e69de29b..19073fc3 100644 --- a/HydroRollCore/log.py +++ b/HydroRollCore/log.py @@ -0,0 +1,30 @@ +"""HydroRollCore 日志。 + +HydroRollCore 使用 [loguru](https://github.com/Delgan/loguru) 来记录日志信息。 +自定义 logger 请参考 [loguru](https://github.com/Delgan/loguru) 文档。 +""" +import os +import sys +from datetime import datetime + +from loguru import logger as _logger + +logger = _logger + +current_path = os.path.dirname(os.path.abspath("__file__")) +log_path = os.path.join( + current_path, "logs", datetime.now().strftime("%Y-%m-%d") + ".log" +) + + +def error_or_exception(message: str, exception: Exception, verbose: bool): + logger.remove() + logger.add( + sys.stderr, + format="{time:YYYY-MM-DD HH:mm:ss.SSS} [{level}] > {name}:{function}:{line} - {message}", + ) + logger.add(sink=log_path, level="INFO", rotation="10 MB") # 每个日志文件最大为 10MB + if verbose: + logger.exception(message) + else: + logger.critical(f"{message} {exception!r}") \ No newline at end of file diff --git a/HydroRollCore/main.html b/HydroRollCore/main.html deleted file mode 100644 index da3b2334..00000000 --- a/HydroRollCore/main.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - {{ super() }} - - - - {% block footer %} - - {% endblock %} - -{% endblock %} diff --git a/HydroRollCore/rule.py b/HydroRollCore/rule.py index 8a72c8c6..8e10b18b 100644 --- a/HydroRollCore/rule.py +++ b/HydroRollCore/rule.py @@ -1,22 +1,14 @@ -"""iamai 插件。 - -所有 iamai 插件的基类。所有用户编写的插件必须继承自 `Plugin` 类。 -""" -from enum import Enum from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Type, Generic, NoReturn, Optional - -from hydrorollcore.config import ConfigModel -from hydrorollcore.utils import is_config_class -from hydrorollcore.typing import T_Event, T_State, T_Config -from hydrorollcore.exceptions import SkipException, StopException +import os, os.path +from enum import Enum +from iamai.config import ConfigModel +from iamai.utils import is_config_class +from typing import Generic, NoReturn, Optional, Type, TYPE_CHECKING +from HydroRoll.typing import T_Event, T_Config if TYPE_CHECKING: from iamai.bot import Bot - -__all__ = ["Rule", "RuleLoadType"] - - + class RuleLoadType(Enum): """插件加载类型。""" @@ -24,35 +16,32 @@ class RuleLoadType(Enum): NAME = "name" FILE = "file" CLASS = "class" + - -class Rule(ABC, Generic[T_Event, T_State, T_Config]): +class Rule(ABC, Generic[T_Event, T_Config]): """所有 iamai 插件的基类。 Attributes: event: 当前正在被此插件处理的事件。 priority: 插件的优先级,数字越小表示优先级越高,默认为 0。 block: 插件执行结束后是否阻止事件的传播。True 表示阻止。 - __plugin_load_type__: 插件加载类型,由 iamai 自动设置,反映了此插件是如何被加载的。 - __plugin_file_path__: 当插件加载类型为 `PluginLoadType.CLASS` 时为 `None`, + __rule_load_type__: 插件加载类型,由 iamai 自动设置,反映了此插件是如何被加载的。 + __rule_file_path__: 当插件加载类型为 `RuleLoadType.CLASS` 时为 `None`, 否则为定义插件在的 Python 模块的位置。 """ event: T_Event priority: int = 0 - block: bool = False Config: Type[ConfigModel] - __plugin_load_type__: PluginLoadType - __plugin_file_path__: Optional[str] + __rule_load_type__: RuleLoadType + __rule_file_path__: Optional[str] def __init__(self, event: T_Event): self.event = event if not hasattr(self, "priority"): self.priority = 0 - if not hasattr(self, "block"): - self.block = False self.get = self.bot.get @@ -64,48 +53,18 @@ class Rule(ABC, Generic[T_Event, T_State, T_Config]): @property def name(self) -> str: - """插件类名称。""" + """规则类名称。""" return self.__class__.__name__ @property def bot(self) -> "Bot": """机器人对象。""" return self.event.adapter.bot - + @property def config(self) -> Optional[T_Config]: - """插件配置。""" - config_class: ConfigModel = getattr(self, "Config", None) + """规则包配置。""" + config_class: ConfigModel = getattr(self, "Rule", None) if is_config_class(config_class): - return getattr(self.bot.config.plugin, config_class.__config_name__, None) - return None - - def stop(self) -> NoReturn: - """停止当前事件传播。""" - raise StopException() - - def skip(self) -> NoReturn: - """跳过自身继续当前事件传播。""" - raise SkipException() - - @property - def state(self) -> T_State: - """插件状态。""" - return self.bot.plugin_state[self.name] - - @state.setter - def state(self, value: T_State): - self.bot.plugin_state[self.name] = value - - @abstractmethod - async def handle(self) -> None: - """处理事件的方法。当 `rule()` 方法返回 `True` 时 iamai 会调用此方法。每个插件必须实现此方法。""" - raise NotImplementedError - - @abstractmethod - async def rule(self) -> bool: - """匹配事件的方法。事件处理时,会按照插件的优先级依次调用此方法,当此方法返回 `True` 时将事件交由此插件处理。每个插件必须实现此方法。 - - 注意:不建议直接在此方法内实现对事件的处理,事件的具体处理请交由 `handle()` 方法。 - """ - raise NotImplementedError \ No newline at end of file + return getattr(self.config.rule, config_class.__config_name__, None) + return None \ No newline at end of file diff --git a/HydroRollCore/typing.py b/HydroRollCore/typing.py deleted file mode 100644 index e69de29b..00000000 diff --git a/HydroRollCore/typing_.py b/HydroRollCore/typing_.py new file mode 100644 index 00000000..0b4a78ba --- /dev/null +++ b/HydroRollCore/typing_.py @@ -0,0 +1,23 @@ +"""HydroRollCore 类型提示支持。 + +此模块定义了部分 HydroRollCore 使用的类型。 +""" + +from typing import TYPE_CHECKING, TypeVar + +if TYPE_CHECKING: + from HydroRollCore.core import Core # noqa + from HydroRollCore.rule import Rule # noqa + from HydroRollCore.config import ConfigModel # noqa + +__all__ = [ + "T_State", + "T_Core", + "T_Rule", + "T_Config" +] + +T_State = TypeVar("T_State") +T_Core = TypeVar("T_Core", bound="Core") +T_Rule = TypeVar("T_Rule", bound="Rule") +T_Config = TypeVar("T_Config", bound="ConfigModel") \ No newline at end of file diff --git a/HydroRollCore/utils.py b/HydroRollCore/utils.py index e69de29b..c69fdd48 100644 --- a/HydroRollCore/utils.py +++ b/HydroRollCore/utils.py @@ -0,0 +1,192 @@ +import os +import json +import asyncio +import inspect +import os.path +import pkgutil +import importlib +import traceback +import dataclasses +from abc import ABC +from types import ModuleType +from functools import partial +from typing_extensions import ParamSpec +from importlib.abc import MetaPathFinder +from importlib.machinery import PathFinder +from typing import Any, List, Type, Tuple, TypeVar, Callable, Iterable, Coroutine + +from HydroRollCore.config import ConfigModel + +__all__ = [ + "ModulePathFinder", + "is_config_class", + "get_classes_from_module", + "get_classes_from_module_name", + "get_classes_from_dir", + "DataclassEncoder", + "samefile", + "sync_func_wrapper", +] + +_T = TypeVar("_T") +_P = ParamSpec("_P") +_R = TypeVar("_R") + + +class ModulePathFinder(MetaPathFinder): + """用于查找 HydroRollCore 组件的元路径查找器。""" + + path: List[str] = [] + + def find_spec(self, fullname, path=None, target=None): + if path is None: + path = [] + return PathFinder.find_spec(fullname, self.path + list(path), target) + + +def is_config_class(config_class: Any) -> bool: + """判断一个对象是否是配置类。 + + Args: + config_class: 待判断的对象。 + + Returns: + 返回是否是配置类。 + """ + return ( + inspect.isclass(config_class) + and issubclass(config_class, ConfigModel) + and isinstance(getattr(config_class, "__config_name__", None), str) + and ABC not in config_class.__bases__ + and not inspect.isabstract(config_class) + ) + + +def get_classes_from_module( + module: ModuleType, super_class: Type[_T] +) -> List[Type[_T]]: + """从模块中查找指定类型的类。 + + Args: + module: Python 模块。 + super_class: 要查找的类的超类。 + + Returns: + 返回符合条件的类的列表。 + """ + classes: List[Type[_T]] = [] + for _, module_attr in inspect.getmembers(module, inspect.isclass): + module_attr: type + if ( + (inspect.getmodule(module_attr) or module) is module + and issubclass(module_attr, super_class) + and module_attr != super_class + and ABC not in module_attr.__bases__ + and not inspect.isabstract(module_attr) + ): + classes.append(module_attr) + return classes + + +def get_classes_from_module_name( + name: str, super_class: Type[_T] +) -> List[Tuple[Type[_T], ModuleType]]: + """从指定名称的模块中查找指定类型的类。 + + Args: + name: 模块名称,格式和 Python `import` 语句相同。 + super_class: 要查找的类的超类。 + + Returns: + 返回由符合条件的类和模块组成的元组的列表。 + + Raises: + ImportError: 当导入模块过程中出现错误。 + """ + try: + importlib.invalidate_caches() + module = importlib.import_module(name) + importlib.reload(module) + return list( + map(lambda x: (x, module), get_classes_from_module(module, super_class)) + ) + except BaseException as e: + # 不捕获 KeyboardInterrupt + # 捕获 KeyboardInterrupt 会阻止用户关闭 Python 当正在导入的模块陷入死循环时 + if isinstance(e, KeyboardInterrupt): + raise e + raise ImportError(e, traceback.format_exc()) from e + + +def get_classes_from_dir( + dirs: Iterable[str], super_class: Type[_T] +) -> List[Tuple[Type[_T], ModuleType]]: + """从指定路径列表中的所有模块中查找指定类型的类,以 `_` 开头的插件不会被导入。路径可以是相对路径或绝对路径。 + + Args: + dirs: 储存模块的路径的列表。 + super_class: 要查找的类的超类。 + + Returns: + 返回由符合条件的类和模块组成的元组的列表。 + """ + classes: List[Tuple[Type[_T], ModuleType]] = [] + for module_info in pkgutil.iter_modules(dirs): + if not module_info.name.startswith("_"): + try: + classes.extend( + get_classes_from_module_name(module_info.name, super_class) + ) + except ImportError: + continue + return classes + + +class DataclassEncoder(json.JSONEncoder): + """用于解析 MessageSegment 的 JSONEncoder 类。""" + + def default(self, o): + return o.as_dict() if dataclasses.is_dataclass(o) else super().default(o) + + +def samefile(path1: str, path2: str) -> bool: + """一个 `os.path.samefile` 的简单包装。 + + Args: + path1: 路径1。 + path2: 路径2。 + + Returns: + 如果两个路径是否指向相同的文件或目录。 + """ + try: + return path1 == path2 or os.path.samefile(path1, path2) + except OSError: + return False + + +def sync_func_wrapper( + func: Callable[_P, _R], to_thread: bool = False +) -> Callable[_P, Coroutine[None, None, _R]]: + """包装一个同步函数为异步函数 + + Args: + func: 待包装的同步函数。 + to_thread: 在独立的线程中运行同步函数。 + + Returns: + 异步函数。 + """ + if to_thread: + + async def _wrapper(*args: _P.args, **kwargs: _P.kwargs): + loop = asyncio.get_running_loop() + func_call = partial(func, *args, **kwargs) + return await loop.run_in_executor(None, func_call) + + else: + + async def _wrapper(*args: _P.args, **kwargs: _P.kwargs): + return func(*args, **kwargs) + + return _wrapper \ No newline at end of file -- cgit v1.2.3-70-g09d2