mirror of
https://github.com/nonebot/nonebot2.git
synced 2024-11-27 18:45:05 +08:00
✨ Feature: 依赖注入支持 Generic TypeVar 和 Matcher 重载 (#2089)
This commit is contained in:
parent
6181c1760f
commit
f6b0809e5f
@ -317,7 +317,17 @@ class MatcherParam(Param):
|
||||
|
||||
# param type is Matcher(s) or subclass(es) of Matcher or None
|
||||
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
|
||||
elif param.annotation == param.empty and param.name == "matcher":
|
||||
return cls(Required)
|
||||
@ -325,6 +335,10 @@ class MatcherParam(Param):
|
||||
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
|
||||
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:
|
||||
def __init__(
|
||||
|
@ -58,8 +58,12 @@ def generic_check_issubclass(
|
||||
) -> bool:
|
||||
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
|
||||
|
||||
特别的,如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
|
||||
特别的:
|
||||
|
||||
- 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
|
||||
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。
|
||||
- 如果 cls 是 `typing.TypeVar` 类型,
|
||||
则会检查其 `__bound__` 或 `__constraints__` 是否是 class_or_tuple 中一个类型的子类或 None。
|
||||
"""
|
||||
try:
|
||||
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)
|
||||
for type_ in get_args(cls)
|
||||
)
|
||||
# ensure generic List, Dict can be checked
|
||||
elif origin:
|
||||
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
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Union
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
@ -31,5 +31,19 @@ async def union_bot(b: Union[FooBot, BarBot]) -> Union[FooBot, BarBot]:
|
||||
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]):
|
||||
...
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Union
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from nonebot.adapters import Event, Message
|
||||
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
|
||||
|
||||
|
||||
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]):
|
||||
...
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
from typing import Union, TypeVar
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import Received, LastReceived
|
||||
@ -7,6 +9,50 @@ async def matcher(m: Matcher) -> Matcher:
|
||||
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:
|
||||
return e
|
||||
|
||||
|
@ -90,7 +90,9 @@ async def test_bot(app: App):
|
||||
sub_bot,
|
||||
union_bot,
|
||||
legacy_bot,
|
||||
generic_bot,
|
||||
not_legacy_bot,
|
||||
generic_bot_none,
|
||||
)
|
||||
|
||||
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.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):
|
||||
async with app.test_dependent(not_bot, allow_types=[BotParam]) as ctx:
|
||||
...
|
||||
@ -139,8 +151,10 @@ async def test_event(app: App):
|
||||
union_event,
|
||||
legacy_event,
|
||||
event_message,
|
||||
generic_event,
|
||||
event_plain_text,
|
||||
not_legacy_event,
|
||||
generic_event_none,
|
||||
)
|
||||
|
||||
fake_message = make_fake_message()("text")
|
||||
@ -173,6 +187,14 @@ async def test_event(app: App):
|
||||
ctx.pass_params(event=fake_fooevent)
|
||||
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):
|
||||
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
|
||||
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()
|
||||
foo_matcher = FooMatcher()
|
||||
|
||||
async with app.test_dependent(matcher, allow_types=[MatcherParam]) as ctx:
|
||||
ctx.pass_params(matcher=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()()
|
||||
fake_matcher.set_receive("test", event)
|
||||
event_next = make_fake_event()()
|
||||
|
@ -71,7 +71,9 @@ async def _(foo: str = "bar"): ...
|
||||
|
||||
获取当前事件的 Bot 对象。
|
||||
|
||||
通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 `Bot` 依赖注入。
|
||||
通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 Bot 依赖注入。
|
||||
|
||||
Bot 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
|
||||
|
||||
<Tabs groupId="python">
|
||||
<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">
|
||||
<TabItem value="3.10" label="Python 3.10+" default>
|
||||
@ -143,6 +147,8 @@ async def _(event): ... # 兼容性处理
|
||||
|
||||
获取当前[会话状态](../appendices/session-state.md)。
|
||||
|
||||
通过标注参数为 `T_State` 类型,即可获取到当前会话状态。为兼容性考虑,如果参数名为 `state` 且无类型注解,也会视为 State 依赖注入。
|
||||
|
||||
```python
|
||||
from nonebot.typing import T_State
|
||||
|
||||
@ -153,10 +159,15 @@ async def _(foo: T_State): ...
|
||||
|
||||
获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。
|
||||
|
||||
通过标注参数为 `Matcher` 类型,或者一系列 `Matcher` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `matcher` 且无类型注解,也会视为 Matcher 依赖注入。
|
||||
|
||||
Matcher 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。
|
||||
|
||||
```python
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
async def _(matcher: Matcher): ...
|
||||
async def _(foo: Matcher): ...
|
||||
async def _(matcher): ... # 兼容性处理
|
||||
```
|
||||
|
||||
### Exception
|
||||
|
@ -12,6 +12,10 @@ options:
|
||||
|
||||
在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。
|
||||
|
||||
:::tip 提示
|
||||
事件响应器允许继承,你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。
|
||||
:::
|
||||
|
||||
## 事件响应器组成
|
||||
|
||||
### 事件响应器类型
|
||||
|
@ -66,7 +66,7 @@ async def handle_onebot(bot: OneBot):
|
||||
:::warning 注意
|
||||
重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。
|
||||
|
||||
但 Bot 和 Event 二者的参数类型注解具有最高检查优先级,如果二者类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
|
||||
但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
|
||||
:::
|
||||
|
||||
:::tip 提示
|
||||
|
Loading…
Reference in New Issue
Block a user