Type Server Protocol(TSP):面向 Python 类型分析的 JSON-RPC 协议
Type Server Protocol
Type Server Protocol(TSP)是一套 JSON-RPC 协议,用于向类型服务器查询 Python 分析数据,涵盖协议版本、快照、Python 搜索路径、导入解析以及计算/声明/期望类型查询。
译文尚不完整,完整内容请切换到原文。
Type Server Protocol(TSP)是一种 JSON-RPC 协议,用于向类型服务器请求 Python 分析数据。类型服务器维护工作区的类型分析状态,并响应关于协议版本、快照、Python 搜索路径、导入解析以及类型查询(计算类型、声明类型和预期类型)的请求。
该协议在有用之处采用 Language Server Protocol(LSP)的数据结构和约定,例如 Position 和 Range,并且像 LSP 一样将 URI 表示为字符串。TSP 方法使用 typeServer/ 前缀。
此文件夹中的协议产物是权威定义:
- typeServerProtocol.ts 定义了 TypeScript 接口和请求类型。
- tsp.json 是机器可读的协议模型。
- tsp.schema.json 是协议模型的 JSON schema。
版本历史
TSP 使用语义化版本字符串进行客户端/服务器兼容性检查。当协议处于 0.x 系列时,次要版本变更可能具有破坏性。补丁版本变更在同一次要版本系列内保持向后兼容。
| 版本 | 变更 |
|---|---|
0.1.0 |
初始协议版本。 |
0.2.0 |
新增了请求类型和字段。 |
0.3.0 |
切换到更复杂的类型结构。 |
0.4.0 |
切换到 Type 联合类型和基于 stub 的类型载荷。 |
0.4.1 |
新增了多连接协商和 typeServer/connection 控制请求。 |
连接模型
客户端启动类型服务器,并通过主 JSON-RPC 连接使用服务器支持的传输方式(例如 stdio 或 Node IPC)与其通信。使用 stdio 时,stdout 必须保留给 JSON-RPC 协议消息。
类型服务器需要接收足够的工作区和文档状态,以响应其支持的 TSP 请求。客户端通常使用常规 LSP 初始化和文档同步来提供此状态,然后在同一 JSON-RPC 连接上发送 typeServer/* 请求。
TSP 还支持可选的额外只读连接。原始连接仍是主连接,负责初始化、工作区状态、文档同步、状态变更通知和连接控制。只有在客户端和服务器于初始化期间协商支持后,才会打开额外连接。
启动顺序
典型会话按以下顺序进行:
- 客户端启动或连接到类型服务器。
- 客户端和服务器初始化任何共享工作区状态,通常通过 LSP
initialize和initialized消息。 - 客户端调用
typeServer/getSupportedProtocolVersion并验证返回的 semver 字符串与其期望的协议版本兼容。 - 客户端调用
typeServer/getSnapshot以获取第一个快照标识符。 - 客户端发送类型查询、导入解析或搜索路径请求,并在请求结构要求时包含当前快照。
- 当先前的类型结果不再有效时,服务器发送
typeServer/snapshotChanged。
多连接模式
多连接模式是可选的性能特性。它允许客户端为只读 TSP 请求向同一类型服务器打开额外的本地通道,而主连接继续拥有可变的工作区状态。
客户端应仅在能够从并发只读工作中受益时使用多连接模式,例如高容量类型查询或不应阻塞主 JSON-RPC 连接的后台工作线程。服务器仍必须在单连接模式下正常工作。协议正确性不得要求支持多连接。
能力握手
多连接支持通过常规的 LSP initialize 请求和响应进行协商。能够打开额外 TSP 通道的客户端在 capabilities.experimental.typeServerMultiConnection 下声明其支持的传输类型:
{
"capabilities": {
"experimental": {
"typeServerMultiConnection": {
"supportedTransports": ["ipc"]
}
}
}
}能够提供额外 TSP 通道的服务器在 initialize 结果中返回相同的能力结构:
{
"capabilities": {
"experimental": {
"typeServerMultiConnection": {
"supportedTransports": ["ipc"]
}
}
}
}仅当双方都声明 typeServerMultiConnection 且两个 supportedTransports 列表至少有一个共同传输类型时,才会启用多连接模式。如果任一方省略该能力,或没有共同传输类型,则会话保持单连接,客户端不得发送 typeServer/connection。
目前额外连接支持的内置传输类型为 ipc。此协商与主连接相互独立,主连接可使用 stdio 或 Node IPC。
打开额外连接
在初始化与传输协商之后,客户端通过在主连接上发送 typeServer/connection 来打开额外通道:
{
"type": "open",
"kind": "ipc",
"args": ["<ipc-endpoint>"]
}对于 ipc,args 数组标识服务器应连接的一个或多个端点。服务器应验证请求,及时连接到所提供的端点,并在接受请求时返回带有 success: true 的结果。如果服务器无法打开该通道,应返回 success: false,并在对日志有用时附带简短的 message。
IPC 端点参数
当平台 IPC 机制支持时,ipc 端点应提供全双工 JSON-RPC 流。在这种情况下,args 包含一个端点字符串:
{
"type": "open",
"kind": "ipc",
"args": ["<full-duplex-ipc-endpoint>"]
}如果平台 IPC 机制仅暴露单向端点,args 包含两个端点字符串。端点名称从类型服务器的视角命名:第一个端点是服务器输入流,第二个端点是服务器输出流。
{
"type": "open",
"kind": "ipc",
"args": ["<input-ipc-endpoint>", "<output-ipc-endpoint>"]
}关闭额外连接
客户端通过在主连接上发送带有相同传输类型和端点标识的 typeServer/connection 来关闭额外通道:
{
"type": "close",
"kind": "ipc",
"args": ["<ipc-endpoint>"]
}服务器应停止服务匹配的额外通道并及时关闭传输。如果服务器未在客户端的超时预算内完成关闭,客户端也可以拆除其本地传输。
close 请求应使用与打开通道时相同的 args 结构:全双工 IPC 流使用一个端点,拆分 IPC 流使用相同的输入/输出端点对。
允许的流量
主连接拥有所有改变状态的流量。以下内容必须仅限主连接:
- LSP 初始化和生命周期消息。
- 工作区、配置、文件监视器和文档同步通知。
- 服务器到客户端的快照通知。
typeServer/connection控制请求。
额外连接仅限 TSP 且为只读。它们可用于不会改变服务器状态、且其结果对客户端提供的快照有效的请求。
额外连接上允许的 TSP 消息受协商的 TSP 版本约束。如果服务器报告支持某个 TSP 版本并为该会话声明多连接支持,则它必须支持该版本定义的所有额外连接 TSP 消息。
下表列出了首次允许各消息在额外连接上使用的 TSP 版本:
| TSP 版本 | 额外连接消息 |
|---|---|
0.4.1 |
typeServer/getSupportedProtocolVersion |
0.4.1 |
typeServer/getSnapshot |
0.4.1 |
typeServer/getPythonSearchPaths |
0.4.1 |
typeServer/resolveImport |
0.4.1 |
typeServer/getComputedType |
0.4.1 |
typeServer/getDeclaredType |
0.4.1 |
typeServer/getExpectedType |
服务器在额外连接上收到 LSP 流量、改变状态的通知、typeServer/connection 以及任何协商 TSP 版本不允许的 TSP 消息时,应予以拒绝。
快照
快照是一个非负整数,用于标识类型服务器分析状态在某一时间点的视图。类型句柄、声明、符号以及其他返回的对象仅在其被返回时所对应的快照中有效,除非请求中明确说明否则。
只要先前返回的类型分析结果可能失效,类型服务器就必须推进快照。典型的失效事件包括已打开文件的更改、已关闭文件、配置更改、监视文件的更改、依赖项更改,或任何会改变分析结果的缓存重置。
快照规则:
typeServer/getSnapshot返回当前快照标识符。- 快照标识符在一次会话期间不得减小。
typeServer/snapshotChanged报告先前的和新的快照标识符。- 包含过期快照的请求应失败,并返回一个 JSON-RPC 错误,使客户端能够用新快照重试。LSP 的
ServerCancelled错误码(-32802)是这种情况下的首选错误。
位置与范围
TSP 遵循 LSP 的位置约定:
- 行号从零开始。
- 字符偏移量以 UTF-16 代码单元为单位,从零开始。
- 范围使用起始位置和结束位置来标识源代码跨度。
如果类型服务器以其他编码存储源代码位置,则必须在协议边界处转换位置。错误的转换可能导致客户端请求错误的语法节点信息。
节点、声明与句柄
The type-query requests (typeServer/getComputedType, typeServer/getDeclaredType, typeServer/getExpectedType) identify a source construct with an arg that is either a node or a declaration. A node is a URI plus a source range. A declaration describes where and how a symbol is defined and is either a regular declaration, which is backed by a source AST node, or a synthesized declaration, which is created by the type checker and has no source node.
InvalidHandle 是 -1,表示无效或不可用的句柄。客户端和服务器应将其视为哨兵值,而不是有效的对象标识符。
类型结果
类型结果是可 JSON 序列化的带标签对象。每个类型都包含一个判别器,告诉客户端如何解释对象的其余部分。协议支持内置类型、声明类型、函数、类、联合类型、模块、类型变量、重载集、合成类型和类型引用。
每个类型对象都带有一个数字 kind 判别器、一个数字 id 以及一个 flags 位域:
kind |
类型 | 描述 |
|---|---|---|
0 |
BuiltIn |
unknown、any、unbound、ellipsis、never 及类似项 |
1 |
Declared |
源代码声明类型的基类(很少直接使用) |
2 |
Function |
来自 def 语句的函数和方法 |
3 |
Class |
类及其实例 |
4 |
Union |
联合类型(int | str | None) |
5 |
Module |
模块对象(import os 为 os 生成 Module 类型) |
6 |
TypeVar |
类型变量(T、P、Ts) |
7 |
Overloaded |
具有多个 @overload 签名的函数 |
8 |
Synthesized |
由类型检查器合成的类型 |
9 |
TypeReference |
通过 id 引用另一个类型,用于去重和循环 |
类型对象与快照绑定,类型 ID 是响应局部的:
- 返回的类型对象仅对产生它的请求所使用的快照有效。
- 类型 ID 用于在同一序列化响应图中将
TypeReference对象与先前的类型对象连接起来。 - 类型 ID 不是快照的稳定全局标识,不应在单独的响应之间进行比较。
- 应使用类型引用来避免在单个响应中重复大型或递归类型图。
协议接口
当前协议有意保持精简。确切的参数和结果形状在协议工件中定义;下表列出了每条 TSP 消息及其方向和载荷形状。
| 消息 | 方向 | 参数 | 结果 |
|---|---|---|---|
typeServer/getSupportedProtocolVersion |
客户端 → 服务器 | 无 | string(semver) |
typeServer/getSnapshot |
客户端 → 服务器 | 无 | number |
typeServer/snapshotChanged |
服务器 → 客户端 | { old: number, new: number } |
通知 |
typeServer/connection |
客户端 → 服务器 | { type: "open" | "close", kind, args? } |
{ success, message? } |
typeServer/getPythonSearchPaths |
客户端 → 服务器 | { fromUri, snapshot } |
string[] | undefined |
typeServer/resolveImport |
客户端 → 服务器 | { sourceUri, moduleDescriptor, snapshot } |
string | undefined |
typeServer/getComputedType |
客户端 → 服务器 | { arg: Declaration | Node, snapshot } |
Type | undefined |
typeServer/getDeclaredType |
客户端 → 服务器 | { arg: Declaration | Node, snapshot } |
Type | undefined |
typeServer/getExpectedType |
客户端 → 服务器 | { arg: Declaration | Node, snapshot } |
Type | undefined |
类型服务器应实现其所集成的客户端所要求的消息,并且仅在协议的结果类型允许时才返回 undefined。
类型查询
这三个类型查询请求的区别在于它们针对同一节点或声明所报告的类型不同:
typeServer/getComputedType返回根据代码流推断出的类型。将a收窄为int后,b = a + 1中b的计算类型为int。typeServer/getDeclaredType返回源代码中显式写出的类型。对于def foo(a: int | str),a的声明类型为int | str。typeServer/getExpectedType返回周围上下文所期望的类型。对于调用foo(4),参数的期望类型为int | str。
错误处理
使用标准 JSON-RPC 错误响应。
| 情况 | 错误代码 | 指导 |
|---|---|---|
| 过期快照 | -32802 (ServerCancelled) |
客户端可以请求新的快照并重试。 |
| 无效参数 | -32602 (InvalidParams) |
必填字段缺失或格式错误。 |
| 内部故障 | -32603 (InternalError) |
服务器在处理有效请求时意外失败。 |
错误应包含足够的消息细节,以便客户端日志能够识别失败的请求,但不应暴露本地机密或仅实现内部的状态。
兼容性检查清单
兼容的类型服务器应验证以下行为:
typeServer/getSupportedProtocolVersion返回客户端支持的 semver 字符串。- 所有请求和通知负载均按照协议工件进行序列化和反序列化。
typeServer/getSnapshot返回非负快照,且快照不会回退。- 当文档、配置和监视文件的更改可能改变返回的分析数据时,这些更改会使快照失效。
- 带有过期快照的请求会以可重试的取消错误失败。
- 位置和范围转换使用 LSP 的从零开始的 UTF-16 约定。
- 导入解析请求根据协议结果类型返回文档 URI 或无结果。
- 类型查询(计算、声明和期望)针对客户端提供的快照返回格式良好的
Type对象,或undefined。 - 仅当服务器能够提供协商的额外传输时,才声明支持多连接。
- 额外连接接受协商的 TSP 版本所允许的所有 TSP 请求,并拒绝 LSP 流量、状态变更通知、
typeServer/connection以及该版本不允许的 TSP 请求。
最后内容更新与 Pylance 2026.4.1 保持一致。
来源:Hacker News · github.com