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