From f52abc831493ee88644161fa21e0320c28ab6497 Mon Sep 17 00:00:00 2001 From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> Date: Tue, 30 May 2023 15:20:31 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20Feature:=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=88=86=E5=8F=91=E6=96=B9=E6=B3=95=20(#2067?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nonebot/message.py | 444 +++++++++++++++++++++------- nonebot/utils.py | 4 +- tests/conftest.py | 6 +- tests/examples/weather.py | 29 -- tests/test_broadcast.py | 388 ++++++++++++++++++++++++ tests/test_examples/test_weather.py | 76 ----- tests/test_matcher/test_matcher.py | 8 +- tests/test_plugin/test_load.py | 4 +- 8 files changed, 728 insertions(+), 231 deletions(-) delete mode 100644 tests/examples/weather.py create mode 100644 tests/test_broadcast.py delete mode 100644 tests/test_examples/test_weather.py diff --git a/nonebot/message.py b/nonebot/message.py index 28dc2edb..9561005e 100644 --- a/nonebot/message.py +++ b/nonebot/message.py @@ -80,7 +80,10 @@ RUN_POSTPCS_PARAMS = ( def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor: - """事件预处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。""" + """事件预处理。 + + 装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。 + """ _event_preprocessors.add( Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS) ) @@ -88,7 +91,10 @@ def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor: def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor: - """事件后处理。装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。""" + """事件后处理。 + + 装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。 + """ _event_postprocessors.add( Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS) ) @@ -96,7 +102,10 @@ def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor: def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor: - """运行预处理。装饰一个函数,使它在每次事件响应器运行前执行。""" + """运行预处理。 + + 装饰一个函数,使它在每次事件响应器运行前执行。 + """ _run_preprocessors.add( Dependent[Any].parse(call=func, allow_types=RUN_PREPCS_PARAMS) ) @@ -104,13 +113,222 @@ def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor: def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor: - """运行后处理。装饰一个函数,使它在每次事件响应器运行后执行。""" + """运行后处理。 + + 装饰一个函数,使它在每次事件响应器运行后执行。 + """ _run_postprocessors.add( Dependent[Any].parse(call=func, allow_types=RUN_POSTPCS_PARAMS) ) return func +async def _apply_event_preprocessors( + bot: "Bot", + event: "Event", + state: T_State, + stack: Optional[AsyncExitStack] = None, + dependency_cache: Optional[T_DependencyCache] = None, + show_log: bool = True, +) -> bool: + """运行事件预处理。 + + 参数: + bot: Bot 对象 + event: Event 对象 + state: 会话状态 + stack: 异步上下文栈 + dependency_cache: 依赖缓存 + show_log: 是否显示日志 + + 返回: + 是否继续处理事件 + """ + if not _event_preprocessors: + return True + + if show_log: + logger.debug("Running PreProcessors...") + + try: + await asyncio.gather( + *( + run_coro_with_catch( + proc( + bot=bot, + event=event, + state=state, + stack=stack, + dependency_cache=dependency_cache, + ), + (SkippedException,), + ) + for proc in _event_preprocessors + ) + ) + except IgnoredException as e: + logger.opt(colors=True).info( + f"Event {escape_tag(event.get_event_name())} is ignored" + ) + return False + except Exception as e: + logger.opt(colors=True, exception=e).error( + "Error when running EventPreProcessors. " + "Event ignored!" + ) + return False + + return True + + +async def _apply_event_postprocessors( + bot: "Bot", + event: "Event", + state: T_State, + stack: Optional[AsyncExitStack] = None, + dependency_cache: Optional[T_DependencyCache] = None, + show_log: bool = True, +) -> None: + """运行事件后处理。 + + 参数: + bot: Bot 对象 + event: Event 对象 + state: 会话状态 + stack: 异步上下文栈 + dependency_cache: 依赖缓存 + show_log: 是否显示日志 + """ + if not _event_postprocessors: + return + + if show_log: + logger.debug("Running PostProcessors...") + + try: + await asyncio.gather( + *( + run_coro_with_catch( + proc( + bot=bot, + event=event, + state=state, + stack=stack, + dependency_cache=dependency_cache, + ), + (SkippedException,), + ) + for proc in _event_postprocessors + ) + ) + except Exception as e: + logger.opt(colors=True, exception=e).error( + "Error when running EventPostProcessors" + ) + + +async def _apply_run_preprocessors( + bot: "Bot", + event: "Event", + state: T_State, + matcher: Matcher, + stack: Optional[AsyncExitStack] = None, + dependency_cache: Optional[T_DependencyCache] = None, +) -> bool: + """运行事件响应器运行前处理。 + + 参数: + bot: Bot 对象 + event: Event 对象 + state: 会话状态 + matcher: 事件响应器 + stack: 异步上下文栈 + dependency_cache: 依赖缓存 + + 返回: + 是否继续处理事件 + """ + if not _run_preprocessors: + return True + + # ensure matcher function can be correctly called + with matcher.ensure_context(bot, event): + try: + await asyncio.gather( + *( + run_coro_with_catch( + proc( + matcher=matcher, + bot=bot, + event=event, + state=state, + stack=stack, + dependency_cache=dependency_cache, + ), + (SkippedException,), + ) + for proc in _run_preprocessors + ) + ) + except IgnoredException: + logger.opt(colors=True).info(f"{matcher} running is cancelled") + return False + except Exception as e: + logger.opt(colors=True, exception=e).error( + "Error when running RunPreProcessors. " + "Running cancelled!" + ) + return False + + return True + + +async def _apply_run_postprocessors( + bot: "Bot", + event: "Event", + matcher: Matcher, + exception: Optional[Exception] = None, + stack: Optional[AsyncExitStack] = None, + dependency_cache: Optional[T_DependencyCache] = None, +) -> None: + """运行事件响应器运行后处理。 + + Args: + bot: Bot 对象 + event: Event 对象 + matcher: 事件响应器 + exception: 事件响应器运行异常 + stack: 异步上下文栈 + dependency_cache: 依赖缓存 + """ + if not _run_postprocessors: + return + + with matcher.ensure_context(bot, event): + try: + await asyncio.gather( + *( + run_coro_with_catch( + proc( + matcher=matcher, + exception=exception, + bot=bot, + event=event, + state=matcher.state, + stack=stack, + dependency_cache=dependency_cache, + ), + (SkippedException,), + ) + for proc in _run_postprocessors + ) + ) + except Exception as e: + logger.opt(colors=True, exception=e).error( + "Error when running RunPostProcessors" + ) + + async def _check_matcher( Matcher: Type[Matcher], bot: "Bot", @@ -118,27 +336,39 @@ async def _check_matcher( state: T_State, stack: Optional[AsyncExitStack] = None, dependency_cache: Optional[T_DependencyCache] = None, -) -> None: +) -> bool: + """检查事件响应器是否符合运行条件。 + + 请注意,过时的事件响应器将被**销毁**。对于未过时的事件响应器,将会一次检查其响应类型、权限和规则。 + + 参数: + Matcher: 要检查的事件响应器 + bot: Bot 对象 + event: Event 对象 + state: 会话状态 + stack: 异步上下文栈 + dependency_cache: 依赖缓存 + + 返回: + bool: 是否符合运行条件 + """ if Matcher.expire_time and datetime.now() > Matcher.expire_time: with contextlib.suppress(Exception): Matcher.destroy() - return + return False try: if not await Matcher.check_perm( bot, event, stack, dependency_cache ) or not await Matcher.check_rule(bot, event, state, stack, dependency_cache): - return + return False except Exception as e: logger.opt(colors=True, exception=e).error( f"Rule check failed for {Matcher}." ) - return + return False - if Matcher.temp: - with contextlib.suppress(Exception): - Matcher.destroy() - await _run_matcher(Matcher, bot, event, state, stack, dependency_cache) + return True async def _run_matcher( @@ -149,36 +379,38 @@ async def _run_matcher( stack: Optional[AsyncExitStack] = None, dependency_cache: Optional[T_DependencyCache] = None, ) -> None: + """运行事件响应器。 + + 临时事件响应器将在运行前被**销毁**。 + + 参数: + Matcher: 事件响应器 + bot: Bot 对象 + event: Event 对象 + state: 会话状态 + stack: 异步上下文栈 + dependency_cache: 依赖缓存 + + 异常: + StopPropagation: 阻止事件继续传播 + """ logger.info(f"Event will be handled by {Matcher}") - matcher = Matcher() - if coros := [ - run_coro_with_catch( - proc( - matcher=matcher, - bot=bot, - event=event, - state=state, - stack=stack, - dependency_cache=dependency_cache, - ), - (SkippedException,), - ) - for proc in _run_preprocessors - ]: - # ensure matcher function can be correctly called - with matcher.ensure_context(bot, event): - try: - await asyncio.gather(*coros) - except IgnoredException: - logger.opt(colors=True).info(f"{matcher} running is cancelled") - return - except Exception as e: - logger.opt(colors=True, exception=e).error( - "Error when running RunPreProcessors. Running cancelled!" - ) + if Matcher.temp: + with contextlib.suppress(Exception): + Matcher.destroy() - return + matcher = Matcher() + + if not await _apply_run_preprocessors( + bot=bot, + event=event, + state=state, + matcher=matcher, + stack=stack, + dependency_cache=dependency_cache, + ): + return exception = None @@ -191,33 +423,55 @@ async def _run_matcher( ) exception = e - if coros := [ - run_coro_with_catch( - proc( - matcher=matcher, - exception=exception, - bot=bot, - event=event, - state=matcher.state, - stack=stack, - dependency_cache=dependency_cache, - ), - (SkippedException,), - ) - for proc in _run_postprocessors - ]: - # ensure matcher function can be correctly called - with matcher.ensure_context(bot, event): - try: - await asyncio.gather(*coros) - except Exception as e: - logger.opt(colors=True, exception=e).error( - "Error when running RunPostProcessors" - ) + await _apply_run_postprocessors( + bot=bot, + event=event, + matcher=matcher, + exception=exception, + stack=stack, + dependency_cache=dependency_cache, + ) if matcher.block: raise StopPropagation - return + + +async def check_and_run_matcher( + Matcher: Type[Matcher], + bot: "Bot", + event: "Event", + state: T_State, + stack: Optional[AsyncExitStack] = None, + dependency_cache: Optional[T_DependencyCache] = None, +) -> None: + """检查并运行事件响应器。 + + 参数: + Matcher: 事件响应器 + bot: Bot 对象 + event: Event 对象 + state: 会话状态 + stack: 异步上下文栈 + dependency_cache: 依赖缓存 + """ + if not await _check_matcher( + Matcher=Matcher, + bot=bot, + event=event, + state=state, + stack=stack, + dependency_cache=dependency_cache, + ): + return + + await _run_matcher( + Matcher=Matcher, + bot=bot, + event=event, + state=state, + stack=stack, + dependency_cache=dependency_cache, + ) async def handle_event(bot: "Bot", event: "Event") -> None: @@ -245,35 +499,16 @@ async def handle_event(bot: "Bot", event: "Event") -> None: state: Dict[Any, Any] = {} dependency_cache: T_DependencyCache = {} + # create event scope context async with AsyncExitStack() as stack: - if coros := [ - run_coro_with_catch( - proc( - bot=bot, - event=event, - state=state, - stack=stack, - dependency_cache=dependency_cache, - ), - (SkippedException,), - ) - for proc in _event_preprocessors - ]: - try: - if show_log: - logger.debug("Running PreProcessors...") - await asyncio.gather(*coros) - except IgnoredException as e: - logger.opt(colors=True).info( - f"Event {escape_tag(event.get_event_name())} is ignored" - ) - return - except Exception as e: - logger.opt(colors=True, exception=e).error( - "Error when running EventPreProcessors. " - "Event ignored!" - ) - return + if not await _apply_event_preprocessors( + bot=bot, + event=event, + state=state, + stack=stack, + dependency_cache=dependency_cache, + ): + return # Trie Match try: @@ -284,6 +519,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None: ) break_flag = False + # iterate through all priority until stop propagation for priority in sorted(matchers.keys()): if break_flag: break @@ -292,14 +528,12 @@ async def handle_event(bot: "Bot", event: "Event") -> None: logger.debug(f"Checking for matchers in priority {priority}...") pending_tasks = [ - _check_matcher( + check_and_run_matcher( matcher, bot, event, state.copy(), stack, dependency_cache ) for matcher in matchers[priority] ] - results = await asyncio.gather(*pending_tasks, return_exceptions=True) - for result in results: if not isinstance(result, Exception): continue @@ -314,24 +548,4 @@ async def handle_event(bot: "Bot", event: "Event") -> None: if show_log: logger.debug("Checking for matchers completed") - if coros := [ - run_coro_with_catch( - proc( - bot=bot, - event=event, - state=state, - stack=stack, - dependency_cache=dependency_cache, - ), - (SkippedException,), - ) - for proc in _event_postprocessors - ]: - try: - if show_log: - logger.debug("Running PostProcessors...") - await asyncio.gather(*coros) - except Exception as e: - logger.opt(colors=True, exception=e).error( - "Error when running EventPostProcessors" - ) + await _apply_event_postprocessors(bot, event, state, stack, dependency_cache) diff --git a/nonebot/utils.py b/nonebot/utils.py index 9391a507..54e7d052 100644 --- a/nonebot/utils.py +++ b/nonebot/utils.py @@ -113,8 +113,7 @@ def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]: loop = asyncio.get_running_loop() pfunc = partial(call, *args, **kwargs) context = copy_context() - context_run = context.run - result = await loop.run_in_executor(None, context_run, pfunc) + result = await loop.run_in_executor(None, partial(context.run, pfunc)) return result return _wrapper @@ -139,6 +138,7 @@ async def run_sync_ctx_manager( async def run_coro_with_catch( coro: Coroutine[Any, Any, T], exc: Tuple[Type[Exception], ...], + return_on_err: None = None, ) -> Union[T, None]: ... diff --git a/tests/conftest.py b/tests/conftest.py index b1a31cac..a25efd85 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,6 @@ def load_plugin(nonebug_init: None) -> Set["Plugin"]: @pytest.fixture(scope="session", autouse=True) -def load_example(nonebug_init: None) -> Set["Plugin"]: - # preload example plugins - return nonebot.load_plugins(str(Path(__file__).parent / "examples")) +def load_builtin_plugin(nonebug_init: None) -> Set["Plugin"]: + # preload builtin plugins + return nonebot.load_builtin_plugins("echo", "single_session") diff --git a/tests/examples/weather.py b/tests/examples/weather.py deleted file mode 100644 index ec0459f0..00000000 --- a/tests/examples/weather.py +++ /dev/null @@ -1,29 +0,0 @@ -from nonebot import on_command -from nonebot.rule import to_me -from nonebot.matcher import Matcher -from nonebot.adapters import Message -from nonebot.params import Arg, CommandArg, ArgPlainText - -weather = on_command("weather", rule=to_me(), aliases={"天气", "天气预报"}, priority=5) - - -@weather.handle() -async def handle_first_receive(matcher: Matcher, args: Message = CommandArg()): - plain_text = args.extract_plain_text() # 首次发送命令时跟随的参数,例:/天气 上海,则args为上海 - if plain_text: - matcher.set_arg("city", args) # 如果用户发送了参数则直接赋值 - - -@weather.got("city", prompt="你想查询哪个城市的天气呢?") -async def handle_city(city: Message = Arg(), city_name: str = ArgPlainText("city")): - if city_name not in ["北京", "上海"]: # 如果参数不符合要求,则提示用户重新输入 - # 可以使用平台的 Message 类直接构造模板消息 - await weather.reject(city.template("你想查询的城市 {city} 暂不支持,请重新输入!")) - - city_weather = await get_weather(city_name) - await weather.finish(city_weather) - - -# 在这里编写获取天气信息的函数 -async def get_weather(city: str) -> str: - return f"{city}的天气是..." diff --git a/tests/test_broadcast.py b/tests/test_broadcast.py new file mode 100644 index 00000000..4814761e --- /dev/null +++ b/tests/test_broadcast.py @@ -0,0 +1,388 @@ +import sys +from typing import Optional + +import pytest +from nonebug import App + +from nonebot import on_message +import nonebot.message as message +from utils import make_fake_event +from nonebot.params import Depends +from nonebot.typing import T_State +from nonebot.matcher import Matcher +from nonebot.adapters import Bot, Event +from nonebot.exception import IgnoredException +from nonebot.log import logger, default_filter, default_format +from nonebot.message import ( + run_preprocessor, + run_postprocessor, + event_preprocessor, + event_postprocessor, +) + + +async def _dependency() -> int: + return 1 + + +@pytest.mark.asyncio +async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch): + with monkeypatch.context() as m: + m.setattr(message, "_event_preprocessors", set()) + + runned = False + + @event_preprocessor + async def test_preprocessor( + bot: Bot, + event: Event, + state: T_State, + sub: int = Depends(_dependency), + default: int = 1, + ): + nonlocal runned + runned = True + + assert test_preprocessor in { + dependent.call for dependent in message._event_preprocessors + } + + with app.provider.context({}): + matcher = on_message() + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + + assert runned, "event_preprocessor should runned" + + +@pytest.mark.asyncio +async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch): + with monkeypatch.context() as m: + m.setattr(message, "_event_preprocessors", set()) + + @event_preprocessor + async def test_preprocessor(): + raise IgnoredException("pass") + + assert test_preprocessor in { + dependent.call for dependent in message._event_preprocessors + } + + runned = False + + async def handler(): + nonlocal runned + runned = True + + with app.provider.context({}): + matcher = on_message(handlers=[handler]) + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + + assert not runned, "matcher should not runned" + + +@pytest.mark.asyncio +async def test_event_preprocessor_exception( + app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +): + with monkeypatch.context() as m: + m.setattr(message, "_event_preprocessors", set()) + + @event_preprocessor + async def test_preprocessor(): + raise RuntimeError("test") + + assert test_preprocessor in { + dependent.call for dependent in message._event_preprocessors + } + + runned = False + + async def handler(): + nonlocal runned + runned = True + + handler_id = logger.add( + sys.stdout, + level=0, + diagnose=False, + filter=default_filter, + format=default_format, + ) + + try: + with app.provider.context({}): + matcher = on_message(handlers=[handler]) + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + finally: + logger.remove(handler_id) + + assert not runned, "matcher should not runned" + assert "RuntimeError: test" in capsys.readouterr().out + + +@pytest.mark.asyncio +async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch): + with monkeypatch.context() as m: + m.setattr(message, "_event_postprocessors", set()) + + runned = False + + @event_postprocessor + async def test_postprocessor( + bot: Bot, + event: Event, + state: T_State, + sub: int = Depends(_dependency), + default: int = 1, + ): + nonlocal runned + runned = True + + assert test_postprocessor in { + dependent.call for dependent in message._event_postprocessors + } + + with app.provider.context({}): + matcher = on_message() + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + + assert runned, "event_postprocessor should runned" + + +@pytest.mark.asyncio +async def test_event_postprocessor_exception( + app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +): + with monkeypatch.context() as m: + m.setattr(message, "_event_postprocessors", set()) + + @event_postprocessor + async def test_postprocessor(): + raise RuntimeError("test") + + assert test_postprocessor in { + dependent.call for dependent in message._event_postprocessors + } + + handler_id = logger.add( + sys.stdout, + level=0, + diagnose=False, + filter=default_filter, + format=default_format, + ) + + try: + with app.provider.context({}): + matcher = on_message() + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + finally: + logger.remove(handler_id) + + assert "RuntimeError: test" in capsys.readouterr().out + + +@pytest.mark.asyncio +async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch): + with monkeypatch.context() as m: + m.setattr(message, "_run_preprocessors", set()) + + runned = False + + @run_preprocessor + async def test_preprocessor( + bot: Bot, + event: Event, + state: T_State, + matcher: Matcher, + sub: int = Depends(_dependency), + default: int = 1, + ): + nonlocal runned + runned = True + + await matcher.send("test") + + assert test_preprocessor in { + dependent.call for dependent in message._run_preprocessors + } + + with app.provider.context({}): + matcher = on_message() + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + ctx.should_call_send(event, "test", True, bot) + + assert runned, "run_preprocessor should runned" + + +@pytest.mark.asyncio +async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch): + with monkeypatch.context() as m: + m.setattr(message, "_run_preprocessors", set()) + + @run_preprocessor + async def test_preprocessor(): + raise IgnoredException("pass") + + assert test_preprocessor in { + dependent.call for dependent in message._run_preprocessors + } + + runned = False + + async def handler(): + nonlocal runned + runned = True + + with app.provider.context({}): + matcher = on_message(handlers=[handler]) + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + + assert not runned, "matcher should not runned" + + +@pytest.mark.asyncio +async def test_run_preprocessor_exception( + app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +): + with monkeypatch.context() as m: + m.setattr(message, "_run_preprocessors", set()) + + @run_preprocessor + async def test_preprocessor(): + raise RuntimeError("test") + + assert test_preprocessor in { + dependent.call for dependent in message._run_preprocessors + } + + runned = False + + async def handler(): + nonlocal runned + runned = True + + handler_id = logger.add( + sys.stdout, + level=0, + diagnose=False, + filter=default_filter, + format=default_format, + ) + + try: + with app.provider.context({}): + matcher = on_message(handlers=[handler]) + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + finally: + logger.remove(handler_id) + + assert not runned, "matcher should not runned" + assert "RuntimeError: test" in capsys.readouterr().out + + +@pytest.mark.asyncio +async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch): + with monkeypatch.context() as m: + m.setattr(message, "_run_postprocessors", set()) + + runned = False + + @run_postprocessor + async def test_postprocessor( + bot: Bot, + event: Event, + state: T_State, + matcher: Matcher, + exception: Optional[Exception], + sub: int = Depends(_dependency), + default: int = 1, + ): + nonlocal runned + runned = True + + await matcher.send("test") + + assert test_postprocessor in { + dependent.call for dependent in message._run_postprocessors + } + + with app.provider.context({}): + matcher = on_message() + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + ctx.should_call_send(event, "test", True, bot) + + assert runned, "run_postprocessor should runned" + + +@pytest.mark.asyncio +async def test_run_postprocessor_exception( + app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +): + with monkeypatch.context() as m: + m.setattr(message, "_run_postprocessors", set()) + + @run_postprocessor + async def test_postprocessor(): + raise RuntimeError("test") + + assert test_postprocessor in { + dependent.call for dependent in message._run_postprocessors + } + + handler_id = logger.add( + sys.stdout, + level=0, + diagnose=False, + filter=default_filter, + format=default_format, + ) + + try: + with app.provider.context({}): + matcher = on_message() + + async with app.test_matcher(matcher) as ctx: + bot = ctx.create_bot() + event = make_fake_event()() + ctx.receive_event(bot, event) + finally: + logger.remove(handler_id) + + assert "RuntimeError: test" in capsys.readouterr().out diff --git a/tests/test_examples/test_weather.py b/tests/test_examples/test_weather.py deleted file mode 100644 index 437ad1b7..00000000 --- a/tests/test_examples/test_weather.py +++ /dev/null @@ -1,76 +0,0 @@ -import pytest -from nonebug import App - -from utils import make_fake_event, make_fake_message - - -@pytest.mark.asyncio -async def test_weather(app: App): - from examples.weather import weather - - # 将此处的 make_fake_message() 替换为你要发送的平台消息 Message 类型 - # from nonebot.adapters.console import Message - Message = make_fake_message() - - async with app.test_matcher(weather) as ctx: - bot = ctx.create_bot() - - msg = Message("/天气 上海") - # 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型 - # from nonebot.adapters.console import MessageEvent - # event = MessageEvent(message=msg, to_me=True, ...) - event = make_fake_event(_message=msg, _to_me=True)() - - ctx.receive_event(bot, event) - ctx.should_call_send(event, "上海的天气是...", True) - ctx.should_finished() - - async with app.test_matcher(weather) as ctx: - bot = ctx.create_bot() - - msg = Message("/天气 南京") - # 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型 - event = make_fake_event(_message=msg, _to_me=True)() - - ctx.receive_event(bot, event) - ctx.should_call_send( - event, - Message.template("你想查询的城市 {} 暂不支持,请重新输入!").format("南京"), - True, - ) - ctx.should_rejected() - - msg = Message("北京") - event = make_fake_event(_message=msg)() - - ctx.receive_event(bot, event) - ctx.should_call_send(event, "北京的天气是...", True) - ctx.should_finished() - - async with app.test_matcher(weather) as ctx: - bot = ctx.create_bot() - - msg = Message("/天气") - # 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型 - event = make_fake_event(_message=msg, _to_me=True)() - - ctx.receive_event(bot, event) - ctx.should_call_send(event, "你想查询哪个城市的天气呢?", True) - - msg = Message("杭州") - event = make_fake_event(_message=msg)() - - ctx.receive_event(bot, event) - ctx.should_call_send( - event, - Message.template("你想查询的城市 {} 暂不支持,请重新输入!").format("杭州"), - True, - ) - ctx.should_rejected() - - msg = Message("北京") - event = make_fake_event(_message=msg)() - - ctx.receive_event(bot, event) - ctx.should_call_send(event, "北京的天气是...", True) - ctx.should_finished() diff --git a/tests/test_matcher/test_matcher.py b/tests/test_matcher/test_matcher.py index ed6c0957..9433e74b 100644 --- a/tests/test_matcher/test_matcher.py +++ b/tests/test_matcher/test_matcher.py @@ -2,8 +2,8 @@ import pytest from nonebug import App from nonebot.permission import User -from nonebot.message import _check_matcher from nonebot.matcher import Matcher, matchers +from nonebot.message import check_and_run_matcher from utils import make_fake_event, make_fake_message @@ -200,19 +200,19 @@ async def test_expire(app: App): async with app.test_api() as ctx: bot = ctx.create_bot() assert test_temp_matcher in matchers[test_temp_matcher.priority] - await _check_matcher(test_temp_matcher, bot, event, {}) + await check_and_run_matcher(test_temp_matcher, bot, event, {}) assert test_temp_matcher not in matchers[test_temp_matcher.priority] event = make_fake_event()() async with app.test_api() as ctx: bot = ctx.create_bot() assert test_datetime_matcher in matchers[test_datetime_matcher.priority] - await _check_matcher(test_datetime_matcher, bot, event, {}) + await check_and_run_matcher(test_datetime_matcher, bot, event, {}) assert test_datetime_matcher not in matchers[test_datetime_matcher.priority] event = make_fake_event()() async with app.test_api() as ctx: bot = ctx.create_bot() assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority] - await _check_matcher(test_timedelta_matcher, bot, event, {}) + await check_and_run_matcher(test_timedelta_matcher, bot, event, {}) assert test_timedelta_matcher not in matchers[test_timedelta_matcher.priority] diff --git a/tests/test_plugin/test_load.py b/tests/test_plugin/test_load.py index a0eb15ab..177ca486 100644 --- a/tests/test_plugin/test_load.py +++ b/tests/test_plugin/test_load.py @@ -22,11 +22,11 @@ async def test_load_plugin(): @pytest.mark.asyncio -async def test_load_plugins(load_plugin: Set[Plugin], load_example: Set[Plugin]): +async def test_load_plugins(load_plugin: Set[Plugin], load_builtin_plugin: Set[Plugin]): loaded_plugins = { plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin } - assert loaded_plugins >= load_plugin | load_example + assert loaded_plugins >= load_plugin | load_builtin_plugin # check simple plugin assert "plugins.export" in sys.modules