monty-typeshed:为 Monty 沙箱定制的精简 typeshed 类型存根裁剪方案
2026/9/16 10:58:55 网站建设 项目流程

monty-typeshed:为 Monty 沙箱定制的精简 typeshed 类型存根裁剪方案

【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/monty

导读

Monty(GitHub 推荐项目精选 / monty3 / monty 仓库中的核心项目)是一个用 Rust 编写的极简、安全的 Python 解释器,面向 AI 场景使用。为了让沙箱内的 Python 代码在运行前就能获得类型检查能力,Monty 需要一个描述自身运行时 API 面、而非 CPython 完整标准库的类型存根(stub)集合——这就是monty-typeshedcrate 的职责。阅读本文后,你将掌握:为什么 Monty 需要一份"裁剪过的 typeshed"、这些存根如何在构建期被打包进二进制、custom/手写存根如何覆盖上游差异,以及如何通过make update-typeshed安全地升级这份存根树而不破坏沙箱的类型检查一致性。

背景:为什么一个最小解释器需要自己的 typeshed

Monty 只实现了 Python 标准库的一个刻意缩小的子集。这意味着它的类型检查器(monty-type-checkingcrate,底层由 ty 驱动)需要的存根必须描述Monty 自己的运行时表面,而不是 CPython 的表面:

  • 代码使用了 Monty不支持的内置函数或标准库模块,应当在类型检查阶段直接失败
  • 而不是让检查通过、等到运行时才报错。

这正是monty-typeshed的核心理念:只 vendoring(内置托管)Monty 实际支持的存根,把不支持的函数和类过滤掉,从而把"类型检查通过但运行失败"这一类问题提前消灭在静态分析阶段。

从来源上看,该 crate 最初派生自 ruff 的ty_vendoredcrate(见 crates/monty-typeshed/README.md),并在其基础上针对 Monty 的 API 面做了裁剪与定制。

目录布局:四条各司其职的路径

monty-typeshedcrate 的整体结构如下:

crates/monty-typeshed/ ├── custom/ # 手写存根,覆盖/补充上游差异 │ ├── README.md │ ├── asyncio.pyi │ ├── base64.pyi │ ├── binascii.pyi │ ├── functools.pyi │ ├── itertools.pyi │ ├── os.pyi │ ├── sys.pyi │ ├── unicodedata.pyi │ └── collections/__init__.pyi ├── vendor/typeshed/ # 裁剪后的上游存根(构建期产物) │ ├── source_commit.txt # 记录上游 commit,暴露为 SOURCE_COMMIT │ └── stdlib/ # 过滤后的 stdlib 存根 + VERSIONS ├── update.py # 更新脚本:克隆、过滤、覆盖 ├── check.py # 同步校验脚本 ├── build.rs # 构建期把存根打包为 zip 嵌入二进制 ├── src/lib.rs # crate 的唯一 API 入口 └── Cargo.toml

各部分职责如下:

  • vendor/typeshed/—— 来自上游 typeshed 的 vendored 存根,其来源 commit 记录在 crates/monty-typeshed/vendor/typeshed/source_commit.txt 中,并通过 src/lib.rs 暴露为常量SOURCE_COMMIT这些文件禁止手工编辑——它们会被更新脚本整体覆盖。
  • custom/—— 手写存根,用于覆盖或补充上游文件,处理 Monty 的运行时表面与 CPython有意不同的模块(如asyncioossysunicodedata)。更新脚本会把它们复制进vendor/typeshed/stdlib
  • update.py—— 克隆上游 typeshed,按白名单过滤出 Monty 支持的 builtins 与模块(白名单与crates/monty/src/builtins/镜像对应),再套用custom/覆盖。在仓库根目录运行make update-typeshed即可执行。
  • build.rs—— 构建期把 vendored 存根打包进编译产物,使 Monty 在磁盘上零存根文件的情况下也能提供类型检查。

唯一公开 API:file_system()

该 crate 对外只暴露一个入口点:file_system()。它返回一个静态的ruff_dbVendoredFileSystem(基于 zip 中的存根构建),monty-type-checking将其用作标准库模块解析的搜索路径。用法如 crates/monty-typeshed/README.md 所示:

let typeshed = monty_typeshed::file_system(); assert!(typeshed.exists("stdlib/builtins.pyi"));

从源码看,src/lib.rs 的实现非常精简:SOURCE_COMMIT通过include_str!直接内联source_commit.txt的内容;zip 字节则通过include_bytes!(concat!(env!("OUT_DIR"), "/zipped_typeshed.zip"))在编译期嵌入;file_system()内部用LazyLock保证VendoredFileSystem只初始化一次。

在消费端,monty-type-checking 的 db.rs 中MemoryDb::default()会调用monty_typeshed::file_system().clone(),把它作为ProgramSettings::empty(&vendored)的 vendored 文件系统,再注册项目根目录并构建搜索路径。这意味着:类型检查器解析import osfrom typing import ...时,看到的完全是 monty-typeshed 裁剪后的世界——这正是"不支持的模块在类型检查阶段就报错"这一目标得以实现的机制基础。

该 crate 还自带了两个构建期/测试期验证(见 src/lib.rs 中的测试模块):

  • test_commit:断言SOURCE_COMMIT长度为 40(即合法的 SHA-1 提交号);
  • typeshed_zip_created_at_build_time:打开内嵌的 zip,确认stdlib/builtins.pyi存在且包含class int:
  • typeshed_versions_file_exists:确认 zip 内stdlib/VERSIONS存在且包含builtins:条目。

更新脚本 update.py:从上游克隆到白名单过滤

crates/monty-typeshed/update.py 是整套裁剪流水线的核心,它依次完成四件事:

  1. 克隆/更新上游 typeshed 仓库crates/monty-typeshed/typeshed-repo,并将其 checkout 到固定 commit0e16ea31d2e188fdc126cb31e7c4fcc6b5a8da96。脚本会先用git status --porcelain检查工作树是否干净——如果本地有未提交改动,会直接raise ValueError,防止本地改动"泄漏"进 vendored 存根。
  2. 过滤builtins.pyi:用 Python 的ast模块解析源码,按白名单保留支持的函数与类,ast.unparse后写回。
  3. 写入固定产物stdlib/builtins.pyistdlib/VERSIONSvendor/typeshed/source_commit.txt
  4. 复制COPY_FILES列表中的文件(无需过滤直接拷贝),再把custom/下的存根覆盖上去。

内置函数与类白名单

内置函数白名单(对应crates/monty/src/builtins/的实现):

ALLOWED_FUNCTIONS = { 'abs', 'all', 'any', 'bin', 'chr', 'divmod', 'hash', 'hex', 'id', 'isinstance', 'iter', 'len', 'max', 'min', 'next', 'oct', 'ord', 'pow', 'print', 'repr', 'round', 'sorted', 'sum', }

内置类白名单(对应crates/monty/src/types/exception_private.rs的实现),分为几组:

  • 核心类型objecttype
  • 基本类型boolintfloat
  • 字符串/字节类型strbytes
  • 容器类型listtupledictsetfrozensetrange
  • 迭代器类enumeratereversedzip
  • 切片slice
  • 供 pathlib.Path 使用的property
  • 异常层级(来自exception_private.rs):BaseExceptionExceptionSystemExitKeyboardInterruptArithmeticErrorOverflowErrorZeroDivisionErrorLookupErrorIndexErrorKeyErrorRuntimeErrorNotImplementedErrorRecursionErrorAttributeErrorAssertionErrorMemoryErrorNameErrorSyntaxErrorOSErrorTimeoutErrorTypeErrorValueErrorStopIteration

过滤算法(filter_statements/filter_if_block)的关键行为:

  • 顶层FunctionDef/AsyncFunctionDef:仅当名字在白名单中时保留;
  • 顶层ClassDef:名字以_开头或命中白名单时保留(下划线开头的私有类会被保留,因为它们通常是公开 API 的内部依赖);
  • If节点:递归过滤版本条件块(如if sys.version_info >= (3, 10):),若两个分支过滤后都为空则整个丢弃;
  • 其余节点(import、类型别名、赋值等)一律保留。

无需过滤直接拷贝的文件

COPY_FILES列出的是完整保留的上游存根,因为它们是类型系统运转的"基础设施":

COPY_FILES = [ 'typing.pyi', 'typing_extensions.pyi', '_collections_abc.pyi', 'abc.pyi', # @abstractmethod 装饰 Protocol 成员,缺失会让成员推断为 Unknown 'types.pyi', # 类型注解中使用 'dataclasses.pyi', # 支持 dataclass 'enum.pyi', # dataclasses 依赖 're.pyi', 'collections/__init__.pyi', 'collections/abc.pyi', '_typeshed/__init__.pyi', '_typeshed/_type_checker_internals.pyi', 'pathlib/__init__.pyi', 'pathlib/types.pyi', 'json/__init__.pyi', 'json/encoder.pyi', 'json/decoder.pyi', 'math.pyi', 'datetime.pyi', ]

脚本注释给出了两个值得注意的设计理由:abc.pyi之所以必须保留,是因为@abstractmethod装饰了存根中大量 Protocol 成员(如Iterator.__next__),缺少它这些成员会推断为Unknown,从而"静默吞掉"下游错误;_typeshed/_type_checker_internals.pyi之所以保留,是因为 ty 会从中合成 TypedDict 成员,缺少它 TypedDict 上的属性访问会错误地解析为Unknown而非unresolved-attribute(对应 issue #799)。

VERSIONS 文件:模块版本门控

脚本会写入一份裁剪后的VERSIONS文件(见 update.py 中的VERSIONS常量),它声明了每个模块对应的 Python 版本范围,例如:

_collections_abc: 3.3- _typeshed: 3.0- # not present at runtime, only for type checking abc: 3.0- # not importable at runtime, only for type checking asyncio: 3.4- base64: 3.0- builtins: 3.0- dataclasses: 3.7- datetime: 3.0- json: 3.0- math: 3.0- os: 3.0- pathlib: 3.4- pathlib.types: 3.14- re: 3.0- sys: 3.0- typing: 3.5- typing_extensions: 3.7- unicodedata: 3.0-

这份文件是模块解析的门控开关:类型检查器对"已列出但无存根"的模块报unresolved-import,对"有存根但未列出"的模块直接忽略。因此,只有 Monty 自身暴露的模块(即custom/中的模块)需要被列出,而仅作为其他存根内部依赖被 vendored 的上游存根(如enum,经由dataclasses被引用)则刻意不列出。

custom/:手写存根覆盖 Monty 的差异化 API

crates/monty-typeshed/custom/README.md 说明了这一目录的定位:当 Monty 中的类型与标准库不同时,这里存放自定义类型存根update.py会用rglob('*.pyi')递归遍历(而非glob,因为需要处理嵌套包如collections/__init__.pyi),把每个文件按相对路径复制到vendor/typeshed/stdlib下,覆盖或补充上游对应文件。

os.pyi为例,custom/os.pyi 只声明了 Monty 沙箱真正暴露的一小部分 API:environ、POSIX 路径常量(sepaltsepextsepcurdirpardirlinesepnamedevnull)、getenv(带两个@overload以支持默认值)、listdirstatmkdirmakedirsremoveunlinkrmdirrenamereplacefspath,以及stat_result类。注释特别说明:"沙箱路径模型在所有宿主上都是 POSIX 的"——这是 Monty 有意为之的简化。

custom/sys.pyi则定义了stdout/stderr(类型为TextIO | MaybeNone)、versionversion_info等,其中_version_info@type_check_only标注,因为它"运行时不可实例化"。custom/asyncio.pyi更是直接手写了一个最小化的_Future类(注释标明是_typeshed/stdlib/_asyncio.pyi的最小拷贝),并为gather提供了逐个参数展开的重载(overload),精确描述 Monty 的 asyncio 子集。

build.rs:把存根打进二进制,磁盘零文件

build.rs 在构建期把整个vendor/typeshed/目录递归打包成一个 zip(zipped_typeshed.zip,输出到OUT_DIR),随后由src/lib.rsinclude_bytes!在编译期嵌入二进制。这样 Monty 运行时不依赖任何磁盘文件即可完成 stdlib 模块解析。

几个值得注意的工程细节:

  • 压缩算法按 feature 选择:启用zstdfeature 时用CompressionMethod::Zstd,启用deflate时用Deflated,否则Stored。注释解释:WASM 构建下编译zstd-sys需要 clang,会大幅复杂化构建,因此 WASM 场景改用 deflate(见 crates/monty-typeshed/Cargo.toml 中的[features]定义);因为 build script 的 target arch 是宿主架构而非构建目标架构,这里不能用#[cfg(...)],只能读取TARGET环境变量。
  • 路径统一用/分隔:zip 内路径用to_slash()归一化,且输出文件名用format!而非Path::join拼接,保证跨平台在编译期都能正确定位 zip。
  • 运行时补丁:打包时若遇到stdlib/VERSIONS,会追加一行ty_extensions: 3.0-,使 ty 的扩展模块在类型检查中可用。

同步校验 check.py:防止"静默漂移"

crates/monty-typeshed/check.py 用于校验 vendored 存根树与update.py的产出是否一致,其背景问题很尖锐:build.rsvendor/typeshed/打进二进制,而没有任何机制自动重新生成它——编辑custom/*.pyiCOPY_FILESVERSIONSCOMMIT在重新运行update.py之前都不会生效。而漂移是"静默"的:过时存根未描述的成员会报unresolved-attribute,从VERSIONS缺失的模块会被当成不存在。

check.py通过导入update.py(脚本兄弟文件)复用其常量定义,从四个维度做校验:

  1. check_tree_contents:双向比较"树中实际文件集合"与"update.py 会写出的文件集合",既抓缺失(update 会写但树里没有)也抓陈旧(树里有但 update 不再写),防止改名/删除存根后旧副本继续躺在 zip 里;
  2. check_custom_stubs:每个custom/*.pyi必须与 zip 中的副本字节级一致;
  3. check_generated_filessource_commit.txt必须等于COMMIT常量 + 换行,stdlib/VERSIONS必须等于VERSIONS常量;
  4. check_versionsVERSIONS中列出的每个模块都必须能解析到 vendored 存根(.pyi__init__.pyi),且每个custom/*.pyi都必须在VERSIONS中列出,否则类型检查器会忽略它。

其中builtins.pyi的过滤需要上游克隆才能重新推导,因此只校验其存在性,内容不做离线重算。

实际操作:升级与校验命令

在仓库根目录下,Monty 提供两个 Makefile 目标(见 Makefile):

# 更新 vendored typeshed:克隆上游 → 过滤 → 应用 custom/ 覆盖 → 格式化 make update-typeshed # 校验 vendored typeshed 与 update.py 的产出是否同步 make check-typeshed

make update-typeshed等价于依次执行:

uv run crates/monty-typeshed/update.py # 核心更新流水线 uv run ruff format # 对产物做格式化 uv run ruff check --fix --fix-only --silent # 自动修复 lint

make check-typeshed则运行uv run crates/monty-typeshed/check.py;校验失败时会逐条列出问题并以状态码 1 退出,同时提示"runmake update-typeshedto regenerate the vendored tree"。

从 crates/monty-typeshed/vendor/typeshed/stdlib 的实际产物可以看到,裁剪后的 stdlib 存根树包含:builtins.pyi(过滤后的核心)、VERSIONStyping.pyityping_extensions.pyi_collections_abc.pyiabc.pyitypes.pyidataclasses.pyienum.pyire.pyimath.pyidatetime.pyi,以及collections/_typeshed/pathlib/json/等子目录和custom/覆盖来的asyncio.pyibase64.pyibinascii.pyifunctools.pyiitertools.pyios.pyisys.pyiunicodedata.pyi——与 README 与脚本声明完全吻合。

在 Monty crate 家族中的位置

monty-typeshed是 Monty 类型检查链路中的"数据层"。它与其余 crate 的分工如下:

  • crates/monty —— 核心解释器:Python 解析器、字节码 VM 与沙箱;
  • crates/monty-types —— 共享边界数据类型(值、异常、OS 调用、资源限制),宿主无需链接解释器即可使用;
  • crates/monty-fs —— 宿主侧文件系统挂载:把虚拟沙箱路径映射到真实宿主目录;
  • crates/monty-runtime ——monty可执行文件:REPL、文件运行器与子进程 worker 模式;
  • crates/monty-pool —— 崩溃隔离的montyworker 子进程弹性池;
  • crates/monty-proto —— 池父进程与 worker 之间的 protobuf 线协议;
  • crates/monty-type-checking —— 沙箱代码的类型检查,由 ty 驱动;依赖 monty-typeshed 提供 stdlib 搜索路径
  • crates/monty-typeshed—— 描述 Monty 所实现 stdlib 子集的裁剪存根(本文主题);
  • crates/monty-macros ——monty参数解析背后的过程宏。

小结

monty-typeshed用一套"上游克隆 → AST 白名单过滤 → custom 覆盖 → 构建期 zip 嵌入 → 同步校验"的流水线,为 Monty 的最小运行时提供了与之一一对应的类型表面。它的设计价值在于把"运行时才暴露的不支持"转化为"类型检查阶段就拒绝",同时借助SOURCE_COMMITcheck.py严格防止上游与定制内容之间的静默漂移。对需要深度集成 Monty 类型检查能力的开发者而言,理解这份裁剪存根的结构与更新机制,是正确使用、安全升级其类型检查能力的前提。

【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/monty

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询