ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CPython 修复 site.addsitedir() 重入崩溃:gh-149504 深度解析与 .pth/.start 启动机制

CPython 修复 site.addsitedir() 重入崩溃:gh-149504 深度解析与 .pth/.start 启动机制 CPython 修复 site.addsitedir() 重入崩溃gh-149504 深度解析与 .pth/.start 启动机制【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本篇技术指南以 CPython 仓库中的变更记录 Misc/NEWS.d/next/Library/2026-05-10-23-51-23.gh-issue-149504.pDSCbn.rst 为核心深入剖析site.addsitedir()在启动文件.pth的import行与.start入口点内部被重入调用时曾经触发RuntimeError: dictionary changed size during iteration崩溃的根因并结合 Lib/site.py 的实现与 Lib/test/test_site.py 中的回归测试完整还原修复方案、PEP 829 批处理模型以及重入安全不变量。读完本文你将理解 site 初始化为何会崩溃、StartupState如何化解该问题以及如何编写不会踩坑的.pth/.start启动配置。一、问题现象uv run --with在启动阶段崩溃1.1 变更记录原文本次变更记录gh-issue-149504的完整内容如下Fixsite.addsitedirto allow re-entrant calls from within startup files. Previously, a.pthfile containing animportline that calledsite.addsitedir(or a.startentry point doing the same) could crash withRuntimeError: dictionary changed size during iterationduring site initialization, breaking tools such asuv run --with.翻译成白话修复site.addsitedir使其允许在启动文件内部被重入调用。此前一个包含import行的.pth文件或做同样事情的.start入口点在调用site.addsitedir时会在 site 初始化期间以RuntimeError: dictionary changed size during iteration崩溃破坏了诸如uv run --with这样的工具。1.2 崩溃的直观场景该崩溃发生在解释器启动的极早期阶段具体触发链如下Python 解释器启动时自动导入site模块除非使用-S或隔离模式site.main()开始处理各 site-packages 目录某个 site-packages 目录下的.pth文件包含一行import site; site.addsitedir(...)或某个.start入口点内部调用了site.addsitedir(...)这个嵌套调用试图修改正在被外层逻辑迭代的模块级“待处理启动数据”历史实现中为模块级字典/集合Python 的字典在迭代期间被修改尺寸时抛出RuntimeError: dictionary changed size during iteration解释器启动直接失败。uv run --with这类工具之所以受影响是因为它依赖.pth文件在启动阶段动态地把额外的包目录追加到sys.path当被追加的目录里恰好还有另一个.pth/.start文件需要再次调用site.addsitedir时就触发了上述重入路径。二、背景机制site 初始化与 PEP 829 启动文件要理解这个 bug必须先弄清site模块在解释器启动时做了什么。该模块的文档字符串见 Lib/site.py说明了两种在 site-packages 目录中被处理的配置文件name.pth文件逐行向sys.path追加目录以import开头的行已按 PEP 829 被弃用Python 3.18/3.19 中静默忽略3.20 起发出警告name.start文件以pkg.mod:callable语法声明启动入口点通过pkgutil.resolve_name()解析并以无参数方式调用。当从main()调用时所有.pth路径扩展会先于任何.start入口点执行确保路径在启动代码运行前就绪见 Lib/site.py。site.main()的完整流程Lib/site.py为removeduppaths()去重并将sys.path全部转成绝对路径创建StartupState依次处理 venv 配置_venv、用户 siteaddusersitepackages与全局 site-packagesaddsitepackages一次性调用state.process()按 PEP 829 顺序刷出全部数据——先扩展sys.path再执行被弃用的import行最后执行.start入口点安装quit/copyright/help等内置对象并执行sitecustomize/usercustomize。换句话说启动期所有 site 数据的收集与“落地”被分成了两个阶段先批量收集再统一处理。这就是 PEP 829 的“批处理batch”模型。三、根因分析共享可变状态与迭代期修改3.1 旧实现的问题所在在修复之前site模块使用模块级共享的可变状态来暂存启动数据pending startup state。当site.addsitedir()被调用时它读取目录下的.pth/.start文件并把结果累积进这些模块级结构当main()结束时再统一刷出。问题在于.pth的import行与.start入口点在“刷出”阶段才被执行例如exec(line)执行 import 行见 Lib/site.py。如果这些被执行的代码里又调用了site.addsitedir()就会在“刷出迭代进行中”去修改正在被迭代的同一批字典/集合外层正在for filename, imports in self._importexecs.items():之类的循环中遍历内层site.addsitedir()向同一字典插入新键字典容量变化 →RuntimeError: dictionary changed size during iteration。由于此前.pth的import行与.start入口点的执行都复用同一组模块级“待处理”结构无论重入来自哪一条路径都可能踩中这个迭代陷阱。3.2 重入为什么是合法的需求乍看之下“启动文件内部再调用site.addsitedir()”似乎只是极端用法但它其实有真实场景例如某个.pth文件想按条件把另一个独立目录及其.pth/.start也纳入处理最自然的写法就是import site; site.addsitedir(extra_dir)。在修复前这种写法在解释器启动时是直接崩溃的因此工具的变通方案要么绕开.pth要么在运行时site初始化完成后再手动处理——uv run --with即属于受此限制影响的一类工具。四、修复方案StartupState的独立状态与重入安全不变量4.1 核心思路每次调用独立状态修复的核心是把“模块级共享待处理状态”改为“每次调用独立的StartupState实例”。在 Lib/site.py 的注释中明确写明了这一设计关键的CRITICAL重入不变量从.start入口点或被exec执行的.pthimport行中递归到达的site.addsitedir()调用不得修改当前正在被处理的那个StartupState。重入调用会到达模块级的site.addsitedir()垫片shim它总是构建一个全新的、按调用隔离的状态。StartupState的注释进一步解释Lib/site.py状态完全保存在实例上不存在模块级待处理状态因此被递归到达的site.addsitedir()调用操作的是与外层调用不同的StartupState重入天然安全。4.2 两条处理路径的分流从 Lib/site.py 的模块级addsitedir()可以看到两种模式def addsitedir(sitedir, known_pathsNone): _trace(fAdding directory: {sitedir!r}) if known_paths is None: state StartupState(_init_pathinfo()) state.addsitedir(sitedir) else: # 保持 legacy known_paths 模式的幂等性gh-75723 # 已在 known_paths 中的 sitedir 会被跳过而不是重复处理。 state StartupState(known_paths) state._addsitedir(sitedir, process_known_sitedirsFalse) state.process() return known_paths独立调用隐式批处理known_paths is None时site.addsitedir()每次创建一个全新的StartupState读取并处理完即丢弃——这正是重入调用所走的路径任何时刻只有一个“活”状态不存在迭代期修改显式批处理调用方可以自行创建StartupState多次调用其addsitedir()/addusersitepackages()/addsitepackages()累积数据最后调用一次process()统一刷出——这正是site.main()与venv()的用法见 Lib/site.py。StartupState的__slots__Lib/site.py展示了其私有数据面槽位用途_known_paths已出现在sys.path上的路径集合大小写归一化用于去重_processed_sitedirs本次批处理中已读取过启动文件的 sitedir 集合_path_entriessys.path扩展台账(pthfile, path)二元组列表保留.pth路径与所在 sitedir 的交错语义_importexecs文件名 → 被弃用的import行列表_entrypoints文件名 →.start入口点字符串列表4.3 数据落地顺序PEP 829StartupState.process()Lib/site.py严格按 PEP 829 的阶段顺序刷出def process(self): 按 PEP 829 顺序应用累积状态。 self._extend_syspath() self._exec_imports() self._execute_start_entrypoints()_extend_syspath()先把所有.pth路径扩展与 sitedir 追加到sys.path缺失路径打印错误并跳过见 Lib/site.py_exec_imports()再执行被弃用的import行除非存在同名.start文件将其压制见 Lib/site.py_execute_start_entrypoints()最后解析并调用.start入口点通过pkgutil.resolve_name(entrypoint, strictTrue)见 Lib/site.py。这样保证任何入口点所依赖的模块路径都已在sys.path上可见。而在执行第 2、3 步的过程中若发生重入调用模块级垫片会创建全新的StartupState其刷出过程完全独立不会反向触碰外层正在迭代的状态——崩溃由此消除。五、回归测试如何验证重入不再崩溃本次修复在 Lib/test/test_site.py 中新增了两个针对性回归测试均标注gh-149504。5.1 从 .pth 的 import 行重入test_reentrant_addsitedir_pthLib/test/test_site.py 复现了“import 行内部重入”场景def test_reentrant_addsitedir_pth(self): # 一个 .pth 文件中的 import 行调用 site.addsitedir() # 在外层调用仍在处理待处理启动状态时不得崩溃或重复执行外层条目。 overlay self.enterContext(os_helper.temp_dir()) overlay_pth os.path.join(overlay, overlay.pth) pkgdir self.enterContext(os_helper.temp_dir()) with open(overlay_pth, w, encodingutf-8) as fp: print(pkgdir, filefp) self._make_pth(fimport site; site.addsitedir({overlay!r})\n) site.addsitedir(self.sitedir, set()) self.assertIn(overlay, sys.path) self.assertIn(pkgdir, sys.path)测试要点外层 sitedir 的.pth文件内容为import site; site.addsitedir(overlay)内层 overlay 目录也有自己的overlay.pth指向pkgdir修复前该场景会因外层_importexecs字典在迭代中被修改而崩溃修复后两个目录overlay与pkgdir都正确进入sys.path。5.2 从 .start 入口点重入test_reentrant_addsitedir_startLib/test/test_site.py 复现了“入口点内部重入”场景def test_reentrant_addsitedir_start(self): # 同上但重入来自 .start 入口点而非 .pth import 行。 # 入口点执行阶段同样容易受这类 bug 影响。 overlay self.enterContext(os_helper.temp_dir()) overlay_pth os.path.join(overlay, overlay.pth) pkgdir self.enterContext(os_helper.temp_dir()) with open(overlay_pth, w, encodingutf-8) as fp: print(pkgdir, filefp) self._make_mod(\ import site def bootstrap(): site.addsitedir(%r) % (overlay,), namereenter_helper, on_pathTrue) self._make_start(reenter_helper:bootstrap\n) site.addsitedir(self.sitedir, set()) self.assertIn(overlay, sys.path) self.assertIn(pkgdir, sys.path)测试要点.start文件声明入口点reenter_helper:bootstrap该入口点内部调用site.addsitedir(overlay)overlay 的.pth又追加pkgdir验证重入路径同样安全最终sys.path同时包含overlay与pkgdir。两个测试共同覆盖了 PEP 829 中“会执行用户代码”的两个阶段确保无论重入来自.pth的import行还是.start入口点StartupState的独立状态模型都能兜住。六、相关修复与设计上下文本次修复并非孤立变更它与site模块的几处既有语义紧密相关幂等性gh-75723同一 sitedir 使用known_paths重复调用addsitedir()时不得重复处理其.pth/.start文件。StartupState._record_sitedir()通过独立的_processed_sitedirs集合区分“已在 sys.path 上”与“启动文件已读过”见 Lib/site.py。对应测试见 Lib/test/test_site.py。sitedir 已在 sys.path 上仍需处理 .pthgh-149819子进程继承PYTHONPATH时site-packages 目录可能早已在sys.path上但其.pth文件仍必须被处理。main()现在以StartupState(set())启动批处理避免旧实现中known_paths预填充导致.pth被跳过的缺陷测试见 Lib/test/test_site.py。.start对.pthimport 行的压制PEP 829存在同名.start文件时.pth中的import行被抑制见 Lib/site.py相关集成测试覆盖.pth/.start混用、字母序、去重等行为Lib/test/test_site.py。七、实践指引如何安全地使用启动期重入结合本次修复编写启动文件时有以下可操作建议优先使用显式批处理如果业务代码需要把多个 site 目录一次性纳入推荐显式创建StartupState并批量addsitedir()后统一process()与site.main()保持一致的语义import site state site.StartupState() state.addsitedir(/opt/extra/packages) state.addsitepackages([/opt/my/prefix]) state.process()重入场景交给模块级垫片当.pth的import行或.start入口点内部必须再调用addsitedir()时直接使用模块级site.addsitedir(dir)——它会创建全新的按调用隔离状态是本次修复后唯一被保证安全的入口避免在启动代码中手动修改sys.path.pth/.start的合法工作方式是声明路径与入口点让StartupState.process()统一落地手写sys.path.insert(...)绕过批处理流程会失去顺序保证路径必须先于入口点可见注意弃用节奏.pth中的import行在 Python 3.18/3.19 被静默忽略、3.20 起发警告新代码应改用.start入口点见 Lib/site.py 的警告逻辑。八、小结gh-issue-149504修复揭示了一个容易被忽视的启动期陷阱当“收集启动数据”与“执行启动代码”混在同一个共享状态上时任何来自.pthimport行或.start入口点的重入调用都可能触发RuntimeError: dictionary changed size during iteration。修复方案通过StartupState将状态按调用实例化、并通过模块级addsitedir()垫片保证重入调用永远操作独立状态从根上消除了迭代期修改问题也让uv run --with这类依赖启动期动态扩展sys.path的工具得以正常工作。理解这一设计不仅有助于排查类似崩溃也能帮助你在编写.pth/.start启动文件时写出符合 PEP 829 语义、安全可重入的配置。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表