mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
chore(runtime): 使用 ruff format 并为 peer 补充中文文档
- 运行 ruff format 统一代码格式 - 说明 Peer 在协议层中的命名含义 - 为 Peer 类及其所有方法补充中文注释型文档
This commit is contained in:
13
run_tests.py
13
run_tests.py
@@ -9,6 +9,7 @@ Usage:
|
||||
python run_tests.py --cov # Run with coverage
|
||||
python run_tests.py -m "not slow" # Skip slow tests
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
@@ -30,11 +31,13 @@ def main() -> int:
|
||||
# Handle --cov flag
|
||||
if "--cov" in args:
|
||||
args.remove("--cov")
|
||||
cmd.extend([
|
||||
"--cov=src-new/astrbot_sdk",
|
||||
"--cov-report=term-missing",
|
||||
"--cov-report=html:.htmlcov",
|
||||
])
|
||||
cmd.extend(
|
||||
[
|
||||
"--cov=src-new/astrbot_sdk",
|
||||
"--cov-report=term-missing",
|
||||
"--cov-report=html:.htmlcov",
|
||||
]
|
||||
)
|
||||
|
||||
# Default flags if no specific args
|
||||
if not args:
|
||||
|
||||
@@ -13,7 +13,6 @@ from .messages import (
|
||||
InitializeMessage,
|
||||
InvokeMessage,
|
||||
PeerInfo,
|
||||
ProtocolMessage,
|
||||
ResultMessage,
|
||||
)
|
||||
|
||||
|
||||
@@ -27,6 +27,13 @@ CancelHandler = Callable[[str], Awaitable[None]]
|
||||
|
||||
|
||||
class Peer:
|
||||
"""表示协议连接中的一个对等端。
|
||||
|
||||
`Peer` 封装一条双向传输通道上的消息收发、初始化握手、能力调用、
|
||||
流式事件转发与取消处理。这里的 `peer` 指“通信对端/本端”这一网络
|
||||
协议概念,而不是业务上的用户、群聊或会话对象。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -34,6 +41,13 @@ class Peer:
|
||||
peer_info: PeerInfo,
|
||||
protocol_version: str = "1.0",
|
||||
) -> None:
|
||||
"""创建一个协议对等端实例。
|
||||
|
||||
Args:
|
||||
transport: 底层传输实现,负责发送字符串消息并回调入站消息。
|
||||
peer_info: 当前端点对外声明的身份信息。
|
||||
protocol_version: 当前端点支持的协议版本,用于初始化握手校验。
|
||||
"""
|
||||
self.transport = transport
|
||||
self.peer_info = peer_info
|
||||
self.protocol_version = protocol_version
|
||||
@@ -55,19 +69,24 @@ class Peer:
|
||||
self._remote_initialized = asyncio.Event()
|
||||
|
||||
def set_initialize_handler(self, handler: InitializeHandler) -> None:
|
||||
"""注册处理远端 `initialize` 请求的握手处理器。"""
|
||||
self._initialize_handler = handler
|
||||
|
||||
def set_invoke_handler(self, handler: InvokeHandler) -> None:
|
||||
"""注册处理远端 `invoke` 请求的能力调用处理器。"""
|
||||
self._invoke_handler = handler
|
||||
|
||||
def set_cancel_handler(self, handler: CancelHandler) -> None:
|
||||
"""注册处理远端 `cancel` 请求的取消回调。"""
|
||||
self._cancel_handler = handler
|
||||
|
||||
async def start(self) -> None:
|
||||
"""启动传输层并将原始入站消息绑定到当前 `Peer`。"""
|
||||
self.transport.set_message_handler(self._handle_raw_message)
|
||||
await self.transport.start()
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""关闭 `Peer` 并清理所有挂起中的请求、流和入站任务。"""
|
||||
self._closed = True
|
||||
# 终止所有挂起的 RPC,避免调用方永久挂起
|
||||
for future in list(self._pending_results.values()):
|
||||
@@ -88,9 +107,15 @@ class Peer:
|
||||
await self.transport.stop()
|
||||
|
||||
async def wait_closed(self) -> None:
|
||||
"""等待底层传输彻底关闭。"""
|
||||
await self.transport.wait_closed()
|
||||
|
||||
async def wait_until_remote_initialized(self, timeout: float | None = 30.0) -> None:
|
||||
"""等待远端完成初始化握手。
|
||||
|
||||
Args:
|
||||
timeout: 等待秒数。传入 `None` 表示无限等待。
|
||||
"""
|
||||
if timeout is None:
|
||||
await self._remote_initialized.wait()
|
||||
return
|
||||
@@ -102,6 +127,15 @@ class Peer:
|
||||
*,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> InitializeOutput:
|
||||
"""向远端发送初始化请求并缓存远端声明的能力信息。
|
||||
|
||||
Args:
|
||||
handlers: 当前端点声明可接收的处理器列表。
|
||||
metadata: 附带给远端的握手元数据。
|
||||
|
||||
Returns:
|
||||
远端返回的初始化结果。
|
||||
"""
|
||||
self._ensure_usable()
|
||||
request_id = self._next_id()
|
||||
future: asyncio.Future[ResultMessage] = (
|
||||
@@ -141,6 +175,14 @@ class Peer:
|
||||
stream: bool = False,
|
||||
request_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""发起一次非流式能力调用并等待最终结果。
|
||||
|
||||
Args:
|
||||
capability: 远端能力名。
|
||||
payload: 调用输入。
|
||||
stream: 必须为 `False`;流式场景应改用 `invoke_stream()`。
|
||||
request_id: 可选的请求 ID;未提供时自动生成。
|
||||
"""
|
||||
self._ensure_usable()
|
||||
if stream:
|
||||
raise ValueError("stream=True 请使用 invoke_stream()")
|
||||
@@ -171,6 +213,16 @@ class Peer:
|
||||
*,
|
||||
request_id: str | None = None,
|
||||
) -> AsyncIterator[EventMessage]:
|
||||
"""发起一次流式能力调用并返回事件迭代器。
|
||||
|
||||
调用方会收到 `delta` 事件,`started` 会被内部吞掉,
|
||||
`completed` 用于结束迭代,`failed` 会转换为异常抛出。
|
||||
|
||||
Args:
|
||||
capability: 远端能力名。
|
||||
payload: 调用输入。
|
||||
request_id: 可选的请求 ID;未提供时自动生成。
|
||||
"""
|
||||
self._ensure_usable()
|
||||
request_id = request_id or self._next_id()
|
||||
queue: asyncio.Queue[Any] = asyncio.Queue()
|
||||
@@ -209,17 +261,21 @@ class Peer:
|
||||
return iterator()
|
||||
|
||||
async def cancel(self, request_id: str, reason: str = "user_cancelled") -> None:
|
||||
"""向远端发送取消请求,尝试中止指定 ID 的在途调用。"""
|
||||
await self._send(CancelMessage(id=request_id, reason=reason))
|
||||
|
||||
def _next_id(self) -> str:
|
||||
"""生成当前连接内递增的消息 ID。"""
|
||||
self._counter += 1
|
||||
return f"msg_{self._counter:04d}"
|
||||
|
||||
def _ensure_usable(self) -> None:
|
||||
"""确保连接仍处于可用状态,否则立即抛出协议错误。"""
|
||||
if self._unusable:
|
||||
raise AstrBotError.protocol_error("连接已进入不可用状态")
|
||||
|
||||
async def _handle_raw_message(self, payload: str) -> None:
|
||||
"""解析原始消息并分发到对应的消息处理分支。"""
|
||||
message = parse_message(payload)
|
||||
if isinstance(message, ResultMessage):
|
||||
await self._handle_result(message)
|
||||
@@ -245,6 +301,7 @@ class Peer:
|
||||
return
|
||||
|
||||
async def _handle_initialize(self, message: InitializeMessage) -> None:
|
||||
"""处理远端发起的初始化握手并返回握手结果。"""
|
||||
self.remote_peer = message.peer
|
||||
self.remote_handlers = message.handlers
|
||||
self.remote_metadata = message.metadata
|
||||
@@ -276,6 +333,7 @@ class Peer:
|
||||
self._remote_initialized.set()
|
||||
|
||||
async def _handle_invoke(self, message: InvokeMessage) -> None:
|
||||
"""处理远端发起的能力调用,并按流式或非流式协议返回结果。"""
|
||||
active = self._inbound_tasks.get(message.id)
|
||||
token = active[1] if active is not None else CancelToken()
|
||||
try:
|
||||
@@ -320,6 +378,7 @@ class Peer:
|
||||
)
|
||||
|
||||
async def _handle_cancel(self, message: CancelMessage) -> None:
|
||||
"""处理远端取消请求并终止对应的入站任务。"""
|
||||
inbound = self._inbound_tasks.get(message.id)
|
||||
if inbound is None:
|
||||
return
|
||||
@@ -330,6 +389,7 @@ class Peer:
|
||||
task.cancel()
|
||||
|
||||
async def _handle_result(self, message: ResultMessage) -> None:
|
||||
"""处理非流式结果消息并唤醒等待中的调用方。"""
|
||||
future = self._pending_results.pop(message.id, None)
|
||||
if future is None:
|
||||
queue = self._pending_streams.get(message.id)
|
||||
@@ -343,6 +403,7 @@ class Peer:
|
||||
future.set_result(message)
|
||||
|
||||
async def _handle_event(self, message: EventMessage) -> None:
|
||||
"""处理流式事件消息并投递到对应请求的事件队列。"""
|
||||
queue = self._pending_streams.get(message.id)
|
||||
if queue is None:
|
||||
future = self._pending_results.get(message.id)
|
||||
@@ -356,6 +417,7 @@ class Peer:
|
||||
async def _send_error_result(
|
||||
self, message: InvokeMessage, error: AstrBotError
|
||||
) -> None:
|
||||
"""根据调用模式,将错误编码为 `result` 或失败事件发回远端。"""
|
||||
if message.stream:
|
||||
await self._send(
|
||||
EventMessage(
|
||||
@@ -376,6 +438,7 @@ class Peer:
|
||||
async def _reject_initialize(
|
||||
self, message: InitializeMessage, error: AstrBotError
|
||||
) -> None:
|
||||
"""拒绝一次初始化握手,并把连接标记为不可继续使用。"""
|
||||
await self._send(
|
||||
ResultMessage(
|
||||
id=message.id,
|
||||
@@ -389,8 +452,10 @@ class Peer:
|
||||
await self.stop()
|
||||
|
||||
async def _send_cancelled_termination(self, message: InvokeMessage) -> None:
|
||||
"""把本端取消执行转换为标准化的取消错误响应。"""
|
||||
error = AstrBotError.cancelled()
|
||||
await self._send_error_result(message, error)
|
||||
|
||||
async def _send(self, message) -> None:
|
||||
"""序列化协议消息并通过底层传输发送出去。"""
|
||||
await self.transport.send(message.model_dump_json(exclude_none=True))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytest
|
||||
|
||||
# 将 src-new 加入路径 - 这使得测试可以运行,但不算"已安装"
|
||||
import sys
|
||||
|
||||
SRC_NEW_PATH = str(Path(__file__).parent.parent / "src-new")
|
||||
sys.path.insert(0, SRC_NEW_PATH)
|
||||
|
||||
@@ -16,6 +17,7 @@ sys.path.insert(0, SRC_NEW_PATH)
|
||||
# Async Configuration
|
||||
# ============================================================
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
|
||||
"""Create an event loop for async tests."""
|
||||
@@ -29,6 +31,7 @@ def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
|
||||
# Transport Fixtures
|
||||
# ============================================================
|
||||
|
||||
|
||||
class MemoryTransport:
|
||||
"""In-memory transport for testing peer communication."""
|
||||
|
||||
@@ -74,6 +77,7 @@ def transport_pair() -> tuple[MemoryTransport, MemoryTransport]:
|
||||
# Mock/Fake Fixtures
|
||||
# ============================================================
|
||||
|
||||
|
||||
class FakeEnvManager:
|
||||
"""Fake environment manager for testing."""
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Unit tests for API decorators and Star class.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
@@ -167,6 +168,7 @@ class TestStarClass:
|
||||
|
||||
def test_star_is_new_star_by_default(self):
|
||||
"""Star subclasses should be recognized as new-style."""
|
||||
|
||||
class MyPlugin(Star):
|
||||
pass
|
||||
|
||||
@@ -174,6 +176,7 @@ class TestStarClass:
|
||||
|
||||
def test_star_collects_handler_names_from_decorators(self):
|
||||
"""Star should collect decorated method names in __handlers__."""
|
||||
|
||||
class MyPlugin(Star):
|
||||
@on_command("hello")
|
||||
async def hello(self, event: MessageEvent, ctx: Context):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Tests for api/event/filter.py - Event filter decorators and utilities.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot_sdk.api.event.filter import ADMIN, command, filter, permission, regex
|
||||
from astrbot_sdk.decorators import get_handler_meta
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""
|
||||
Tests for _legacy_api.py - Legacy compatibility layer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Tests for API module exports and re-exports.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestApiStarModule:
|
||||
|
||||
@@ -3,15 +3,19 @@ Pytest-based tests for transport and peer communication.
|
||||
|
||||
These tests demonstrate the pytest fixtures defined in conftest.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot_sdk.context import CancelToken
|
||||
from astrbot_sdk.errors import AstrBotError
|
||||
from astrbot_sdk.protocol.descriptors import CapabilityDescriptor
|
||||
from astrbot_sdk.protocol.messages import EventMessage, InitializeOutput, PeerInfo, ResultMessage
|
||||
from astrbot_sdk.runtime.capability_router import CapabilityRouter, StreamExecution
|
||||
from astrbot_sdk.protocol.messages import (
|
||||
EventMessage,
|
||||
PeerInfo,
|
||||
ResultMessage,
|
||||
)
|
||||
from astrbot_sdk.runtime.capability_router import CapabilityRouter
|
||||
from astrbot_sdk.runtime.peer import Peer
|
||||
|
||||
|
||||
@@ -98,7 +102,6 @@ class TestProtocolErrors:
|
||||
|
||||
async def test_stream_true_receiving_result_is_error(self, transport_pair):
|
||||
"""stream=true receiving result should raise protocol error."""
|
||||
import asyncio
|
||||
|
||||
left, right = transport_pair
|
||||
|
||||
@@ -113,7 +116,9 @@ class TestProtocolErrors:
|
||||
stream = await plugin.invoke_stream(
|
||||
"llm.stream_chat", {"prompt": "bad"}, request_id="stream-1"
|
||||
)
|
||||
await left.send(ResultMessage(id="stream-1", success=True, output={}).model_dump_json())
|
||||
await left.send(
|
||||
ResultMessage(id="stream-1", success=True, output={}).model_dump_json()
|
||||
)
|
||||
|
||||
with pytest.raises(AstrBotError) as exc_info:
|
||||
async for _ in stream:
|
||||
@@ -134,9 +139,7 @@ class TestCapabilityRouter:
|
||||
invalid_names = ["llm", "llm.chat.extra", "LLM.chat", "llm.Chat"]
|
||||
for name in invalid_names:
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
router.register(
|
||||
CapabilityDescriptor(name=name, description="invalid")
|
||||
)
|
||||
router.register(CapabilityDescriptor(name=name, description="invalid"))
|
||||
assert name in str(exc_info.value)
|
||||
|
||||
def test_reserved_namespaces_rejected_for_exposed(self):
|
||||
@@ -146,9 +149,7 @@ class TestCapabilityRouter:
|
||||
reserved_names = ["handler.demo", "system.health", "internal.trace"]
|
||||
for name in reserved_names:
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
router.register(
|
||||
CapabilityDescriptor(name=name, description="reserved")
|
||||
)
|
||||
router.register(CapabilityDescriptor(name=name, description="reserved"))
|
||||
assert name in str(exc_info.value)
|
||||
|
||||
def test_reserved_namespaces_allowed_for_hidden(self):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Unit tests for Context module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Tests for decorators.py - Handler decorator infrastructure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot_sdk.decorators import (
|
||||
HANDLER_META_ATTR,
|
||||
@@ -57,6 +57,7 @@ class TestGetHandlerMeta:
|
||||
|
||||
def test_returns_none_for_undecorated_function(self):
|
||||
"""get_handler_meta should return None for undecorated functions."""
|
||||
|
||||
async def plain_function():
|
||||
pass
|
||||
|
||||
@@ -212,6 +213,7 @@ class TestOnEventDecorator:
|
||||
event_types = ["message_received", "user_joined", "custom_event"]
|
||||
|
||||
for event_type in event_types:
|
||||
|
||||
@on_event(event_type)
|
||||
async def handler():
|
||||
pass
|
||||
|
||||
@@ -28,7 +28,7 @@ def _is_astrbot_sdk_installed_in_site_packages() -> bool:
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.skipif(
|
||||
not _is_astrbot_sdk_installed_in_site_packages(),
|
||||
reason="astrbot_sdk not installed in site-packages (run: pip install -e .)"
|
||||
reason="astrbot_sdk not installed in site-packages (run: pip install -e .)",
|
||||
)
|
||||
class EntryPointTest(unittest.TestCase):
|
||||
def test_import_package(self) -> None:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Unit tests for Events module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
Reference in New Issue
Block a user