chore(runtime): 使用 ruff format 并为 peer 补充中文文档

- 运行 ruff format 统一代码格式
- 说明 Peer 在协议层中的命名含义
- 为 Peer 类及其所有方法补充中文注释型文档
This commit is contained in:
united_pooh
2026-03-13 01:39:22 +08:00
parent 0159b008c3
commit f865b2b7e9
14 changed files with 102 additions and 24 deletions

View File

@@ -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:

View File

@@ -13,7 +13,6 @@ from .messages import (
InitializeMessage,
InvokeMessage,
PeerInfo,
ProtocolMessage,
ResultMessage,
)

View File

@@ -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))

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import asyncio
import tempfile
import unittest
from pathlib import Path

View File

@@ -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."""

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -1,9 +1,9 @@
"""
Tests for API module exports and re-exports.
"""
from __future__ import annotations
import pytest
class TestApiStarModule:

View File

@@ -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):

View File

@@ -1,6 +1,7 @@
"""
Unit tests for Context module.
"""
from __future__ import annotations
import asyncio

View File

@@ -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

View File

@@ -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:

View File

@@ -1,9 +1,9 @@
"""
Unit tests for Events module.
"""
from __future__ import annotations
import asyncio
import pytest