Feature: 依赖注入支持 Generic TypeVar 和 Matcher 重载 (#2089)

This commit is contained in:
Ju4tCode 2023-06-11 15:33:33 +08:00 committed by GitHub
parent 6181c1760f
commit f6b0809e5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 198 additions and 10 deletions

View File

@ -317,7 +317,17 @@ class MatcherParam(Param):
# param type is Matcher(s) or subclass(es) of Matcher or None # param type is Matcher(s) or subclass(es) of Matcher or None
if generic_check_issubclass(param.annotation, Matcher): if generic_check_issubclass(param.annotation, Matcher):
return cls(Required) checker: Optional[ModelField] = None
if param.annotation is not Matcher:
checker = ModelField(
name=param.name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
)
return cls(Required, checker=checker)
# legacy: param is named "matcher" and has no type annotation # legacy: param is named "matcher" and has no type annotation
elif param.annotation == param.empty and param.name == "matcher": elif param.annotation == param.empty and param.name == "matcher":
return cls(Required) return cls(Required)
@ -325,6 +335,10 @@ class MatcherParam(Param):
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any: async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
return matcher return matcher
async def _check(self, matcher: "Matcher", **kwargs: Any) -> Any:
if checker := self.extra.get("checker", None):
check_field_type(checker, matcher)
class ArgInner: class ArgInner:
def __init__( def __init__(

View File

@ -58,8 +58,12 @@ def generic_check_issubclass(
) -> bool: ) -> bool:
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。 """检查 cls 是否是 class_or_tuple 中的一个类型子类。
特别的如果 cls `typing.Union` `types.UnionType` 类型 特别的
- 如果 cls `typing.Union` `types.UnionType` 类型
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None 则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None
- 如果 cls `typing.TypeVar` 类型
则会检查其 `__bound__` `__constraints__` 是否是 class_or_tuple 中一个类型的子类或 None
""" """
try: try:
return issubclass(cls, class_or_tuple) return issubclass(cls, class_or_tuple)
@ -70,8 +74,18 @@ def generic_check_issubclass(
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple) is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
for type_ in get_args(cls) for type_ in get_args(cls)
) )
# ensure generic List, Dict can be checked
elif origin: elif origin:
return issubclass(origin, class_or_tuple) return issubclass(origin, class_or_tuple)
elif isinstance(cls, TypeVar):
if cls.__constraints__:
return all(
is_none_type(type_)
or generic_check_issubclass(type_, class_or_tuple)
for type_ in cls.__constraints__
)
elif cls.__bound__:
return generic_check_issubclass(cls.__bound__, class_or_tuple)
return False return False

View File

@ -1,4 +1,4 @@
from typing import Union from typing import Union, TypeVar
from nonebot.adapters import Bot from nonebot.adapters import Bot
@ -31,5 +31,19 @@ async def union_bot(b: Union[FooBot, BarBot]) -> Union[FooBot, BarBot]:
return b return b
B = TypeVar("B", bound=Bot)
async def generic_bot(b: B) -> B:
return b
CB = TypeVar("CB", Bot, None)
async def generic_bot_none(b: CB) -> CB:
return b
async def not_bot(b: Union[int, Bot]): async def not_bot(b: Union[int, Bot]):
... ...

View File

@ -1,4 +1,4 @@
from typing import Union from typing import Union, TypeVar
from nonebot.adapters import Event, Message from nonebot.adapters import Event, Message
from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText
@ -32,6 +32,20 @@ async def union_event(e: Union[FooEvent, BarEvent]) -> Union[FooEvent, BarEvent]
return e return e
E = TypeVar("E", bound=Event)
async def generic_event(e: E) -> E:
return e
CE = TypeVar("CE", Event, None)
async def generic_event_none(e: CE) -> CE:
return e
async def not_event(e: Union[int, Event]): async def not_event(e: Union[int, Event]):
... ...

View File

@ -1,3 +1,5 @@
from typing import Union, TypeVar
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.params import Received, LastReceived from nonebot.params import Received, LastReceived
@ -7,6 +9,50 @@ async def matcher(m: Matcher) -> Matcher:
return m return m
async def legacy_matcher(matcher):
return matcher
async def not_legacy_matcher(matcher: int):
...
class FooMatcher(Matcher):
...
async def sub_matcher(m: FooMatcher) -> FooMatcher:
return m
class BarMatcher(Matcher):
...
async def union_matcher(
m: Union[FooMatcher, BarMatcher]
) -> Union[FooMatcher, BarMatcher]:
return m
M = TypeVar("M", bound=Matcher)
async def generic_matcher(m: M) -> M:
return m
CM = TypeVar("CM", Matcher, None)
async def generic_matcher_none(m: CM) -> CM:
return m
async def not_matcher(m: Union[int, Matcher]):
...
async def receive(e: Event = Received("test")) -> Event: async def receive(e: Event = Received("test")) -> Event:
return e return e

View File

@ -90,7 +90,9 @@ async def test_bot(app: App):
sub_bot, sub_bot,
union_bot, union_bot,
legacy_bot, legacy_bot,
generic_bot,
not_legacy_bot, not_legacy_bot,
generic_bot_none,
) )
async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx: async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx:
@ -122,6 +124,16 @@ async def test_bot(app: App):
ctx.pass_params(bot=bot) ctx.pass_params(bot=bot)
ctx.should_return(bot) ctx.should_return(bot)
async with app.test_dependent(generic_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot()
ctx.pass_params(bot=bot)
ctx.should_return(bot)
async with app.test_dependent(generic_bot_none, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot()
ctx.pass_params(bot=bot)
ctx.should_return(bot)
with pytest.raises(ValueError): with pytest.raises(ValueError):
async with app.test_dependent(not_bot, allow_types=[BotParam]) as ctx: async with app.test_dependent(not_bot, allow_types=[BotParam]) as ctx:
... ...
@ -139,8 +151,10 @@ async def test_event(app: App):
union_event, union_event,
legacy_event, legacy_event,
event_message, event_message,
generic_event,
event_plain_text, event_plain_text,
not_legacy_event, not_legacy_event,
generic_event_none,
) )
fake_message = make_fake_message()("text") fake_message = make_fake_message()("text")
@ -173,6 +187,14 @@ async def test_event(app: App):
ctx.pass_params(event=fake_fooevent) ctx.pass_params(event=fake_fooevent)
ctx.should_return(fake_event) ctx.should_return(fake_event)
async with app.test_dependent(generic_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
ctx.should_return(fake_event)
async with app.test_dependent(generic_event_none, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
ctx.should_return(fake_event)
with pytest.raises(ValueError): with pytest.raises(ValueError):
async with app.test_dependent(not_event, allow_types=[EventParam]) as ctx: async with app.test_dependent(not_event, allow_types=[EventParam]) as ctx:
... ...
@ -351,14 +373,63 @@ async def test_state(app: App):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_matcher(app: App): async def test_matcher(app: App):
from plugins.param.param_matcher import matcher, receive, last_receive from plugins.param.param_matcher import (
FooMatcher,
matcher,
receive,
not_matcher,
sub_matcher,
last_receive,
union_matcher,
legacy_matcher,
generic_matcher,
not_legacy_matcher,
generic_matcher_none,
)
fake_matcher = Matcher() fake_matcher = Matcher()
foo_matcher = FooMatcher()
async with app.test_dependent(matcher, allow_types=[MatcherParam]) as ctx: async with app.test_dependent(matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher) ctx.pass_params(matcher=fake_matcher)
ctx.should_return(fake_matcher) ctx.should_return(fake_matcher)
async with app.test_dependent(legacy_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher)
ctx.should_return(fake_matcher)
with pytest.raises(ValueError):
async with app.test_dependent(
not_legacy_matcher, allow_types=[MatcherParam]
) as ctx:
...
async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=foo_matcher)
ctx.should_return(foo_matcher)
with pytest.raises(TypeMisMatch):
async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher)
async with app.test_dependent(union_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=foo_matcher)
ctx.should_return(foo_matcher)
async with app.test_dependent(generic_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher)
ctx.should_return(fake_matcher)
async with app.test_dependent(
generic_matcher_none, allow_types=[MatcherParam]
) as ctx:
ctx.pass_params(matcher=fake_matcher)
ctx.should_return(fake_matcher)
with pytest.raises(ValueError):
async with app.test_dependent(not_matcher, allow_types=[MatcherParam]) as ctx:
...
event = make_fake_event()() event = make_fake_event()()
fake_matcher.set_receive("test", event) fake_matcher.set_receive("test", event)
event_next = make_fake_event()() event_next = make_fake_event()()

View File

@ -71,7 +71,9 @@ async def _(foo: str = "bar"): ...
获取当前事件的 Bot 对象。 获取当前事件的 Bot 对象。
通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 `Bot` 依赖注入。 通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 Bot 依赖注入。
Bot 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
<Tabs groupId="python"> <Tabs groupId="python">
<TabItem value="3.10" label="Python 3.10+" default> <TabItem value="3.10" label="Python 3.10+" default>
@ -108,7 +110,9 @@ async def _(bot): ... # 兼容性处理
获取当前事件。 获取当前事件。
通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 `Event` 依赖注入。 通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 Event 依赖注入。
Event 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
<Tabs groupId="python"> <Tabs groupId="python">
<TabItem value="3.10" label="Python 3.10+" default> <TabItem value="3.10" label="Python 3.10+" default>
@ -143,6 +147,8 @@ async def _(event): ... # 兼容性处理
获取当前[会话状态](../appendices/session-state.md)。 获取当前[会话状态](../appendices/session-state.md)。
通过标注参数为 `T_State` 类型,即可获取到当前会话状态。为兼容性考虑,如果参数名为 `state` 且无类型注解,也会视为 State 依赖注入。
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@ -153,10 +159,15 @@ async def _(foo: T_State): ...
获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。 获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。
通过标注参数为 `Matcher` 类型,或者一系列 `Matcher` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `matcher` 且无类型注解,也会视为 Matcher 依赖注入。
Matcher 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
```python ```python
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
async def _(matcher: Matcher): ... async def _(foo: Matcher): ...
async def _(matcher): ... # 兼容性处理
``` ```
### Exception ### Exception

View File

@ -12,6 +12,10 @@ options:
在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。 在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。
:::tip 提示
事件响应器允许继承,你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。
:::
## 事件响应器组成 ## 事件响应器组成
### 事件响应器类型 ### 事件响应器类型

View File

@ -66,7 +66,7 @@ async def handle_onebot(bot: OneBot):
:::warning 注意 :::warning 注意
重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。 重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。
但 Bot 和 Event 二者的参数类型注解具有最高检查优先级,如果二者类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。 但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
::: :::
:::tip 提示 :::tip 提示