Skip to content

Commit

Permalink
✨ event for plugin loaded/unloaded
Browse files Browse the repository at this point in the history
  • Loading branch information
RF-Tar-Railt committed Jan 1, 2025
1 parent 09f7dc4 commit d30308d
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 28 deletions.
3 changes: 3 additions & 0 deletions arclet/entari/core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import asyncio
from contextlib import suppress
import os

Expand Down Expand Up @@ -228,3 +229,5 @@ def validate(self, param: Param):


global_providers.extend([EntariProvider(), LaunartProvider(), ServiceProviderFactory()]) # type: ignore

es.loop = it(asyncio.AbstractEventLoop)
2 changes: 1 addition & 1 deletion arclet/entari/event/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
2 changes: 1 addition & 1 deletion arclet/entari/event/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions arclet/entari/event/plugin.py
Original file line number Diff line number Diff line change
@@ -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
50 changes: 33 additions & 17 deletions arclet/entari/plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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 <blue>{path!r}</blue>")
es.publish(PluginLoadedFailed(path))
return
log.plugin.opt(colors=True).success(f"loaded plugin <blue>{mod.__name__!r}</blue>")
if mod.__name__ in plugin_service._unloaded:
Expand All @@ -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 <blue>{path!r}</blue>: {e.args[0]}")
es.publish(PluginLoadedFailed(path, e))
except Exception as e:
log.plugin.opt(colors=True).exception(
f"failed to load plugin <blue>{path!r}</blue> caused by {e!r}", exc_info=e
)
es.publish(PluginLoadedFailed(path, e))


def load_plugins(dir_: str | PathLike | Path):
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion arclet/entari/plugin/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <y>{plug.id}</y>")
try:
await es.publish(PluginUnloaded(plug.id))
plug.dispose()
except Exception as e:
log.plugin.opt(colors=True).error(f"failed to dispose plugin <y>{plug.id}</y> caused by {e!r}")
Expand Down
19 changes: 15 additions & 4 deletions example_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down
6 changes: 3 additions & 3 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ authors = [
{name = "RF-Tar-Railt",email = "[email protected]"},
]
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",
Expand Down

0 comments on commit d30308d

Please sign in to comment.