From 9f124043385c62e39174bc8ba17aa0b60ae4d8ef Mon Sep 17 00:00:00 2001 From: Akirami <839592615@qq.com> Date: Tue, 15 Feb 2022 08:20:29 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20full=20match=20Matcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nonebot/__init__.py | 2 + nonebot/plugin/__init__.py | 2 + nonebot/plugin/on.py | 47 +++++++++++++++++++ nonebot/plugin/on.pyi | 25 ++++++++++ nonebot/rule.py | 41 ++++++++++++++++ tests/test_rule.py | 37 +++++++++++++++ .../docs/tutorial/plugin/create-matcher.md | 15 +++--- 7 files changed, 162 insertions(+), 7 deletions(-) diff --git a/nonebot/__init__.py b/nonebot/__init__.py index 89fb4850..b08d00c2 100644 --- a/nonebot/__init__.py +++ b/nonebot/__init__.py @@ -11,6 +11,7 @@ - `on_request` => {ref}``on_request` ` - `on_startswith` => {ref}``on_startswith` ` - `on_endswith` => {ref}``on_endswith` ` +- `on_fullmatch` => {ref}``on_fullmatch` ` - `on_keyword` => {ref}``on_keyword` ` - `on_command` => {ref}``on_command` ` - `on_shell_command` => {ref}``on_shell_command` ` @@ -274,6 +275,7 @@ from nonebot.plugin import CommandGroup as CommandGroup from nonebot.plugin import MatcherGroup as MatcherGroup from nonebot.plugin import load_plugins as load_plugins from nonebot.plugin import on_metaevent as on_metaevent +from nonebot.plugin import on_fullmatch as on_fullmatch from nonebot.plugin import on_startswith as on_startswith from nonebot.plugin import load_from_json as load_from_json from nonebot.plugin import load_from_toml as load_from_toml diff --git a/nonebot/plugin/__init__.py b/nonebot/plugin/__init__.py index 7898518f..c63530db 100644 --- a/nonebot/plugin/__init__.py +++ b/nonebot/plugin/__init__.py @@ -11,6 +11,7 @@ - `on_request` => {ref}``on_request` ` - `on_startswith` => {ref}``on_startswith` ` - `on_endswith` => {ref}``on_endswith` ` +- `on_fullmatch` => {ref}``on_fullmatch` ` - `on_keyword` => {ref}``on_keyword` ` - `on_command` => {ref}``on_command` ` - `on_shell_command` => {ref}``on_shell_command` ` @@ -61,6 +62,7 @@ from .on import MatcherGroup as MatcherGroup from .on import on_metaevent as on_metaevent from .plugin import get_plugin as get_plugin from .load import load_plugins as load_plugins +from .on import on_fullmatch as on_fullmatch from .on import on_startswith as on_startswith from .load import load_from_json as load_from_json from .load import load_from_toml as load_from_toml diff --git a/nonebot/plugin/on.py b/nonebot/plugin/on.py index 266a0fbc..429add59 100644 --- a/nonebot/plugin/on.py +++ b/nonebot/plugin/on.py @@ -21,6 +21,7 @@ from nonebot.rule import ( command, keyword, endswith, + fullmatch, startswith, shell_command, ) @@ -283,6 +284,30 @@ def on_endswith( return on_message(endswith(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1) +def on_fullmatch( + msg: Union[str, Tuple[str, ...]], + rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = None, + ignorecase: bool = False, + _depth: int = 0, + **kwargs, +) -> Matcher: + """ + 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 + + 参数: + msg: 指定消息全匹配内容 + rule: 事件响应规则 + ignorecase: 是否忽略大小写 + permission: 事件响应权限 + handlers: 事件处理函数列表 + temp: 是否为临时事件响应器(仅执行一次) + priority: 事件响应器优先级 + block: 是否阻止事件向更低优先级传递 + state: 默认 state + """ + return on_message(fullmatch(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1) + + def on_keyword( keywords: Set[str], rule: Optional[Union[Rule, T_RuleChecker]] = None, @@ -611,6 +636,28 @@ class MatcherGroup: self.matchers.append(matcher) return matcher + def on_fullmatch(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]: + """ + 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 + + 参数: + msg: 指定消息全匹配内容 + rule: 事件响应规则 + ignorecase: 是否忽略大小写 + permission: 事件响应权限 + handlers: 事件处理函数列表 + temp: 是否为临时事件响应器(仅执行一次) + priority: 事件响应器优先级 + block: 是否阻止事件向更低优先级传递 + state: 默认 state + """ + final_kwargs = self.base_kwargs.copy() + final_kwargs.update(kwargs) + final_kwargs.pop("type", None) + matcher = on_fullmatch(msg, **final_kwargs, _depth=1) + self.matchers.append(matcher) + return matcher + def on_keyword(self, keywords: Set[str], **kwargs) -> Type[Matcher]: """ 注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 diff --git a/nonebot/plugin/on.pyi b/nonebot/plugin/on.pyi index 68f6873a..6011eb43 100644 --- a/nonebot/plugin/on.pyi +++ b/nonebot/plugin/on.pyi @@ -79,6 +79,18 @@ def on_endswith( block: bool = ..., state: Optional[T_State] = ..., ) -> Type[Matcher]: ... +def on_fullmatch( + msg: Union[str, Tuple[str, ...]], + rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ..., + ignorecase: bool = ..., + *, + permission: Optional[Union[Permission, T_PermissionChecker]] = ..., + handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., + temp: bool = ..., + priority: int = ..., + block: bool = ..., + state: Optional[T_State] = ..., +) -> Type[Matcher]: ... def on_keyword( keywords: Set[str], rule: Optional[Union[Rule, T_RuleChecker]] = ..., @@ -261,6 +273,19 @@ class MatcherGroup: block: bool = ..., state: Optional[T_State] = ..., ) -> Type[Matcher]: ... + def on_fullmatch( + self, + msg: Union[str, Tuple[str, ...]], + *, + ignorecase: bool = ..., + rule: Optional[Union[Rule, T_RuleChecker]] = ..., + permission: Optional[Union[Permission, T_PermissionChecker]] = ..., + handlers: Optional[List[Union[T_Handler, Dependent]]] = ..., + temp: bool = ..., + priority: int = ..., + block: bool = ..., + state: Optional[T_State] = ..., + ) -> Type[Matcher]: ... def on_keyword( self, keywords: Set[str], diff --git a/nonebot/rule.py b/nonebot/rule.py index f49d38fc..60e6035e 100644 --- a/nonebot/rule.py +++ b/nonebot/rule.py @@ -171,6 +171,47 @@ def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule return Rule(EndswithRule(msg, ignorecase)) +class FullmatchRule: + """检查消息纯文本是否与指定字符串全匹配。 + + 参数: + msg: 指定消息全匹配字符串元组 + ignorecase: 是否忽略大小写 + """ + + __slots__ = ("msg", "ignorecase") + + def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False): + self.msg = msg + self.ignorecase = ignorecase + + async def __call__( + self, type: str = EventType(), text: str = EventPlainText() + ) -> bool: + if type != "message": + return False + return bool( + text + and any( + full.lower() == text.lower() if self.ignorecase else full == text + for full in self.msg + ) + ) + + +def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule: + """完全匹配消息。 + + 参数: + msg: 指定消息全匹配字符串元组 + ignorecase: 是否忽略大小写 + """ + if isinstance(msg, str): + msg = (msg,) + + return Rule(FullmatchRule(msg, ignorecase)) + + class KeywordsRule: """检查消息纯文本是否包含指定关键字。 diff --git a/tests/test_rule.py b/tests/test_rule.py index b3947b9a..7415f33a 100644 --- a/tests/test_rule.py +++ b/tests/test_rule.py @@ -105,6 +105,43 @@ async def test_endswith( assert await dependent(event=event) == expected +@pytest.mark.asyncio +@pytest.mark.parametrize( + "msg,ignorecase,type,text,expected", + [ + ("fullmatch", False, "message", "fullmatch", True), + ("fullmatch", False, "message", "Fullmatch", False), + ("fullmatch", True, "message", "fullmatch", True), + ("fullmatch", True, "message", "Fullmatch", True), + ("fullmatch", False, "message", "fullfoo", False), + ("fullmatch", False, "message", "_fullmatch_", False), + (("fullmatch", "foo"), False, "message", "fullmatchfoo", False), + ("fullmatch", False, "notice", "foo", False), + ], +) +async def test_fullmatch( + app: App, + msg: Union[str, Tuple[str, ...]], + ignorecase: bool, + type: str, + text: str, + expected: bool, +): + from nonebot.rule import FullmatchRule, fullmatch + + test_fullmatch = fullmatch(msg, ignorecase) + dependent = list(test_fullmatch.checkers)[0] + checker = dependent.call + + assert isinstance(checker, FullmatchRule) + assert checker.msg == (msg,) if isinstance(msg, str) else msg + assert checker.ignorecase == ignorecase + + message = make_fake_message()(text) + event = make_fake_event(_type=type, _message=message)() + assert await dependent(event=event) == expected + + @pytest.mark.asyncio @pytest.mark.parametrize( "kws,type,text,expected", diff --git a/website/docs/tutorial/plugin/create-matcher.md b/website/docs/tutorial/plugin/create-matcher.md index 56f1243a..b6a834ff 100644 --- a/website/docs/tutorial/plugin/create-matcher.md +++ b/website/docs/tutorial/plugin/create-matcher.md @@ -122,14 +122,15 @@ matcher = on_message() 5. `on_notice`: 创建通知事件响应器。 6. `on_startswith`: 创建消息开头匹配事件响应器。 7. `on_endswith`: 创建消息结尾匹配事件响应器。 -8. `on_keyword`: 创建消息关键词匹配事件响应器。 -9. `on_command`: 创建命令消息事件响应器。 -10. `on_shell_command`: 创建 shell 命令消息事件响应器。 -11. `on_regex`: 创建正则表达式匹配事件响应器。 -12. `CommandGroup`: 创建具有共同命令名称前缀的命令组。 -13. `MatcherGroup`: 创建具有共同参数的响应器组。 +8. `on_fullmatch`: 创建消息完全匹配事件响应器。 +9. `on_keyword`: 创建消息关键词匹配事件响应器。 +10. `on_command`: 创建命令消息事件响应器。 +11. `on_shell_command`: 创建 shell 命令消息事件响应器。 +12. `on_regex`: 创建正则表达式匹配事件响应器。 +13. `CommandGroup`: 创建具有共同命令名称前缀的命令组。 +14. `MatcherGroup`: 创建具有共同参数的响应器组。 -其中,`on_metaevent` `on_message` `on_request` `on_notice` 函数都是在 `on` 的基础上添加了对应的事件类型 `type`;`on_startswith` `on_endswith` `on_keyword` `on_command` `on_shell_command` `on_regex` 函数都是在 `on_message` 的基础上添加了对应的匹配规则 `rule`。 +其中,`on_metaevent` `on_message` `on_request` `on_notice` 函数都是在 `on` 的基础上添加了对应的事件类型 `type`;`on_startswith` `on_endswith` `on_fullmatch` `on_keyword` `on_command` `on_shell_command` `on_regex` 函数都是在 `on_message` 的基础上添加了对应的匹配规则 `rule`。 ## 自定义规则