IDLE Shell 忙碌时运行 "Run... Customized":gh-82183 修复解析与实战指南
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本指南围绕 CPython 仓库中 IDLE 模块的一项行为修复展开:当 Python Shell 正在执行代码时,用户在编辑器中使用 "Run... Customized"(并取消勾选 "Restart shell")触发运行,IDLE 不再强行重启 Shell,而是明确报告 Shell 正处于执行状态。读完本文,你将掌握该修复的完整背景、底层调用链、触发路径、源码与测试验证方法,以及在实际使用 IDLE 时的注意事项与规避策略。
该修复记录位于 Misc/NEWS.d/next/IDLE/2026-07-01-19-00-00.gh-issue-82183.rNsTrt.rst,本文所有源码与测试证据均来自当前仓库。
一、修复背景:一个被 "Run Module" 掩盖的边界场景
IDLE 编辑器窗口的 Run 菜单包含两条核心命令(定义见 Lib/idlelib/mainmenu.py):
- Run Module(
<<run-module>>):语法检查后,在 Shell 中执行当前模块,默认会重启 Shell 子进程; - Run... Customized(
<<run-custom>>):与 Run Module 相同,但弹出对话框允许自定义命令行参数(扩展sys.argv)并选择是否重启 Shell。
关键差异在“是否重启”:普通的 Run Module 无条件重启子进程(源码中run_args if customize else ([], True)的默认值即为True),而 Run... Customized 允许用户取消勾选 "Restart shell",把模块直接运行在当前 Shell 上下文中,保留已有命名空间与状态,这与命令行执行python -i file的交互效果类似(参见 Lib/idlelib/help.html 中 "Run… Customized" 的官方说明)。
修复前的隐患正是出在这个“不重启”的选项上:如果此时 Shell 正处于忙碌状态(正在执行一段耗时代码),旧的执行路径仍然会尝试继续运行模块,导致与正在进行的执行冲突,用户体验是 Shell 行为异常而非得到明确提示。
二、修复内容:从“照常重启”到“明确报错”
本次变更的核心逻辑位于 Lib/idlelib/runscript.py 的run_module_event()方法中:
if customize: title = f"Customize {self.editwin.short_title()} Run" run_args = CustomRun(self.shell.text, title, cli_args=self.cli_args).result if not run_args: # User cancelled. return 'break' self.cli_args, restart = run_args if customize else ([], True) interp = self.shell.interp if self.shell.executing and not restart: # Cannot run without restarting the busy shell (gh-82183). interp.display_executing_dialog() return 'break' if pyshell.use_subprocess and restart: interp.restart_subprocess( with_cwd=False, filename=filename)新增的守卫条件if self.shell.executing and not restart:准确描述了修复语义:
self.shell.executing为 True:Shell 正忙于执行代码;restart为 False:用户通过 Run... Customized 明确取消了 "Restart shell" 勾选。
两者同时成立时,代码不再继续执行interp.restart_subprocess(...)与后续的interp.runcommand(...)/interp.runcode(...),而是调用display_executing_dialog()弹出错误对话框并提前返回'break'。
值得注意的是,run_custom_event是run_module_event的薄封装:
def run_custom_event(self, event): return self.run_module_event(event, customize=True)因此两条命令共享同一执行函数,修复对两者都生效;普通 Run Module 因为restart恒为True,不受新守卫影响,行为保持不变。
三、底层原理:executing 状态与 display_executing_dialog
3.1executing标志的维护
executing是 Shell 窗口(PyShell,继承自OutputWindow)的一个实例属性,定义于 Lib/idlelib/pyshell.py:
reading = False executing = False canceled = False它由两个配套方法控制,定义在同文件 pyshell.py:
beginexecuting():调用resetoutput()清空输出区后置executing = True;endexecuting():置executing = False、清空canceled并重新显示提示符showprompt()。
ModifiedInterpreter.runcode()(pyshell.py)在真正执行用户代码前调用self.tkconsole.beginexecuting(),执行结束(无论成功还是异常)在finally中恢复。因此只要 Shell 内正在跑耗时任务,executing就持续为True。
3.2 错误对话框的实现
新增守卫调用的display_executing_dialog()定义于 pyshell.py:
def display_executing_dialog(self): messagebox.showerror( "Already executing", "The Python Shell window is already executing a command; " "please wait until it is finished.", parent=self.tkconsole.text)它弹出一个标题为 "Already executing" 的模态错误框,提示用户“Shell 正在执行命令,请等待其完成”。该对话框本身并非新方法——它此前已被ModifiedInterpreter.runcommand()用于 Shell 内部冲突场景(pyshell.py),本次修复是将其复用到 Run... Customized 的调用路径上,实现了错误提示的一致性。
3.3 单线程架构下的必然冲突
IDLE 的 GUI 主循环与代码执行共享同一线程(tkinter 单线程模型)。当 Shell 处于executing状态时,再尝试向同一解释器提交新代码,必然与正在执行的代码在sys.argv、工作目录、sys.modules、__main__命名空间等全局状态上产生竞争。因此“不重启而强行运行”在架构上就是不安全的,这也解释了为何修复选择“报告错误”而非“排队执行”。
四、触发路径全链路:从菜单到守卫
一次完整的触发链路如下(以 Run... Customized 为例):
- 菜单绑定:mainmenu.py 定义
('Run... _Customized', '<<run-custom>>'); - 按键绑定:事件
<<run-custom>>在 Lib/idlelib/editor.py 绑定到scriptbinding.run_custom_event;默认快捷键Shift+F5,定义于 Lib/idlelib/config-keys.def(Windows 下为run-custom= <Shift-Key-F5>,config.py 注册该默认绑定); - 弹窗收集参数:
run_custom_event→run_module_event(customize=True)→ 弹出CustomRun对话框(Lib/idlelib/query.py)。该对话框提供两处输入:Command Line Arguments(用shlex.split按 POSIX 规则解析为sys.argv扩展列表)和 "Restart shell" 复选框(BooleanVar,默认勾选True); - 语法预检:
checksyntax()与tabnanny()分别做语法检查和缩进一致性检查(详见 runscript.py 与 runscript.py); - 守卫判断:
self.shell.executing and not restart成立 →interp.display_executing_dialog()弹框并return 'break',整个运行流程中止; - 正常路径:若 Shell 空闲,则按
restart决定是否interp.restart_subprocess(with_cwd=False, filename=filename)(pyshell.py 会关闭旧子进程、spawn_subprocess()新建、rpcclt.accept()接受连接并清空 Shell 输出),随后设置__file__、sys.argv、os.chdir()到模块目录、prepend_syspath(filename)加入搜索路径,最后interp.runcode(code)真正执行。
五、测试验证:为修复写下的回归测试
该修复配套了专门的回归测试,位于 Lib/idlelib/idle_test/test_runscript.py:
def test_run_module_event_shell_busy_no_restart(self): # gh-82183: running without restarting the busy shell aborts. ew = EditorWindow(root=self.root) sb = runscript.ScriptBinding(ew) sb.getfilename = mock.Mock(return_value='test.py') sb.checksyntax = mock.Mock(return_value='code') sb.tabnanny = mock.Mock(return_value=True) sb.shell = shell = mock.Mock() shell.executing = True interp = shell.interp with mock.patch.object(runscript, 'CustomRun') as customrun: # Restart shell unchecked. customrun.return_value.result = (['arg'], False) result = sb.run_module_event(None, customize=True) self.assertEqual(result, 'break') interp.display_executing_dialog.assert_called_once() interp.restart_subprocess.assert_not_called() interp.runcode.assert_not_called() ew._close()测试精确覆盖了修复语义:
- 构造繁忙 Shell:
shell.executing = True; - 模拟不重启:
CustomRun对话框返回(['arg'], False),即restart = False; - 断言行为:返回
'break'(事件被消费)、display_executing_dialog恰好被调用一次、且restart_subprocess与runcode均未被调用。
这三个断言保证了“报错而非运行”的修复方向:既不重启 Shell,也不向繁忙的解释器提交任何代码。该测试要求gui环境(@requires('gui'),见测试类 setUpClass),因此通常在带图形界面的桌面环境中运行,例如:
./python -m unittest -v idlelib.idle_test.test_runscript六、实际操作:如何复现与规避
6.1 复现步骤
- 在 IDLE 编辑器窗口打开一个脚本,例如包含
time.sleep(30)的模块; - 用Run Module(
F5)启动它,Shell 随即进入忙碌状态; - 在 Shell 执行完之前,回到编辑器,选择Run → Run... Customized(
Shift+F5); - 在弹出的对话框中取消勾选 "Restart shell",点击 Run;
- 修复后的行为:弹出 "Already executing" 错误框,提示 Shell 正在执行命令,模块不会被运行,Shell 原有执行不受干扰。
6.2 规避与正确做法
- 若确实希望立即运行新模块,保持"Restart shell" 勾选——
restart = True时守卫条件不成立,会走restart_subprocess强制重启子进程后运行; - 若希望保留当前 Shell 上下文(不重启),请等待当前代码执行完毕,Shell 重新出现
>>>提示符(endexecuting()已调用)后再运行; - Shell 窗口菜单栏的Shell → View Last Restart可以快速跳转到最近一次重启标记处,方便确认 Shell 状态(见 help.html)。
七、总结
gh-82183 修复的是一处真实存在的状态竞争:Run... Customized 的“不重启”选项遇上忙碌的 Shell。修复在 Lib/idlelib/runscript.py 增加了一行守卫,将“强行运行”转变为“明确报错”,复用已有的display_executing_dialog()提示,并通过 test_runscript.py 的回归测试锁定行为。这条变更体现了 IDLE 对 Shell 单线程执行模型的一致遵循:无论从哪条路径触发运行,都不允许在 Shell 忙碌时绕过状态检查向解释器注入新代码。
对于使用者,牢记一句话即可:Shell 忙碌且不打算重启时,Run... Customized 会礼貌地拒绝你——先等它忙完,或者干脆勾上 "Restart shell"。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考