diff --git a/arclet/entari/core.py b/arclet/entari/core.py index 1345bb5..955aa78 100644 --- a/arclet/entari/core.py +++ b/arclet/entari/core.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from contextlib import suppress import os @@ -228,3 +229,5 @@ def validate(self, param: Param): global_providers.extend([EntariProvider(), LaunartProvider(), ServiceProviderFactory()]) # type: ignore + +es.loop = it(asyncio.AbstractEventLoop) diff --git a/arclet/entari/event/command.py b/arclet/entari/event/command.py index 1203a20..61885c2 100644 --- a/arclet/entari/event/command.py +++ b/arclet/entari/event/command.py @@ -7,7 +7,7 @@ @dataclass -@make_event(name="entari.event/command_execute") +@make_event(name="entari.event/command/execute") class CommandExecute: command: Union[str, MessageChain] diff --git a/arclet/entari/event/config.py b/arclet/entari/event/config.py index 19c6588..b929937 100644 --- a/arclet/entari/event/config.py +++ b/arclet/entari/event/config.py @@ -7,7 +7,7 @@ @dataclass -@make_event(name="entari.event/config_reload") +@make_event(name="entari.event/config/reload") class ConfigReload: scope: str key: str diff --git a/arclet/entari/event/plugin.py b/arclet/entari/event/plugin.py new file mode 100644 index 0000000..26ac094 --- /dev/null +++ b/arclet/entari/event/plugin.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from typing import Optional +from arclet.letoderea import make_event + + +@dataclass +@make_event(name="entari.event/plugin/loaded_success") +class PluginLoadedSuccess: + name: str + + +@dataclass +@make_event(name="entari.event/plugin/loaded_failed") +class PluginLoadedFailed: + name: str + error: Optional[Exception] = None + """若没有异常信息,说明该插件加载失败的原因是插件不存在。""" + + +@dataclass +@make_event(name="entari.event/plugin/unloaded") +class PluginUnloaded: + name: str diff --git a/arclet/entari/plugin/__init__.py b/arclet/entari/plugin/__init__.py index 2935b7d..8da10bb 100644 --- a/arclet/entari/plugin/__init__.py +++ b/arclet/entari/plugin/__init__.py @@ -3,12 +3,13 @@ from os import PathLike from pathlib import Path from typing import Any, Callable, overload - +import inspect from arclet.letoderea import es from tarina import init_spec from ..config import C, EntariConfig, config_model_validate from ..logger import log +from ..event.plugin import PluginLoadedSuccess, PluginLoadedFailed, PluginUnloaded from .model import PluginMetadata as PluginMetadata from .model import RegisterNotInPluginError from .model import RootlessPlugin as RootlessPlugin @@ -21,10 +22,28 @@ from .service import plugin_service -def dispatch(event: type[TE], name: str | None = None): - if not (plugin := _current_plugin.get(None)): +def get_plugin(depth: int | None = None) -> Plugin: + if plugin := _current_plugin.get(None): + return plugin + if depth is None: raise LookupError("no plugin context found") - return plugin.dispatch(event, name=name) + current_frame = inspect.currentframe() + if current_frame is None: + raise ValueError("Depth out of range") + frame = current_frame + d = depth + 1 + while d > 0: + frame = frame.f_back + if frame is None: + raise ValueError("Depth out of range") + d -= 1 + if (mod := inspect.getmodule(frame)) and "__plugin__" in mod.__dict__: + return mod.__plugin__ + raise LookupError("no plugin context found") + + +def dispatch(event: type[TE], name: str | None = None): + return get_plugin().dispatch(event, name=name) def load_plugin( @@ -63,6 +82,7 @@ def load_plugin( mod = import_plugin(path1, config=conf) if not mod: log.plugin.opt(colors=True).error(f"cannot found plugin {path!r}") + es.publish(PluginLoadedFailed(path)) return log.plugin.opt(colors=True).success(f"loaded plugin {mod.__name__!r}") if mod.__name__ in plugin_service._unloaded: @@ -82,13 +102,16 @@ def load_plugin( else: recursive_guard.add(referent) plugin_service._unloaded.discard(mod.__name__) + es.publish(PluginLoadedSuccess(mod.__name__)) return mod.__plugin__ except (ImportError, RegisterNotInPluginError, StaticPluginDispatchError) as e: log.plugin.opt(colors=True).error(f"failed to load plugin {path!r}: {e.args[0]}") + es.publish(PluginLoadedFailed(path, e)) except Exception as e: log.plugin.opt(colors=True).exception( f"failed to load plugin {path!r} caused by {e!r}", exc_info=e ) + es.publish(PluginLoadedFailed(path, e)) def load_plugins(dir_: str | PathLike | Path): @@ -102,9 +125,7 @@ def load_plugins(dir_: str | PathLike | Path): @init_spec(PluginMetadata) def metadata(data: PluginMetadata): - if not (plugin := _current_plugin.get(None)): - raise LookupError("no plugin context found") - plugin._metadata = data # type: ignore + get_plugin()._metadata = data # type: ignore @overload @@ -117,8 +138,7 @@ def plugin_config(model_type: type[C]) -> C: ... def plugin_config(model_type: type[C] | None = None): """获取当前插件的配置""" - if not (plugin := _current_plugin.get(None)): - raise LookupError("no plugin context found") + plugin = get_plugin() if model_type: return config_model_validate(model_type, plugin.config) return plugin.config @@ -129,23 +149,18 @@ def plugin_config(model_type: type[C] | None = None): def declare_static(): """声明当前插件为静态插件""" - if not (plugin := _current_plugin.get(None)): - raise LookupError("no plugin context found") + plugin = get_plugin() plugin.is_static = True if plugin._scope.subscribers: raise StaticPluginDispatchError("static plugin cannot dispatch events") def add_service(serv: TS | type[TS]) -> TS: - if not (plugin := _current_plugin.get(None)): - raise LookupError("no plugin context found") - return plugin.service(serv) + return get_plugin().service(serv) def collect_disposes(*disposes: Callable[[], None]): - if not (plugin := _current_plugin.get(None)): - raise LookupError("no plugin context found") - plugin.collect(*disposes) + return get_plugin().collect(*disposes) def find_plugin(name: str) -> Plugin | None: @@ -175,6 +190,7 @@ def unload_plugin(plugin: str): plugin = plugin_service._subplugined[plugin] if not (_plugin := find_plugin(plugin)): return False + es.publish(PluginUnloaded(_plugin.id)) _plugin.dispose() return True diff --git a/arclet/entari/plugin/service.py b/arclet/entari/plugin/service.py index 8e3b5d4..288e573 100644 --- a/arclet/entari/plugin/service.py +++ b/arclet/entari/plugin/service.py @@ -5,6 +5,7 @@ from launart.status import Phase from ..event.lifespan import Cleanup, Ready, Startup +from ..event.plugin import PluginUnloaded from ..filter import Filter from ..logger import log @@ -55,11 +56,12 @@ async def launch(self, manager: Launart): async with self.stage("cleanup"): await es.publish(Cleanup()) ids = [k for k in self.plugins.keys() if k not in self._subplugined] - for plug_id in ids: + for plug_id in reversed(ids): plug = self.plugins[plug_id] if not plug.id.startswith("."): log.plugin.opt(colors=True).debug(f"disposing plugin {plug.id}") try: + await es.publish(PluginUnloaded(plug.id)) plug.dispose() except Exception as e: log.plugin.opt(colors=True).error(f"failed to dispose plugin {plug.id} caused by {e!r}") diff --git a/example_plugin.py b/example_plugin.py index 30f7645..f8e82a7 100644 --- a/example_plugin.py +++ b/example_plugin.py @@ -20,12 +20,12 @@ @plug.use("::startup") async def prepare(): - print("example: Preparing") + print(">> example: Preparing") @plug.use("::cleanup") async def cleanup(): - print("example: Cleanup") + print(">> example: Cleanup") @plug.dispatch(MessageCreatedEvent) @@ -84,11 +84,22 @@ async def send_hook(message: MessageChain): return message + "喵" -@plug.use("::config_reload") +@plug.use("::config/reload") async def config_reload(): - print("Config Reloaded") + print(">> Config Reloaded") return True + +@plug.use("::plugin/loaded_success") +async def loaded_success(event): + print(f">> Plugin {event.name} Loaded Successfully") + + +@plug.use("::plugin/unloaded") +async def unloaded(event): + print(f">> Plugin {event.name} Unloaded") + + # @scheduler.cron("* * * * *") # async def broadcast(app: Entari): # for account in app.accounts.values(): diff --git a/pdm.lock b/pdm.lock index 8079c9e..a1d4a06 100644 --- a/pdm.lock +++ b/pdm.lock @@ -174,15 +174,15 @@ files = [ [[package]] name = "arclet-letoderea" -version = "0.14.5" +version = "0.14.8" requires_python = ">=3.9" summary = "A high-performance, simple-structured event system, relies on asyncio" dependencies = [ "tarina>=0.6.7", ] files = [ - {file = "arclet_letoderea-0.14.5-py3-none-any.whl", hash = "sha256:1391e548b837a65c54c6d4c5482642569ea3ca4672708c2baf3e9c7ef91044cb"}, - {file = "arclet_letoderea-0.14.5.tar.gz", hash = "sha256:4e67a8e7350e21a827055690cee555680af39847de1476c8390eef626b5bf862"}, + {file = "arclet_letoderea-0.14.8-py3-none-any.whl", hash = "sha256:62f8b2aa30549c597e3c9777250c56d0f699fb6ef4ce8adb60bbe5b25edca6b3"}, + {file = "arclet_letoderea-0.14.8.tar.gz", hash = "sha256:04b9c94c0eed2a6c47f3492aff57cf073b1ce698a0c9148d45760f02309bc0be"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index fb40396..2861f30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ {name = "RF-Tar-Railt",email = "rf_tar_railt@qq.com"}, ] dependencies = [ - "arclet-letoderea>=0.14.5", + "arclet-letoderea>=0.14.9", "arclet-alconna<2.0,>=1.8.34", "satori-python-core>=0.15.2", "satori-python-client>=0.15.2",