Feature: 支持 PEP 695 类型别名 (#3621)

This commit is contained in:
Ju4tCode
2025-08-07 14:54:22 +08:00
committed by GitHub
parent 56f52f2c9f
commit 0d8b81614a
16 changed files with 232 additions and 9 deletions

View File

@ -2,6 +2,7 @@ from collections.abc import Generator
from functools import wraps
import os
from pathlib import Path
import sys
import threading
from typing import TYPE_CHECKING, Callable, TypeVar
from typing_extensions import ParamSpec
@ -67,7 +68,14 @@ def run_once(func: Callable[P, R]) -> Callable[P, R]:
@run_once
def load_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
# preload global plugins
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
plugins: set["Plugin"] = set()
plugins |= nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
if sys.version_info >= (3, 12):
# preload python 3.12 plugins
plugins |= nonebot.load_plugins(
str(Path(__file__).parent / "python_3_12" / "plugins")
)
return plugins
@pytest.fixture(scope="session", autouse=True)

View File

@ -0,0 +1,7 @@
from pathlib import Path
from nonebot import load_plugins
_sub_plugins = set()
_sub_plugins |= load_plugins(str(Path(__file__).parent))

View File

@ -0,0 +1,10 @@
from typing import Annotated
from nonebot.adapters import Message
from nonebot.params import Arg
type AliasedArg = Annotated[Message, Arg()]
async def aliased_arg(key: AliasedArg) -> Message:
return key

View File

@ -0,0 +1,7 @@
from nonebot.adapters import Bot
type AliasedBot = Bot
async def get_aliased_bot(b: AliasedBot) -> Bot:
return b

View File

@ -0,0 +1,21 @@
from typing import Annotated
from nonebot import on_message
from nonebot.params import Depends
test_depends = on_message()
runned = []
def dependency():
runned.append(1)
return 1
type AliasedDepends = Annotated[int, Depends(dependency)]
@test_depends.handle()
async def aliased_depends(x: AliasedDepends):
return x

View File

@ -0,0 +1,7 @@
from nonebot.adapters import Event
type AliasedEvent = Event
async def aliased_event(e: AliasedEvent) -> Event:
return e

View File

@ -0,0 +1,5 @@
type AliasedException = Exception
async def aliased_exc(e: AliasedException) -> Exception:
return e

View File

@ -0,0 +1,7 @@
from nonebot.matcher import Matcher
type AliasedMatcher = Matcher
async def aliased_matcher(m: AliasedMatcher) -> Matcher:
return m

View File

@ -0,0 +1,7 @@
from nonebot.typing import T_State
type AliasedState = T_State
async def aliased_state(x: AliasedState) -> T_State:
return x

View File

@ -0,0 +1,3 @@
[tool.ruff]
extend = "../pyproject.toml"
target-version = "py312"

View File

@ -1,5 +1,6 @@
from contextlib import suppress
import re
import sys
from exceptiongroup import BaseExceptionGroup
from nonebug import App
@ -156,6 +157,22 @@ async def test_depend(app: App):
ctx.should_return(1)
@pytest.mark.anyio
@pytest.mark.skipif(
sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher"
)
async def test_aliased_depend(app: App):
from python_3_12.plugins.aliased_param.param_depend import aliased_depends, runned
async with app.test_dependent(aliased_depends, allow_types=[DependParam]) as ctx:
ctx.should_return(1)
assert len(runned) == 1
assert runned[0] == 1
runned.clear()
@pytest.mark.anyio
async def test_bot(app: App):
from plugins.param.param_bot import (
@ -221,6 +238,19 @@ async def test_bot(app: App):
app.test_dependent(not_bot, allow_types=[BotParam])
@pytest.mark.anyio
@pytest.mark.skipif(
sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher"
)
async def test_aliased_bot(app: App):
from python_3_12.plugins.aliased_param.param_bot import get_aliased_bot
async with app.test_dependent(get_aliased_bot, allow_types=[BotParam]) as ctx:
bot = ctx.create_bot()
ctx.pass_params(bot=bot)
ctx.should_return(bot)
@pytest.mark.anyio
async def test_event(app: App):
from plugins.param.param_event import (
@ -310,6 +340,21 @@ async def test_event(app: App):
ctx.should_return(fake_event.is_tome())
@pytest.mark.anyio
@pytest.mark.skipif(
sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher"
)
async def test_aliased_event(app: App):
from python_3_12.plugins.aliased_param.param_event import aliased_event
fake_message = FakeMessage("text")
fake_event = make_fake_event(_message=fake_message)()
async with app.test_dependent(aliased_event, allow_types=[EventParam]) as ctx:
ctx.pass_params(event=fake_event)
ctx.should_return(fake_event)
@pytest.mark.anyio
async def test_state(app: App):
from plugins.param.param_state import (
@ -461,6 +506,37 @@ async def test_state(app: App):
ctx.should_return(fake_state[KEYWORD_KEY])
@pytest.mark.anyio
@pytest.mark.skipif(
sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher"
)
async def test_aliased_state(app: App):
from python_3_12.plugins.aliased_param.param_state import aliased_state
fake_message = FakeMessage("text")
fake_matched = re.match(r"\[cq:(?P<type>.*?),(?P<arg>.*?)\]", "[cq:test,arg=value]")
fake_state = {
PREFIX_KEY: {
CMD_KEY: ("cmd",),
RAW_CMD_KEY: "/cmd",
CMD_START_KEY: "/",
CMD_ARG_KEY: fake_message,
CMD_WHITESPACE_KEY: " ",
},
SHELL_ARGV: ["-h"],
SHELL_ARGS: {"help": True},
REGEX_MATCHED: fake_matched,
STARTSWITH_KEY: "startswith",
ENDSWITH_KEY: "endswith",
FULLMATCH_KEY: "fullmatch",
KEYWORD_KEY: "keyword",
}
async with app.test_dependent(aliased_state, allow_types=[StateParam]) as ctx:
ctx.pass_params(state=fake_state)
ctx.should_return(fake_state)
@pytest.mark.anyio
async def test_matcher(app: App):
from plugins.param.param_matcher import (
@ -573,6 +649,20 @@ async def test_matcher(app: App):
ctx.should_return(False)
@pytest.mark.anyio
@pytest.mark.skipif(
sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher"
)
async def test_aliased_matcher(app: App):
from python_3_12.plugins.aliased_param.param_matcher import aliased_matcher
fake_matcher = Matcher()
async with app.test_dependent(aliased_matcher, allow_types=[MatcherParam]) as ctx:
ctx.pass_params(matcher=fake_matcher)
ctx.should_return(fake_matcher)
@pytest.mark.anyio
async def test_arg(app: App):
from plugins.param.param_arg import (
@ -642,11 +732,28 @@ async def test_arg(app: App):
ctx.should_return(message.extract_plain_text())
@pytest.mark.anyio
@pytest.mark.skipif(
sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher"
)
async def test_aliased_arg(app: App):
from python_3_12.plugins.aliased_param.param_arg import aliased_arg
matcher = Matcher()
message = FakeMessage("text")
matcher.set_arg("key", message)
async with app.test_dependent(aliased_arg, allow_types=[ArgParam]) as ctx:
ctx.pass_params(matcher=matcher)
ctx.should_return(message)
@pytest.mark.anyio
async def test_exception(app: App):
from plugins.param.param_exception import exc, legacy_exc
exception = ValueError("test")
async with app.test_dependent(exc, allow_types=[ExceptionParam]) as ctx:
ctx.pass_params(exception=exception)
ctx.should_return(exception)
@ -656,6 +763,20 @@ async def test_exception(app: App):
ctx.should_return(exception)
@pytest.mark.anyio
@pytest.mark.skipif(
sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher"
)
async def test_aliased_exception(app: App):
from python_3_12.plugins.aliased_param.param_exception import aliased_exc
exception = ValueError("test")
async with app.test_dependent(aliased_exc, allow_types=[ExceptionParam]) as ctx:
ctx.pass_params(exception=exception)
ctx.should_return(exception)
@pytest.mark.anyio
async def test_default(app: App):
from plugins.param.param_default import default