post_execute fires during comm_msg handling on a subshell, so output published from the hook is parented to the comm_msg and dropped by frontends
还没有人认领这个 Issue。
评估
调研方向
Start with the embedded repro_parent_header.py, especially create_subshell() and the comm_msg path, then compare its ipykernel 7.3.0 output with the 6.31.0 run. Trace where pre_execute/post_execute are fired during subshell comm handling; done means comm handling no longer causes a hook to publish output parented to the comm_msg or drain the cell's pending output.
由索引模型根据 Issue 内容生成。
描述
Summary (AI created)
When comm messages are routed over subshells, ipykernel 7 fires IPython's pre_execute / post_execute events while handling a comm_msg — on the subshell thread, and under the comm_msg's parent header.
Any hook that publishes output from post_execute therefore publishes it with parent_header.msg_id pointing at the comm_msg instead of at the cell's execute_request. Frontends route iopub output to a cell by that id, so the output is silently discarded — no error, no placeholder, nothing.
It is worse than a misroute: the hook has now drained whatever it had queued, so when the cell's own post_execute fires a moment later there is nothing left to display.
This is reachable in a default install — JupyterLab 4.6's commsOverSubshells setting defaults to perCommTarget, so subshells are used whenever the kernel supports them.
Why it matters
I believe this is the cause of matplotlib/ipympl#609 ("figure does not show up in the output area unless plt.show() is explicitly called").
ipympl queues figures during a cell and displays them from post_execute (flush_figures). A figure that is already on screen keeps its comm busy, so the next figure created is the one that vanishes. That accounts for every reported characteristic of #609:
the first plot of a session always works — nothing is messaging a comm yet
it is intermittent, and more likely with slower cells or larger datasets — a longer cell is a wider window for a comm_msg to land mid-execution
it never happens with %matplotlib inline — no comms, so no subshell traffic
matplotlib_inline uses the same post_execute pattern and would be affected anywhere comms are in play.
Reproducer
No matplotlib, ipympl or ipywidgets required. It starts a kernel, registers a post_execute hook that displays a queued payload, opens a comm, creates a subshell, then runs a 3-second cell and sends one comm_msg on the subshell one second in.
Exit code 0 = correct, 1 = bug reproduced.
repro_parent_header.py
"""
ipykernel 7 fires IPython's pre_execute/post_execute events while handling a
comm_msg, on the subshell thread and under the comm_msg's parent header.
Anything that publishes output from a post_execute hook -- ipympl's
flush_figures, matplotlib_inline's -- therefore publishes it with
parent_header.msg_id pointing at the comm_msg rather than at the cell's
execute_request. A frontend routes iopub output to a cell by that id, so the
output is silently dropped. Worse, the hook has now drained its pending queue,
so the cell's own post_execute has nothing left to display.
This is, I believe, the cause of matplotlib/ipympl#609: an ipympl figure that is
already on screen keeps a comm busy, so the next figure is the one that
vanishes -- intermittently, more often with slower cells, and never with
%matplotlib inline (no comms).
No matplotlib / ipympl / ipywidgets required.
python repro_parent_header.py
Exit code 0 = correct behaviour, 1 = bug reproduced.
"""
from future import annotations
import queue
import sys
import time
import ipykernel
from jupyter_client.manager import start_new_kernel
SETUP = r'''
import time, threading
from IPython.display import display
from ipykernel.comm import Comm
T0 = time.time()
EVENTS = []
_pending = []
def _post_execute():
# This is what ipympl's flush_figures does: display whatever the cell queued.
EVENTS.append(("post_execute", round(time.time() - T0, 2), threading.current_thread().name))
while _pending:
display(_pending.pop())
get_ipython().events.register("post_execute", _post_execute)
_comm = Comm(target_name="repro")
_comm.on_msg(lambda msg: None)
print("COMM_ID", _comm.comm_id)
'''
CELL = r'''
_pending.append("PAYLOAD-PUBLISHED-FROM-POST-EXECUTE")
time.sleep(3) # window for a comm_msg to land mid-cell
'''
REPORT = "for e in EVENTS: print(*e)"
def drain(kc, stop_msg_id, log=None):
deadline = time.time() + 30
while time.time() < deadline:
try:
msg = kc.get_iopub_msg(timeout=0.5)
except queue.Empty:
continue
parent = msg.get("parent_header") or {}
item = (msg["msg_type"], parent.get("msg_type"), parent.get("msg_id"), msg["content"])
if log is not None:
log.append(item)
yield item
if (
msg["msg_type"] == "status"
and msg["content"]["execution_state"] == "idle"
and parent.get("msg_id") == stop_msg_id
):
return
def create_subshell(kc) -> str | None:
"""Ask for a subshell, as JupyterLab does for kernel comms. None on ipykernel 6."""
msg = kc.session.msg("create_subshell_request", {})
kc.control_channel.send(msg)
deadline = time.time() + 10
while time.time() < deadline:
try:
reply = kc.get_control_msg(timeout=2)
except queue.Empty:
return None
if reply["parent_header"].get("msg_id") == msg["header"]["msg_id"]:
return reply["content"].get("subshell_id")
return None
def main() -> int:
print(f"ipykernel {ipykernel.version}, python {sys.version.split()[0]}")
km, kc = start_new_kernel(kernel_name="python3")
try:
setup_id = kc.execute(SETUP)
comm_id = None
for mtype, _, _, content in drain(kc, setup_id):
if mtype == "stream" and content["text"].startswith("COMM_ID"):
comm_id = content["text"].split()[1]
assert comm_id, "no comm id"
subshell_id = create_subshell(kc)
print(f"subshell: {subshell_id or 'not supported (ipykernel 6)'}")
cell_id = kc.execute(CELL)
time.sleep(1.0) # let the cell get going
comm_msg = kc.session.msg(
"comm_msg", {"comm_id": comm_id, "data": {"method": "custom", "content": {}}}
)
if subshell_id:
comm_msg["header"]["subshell_id"] = subshell_id
kc.shell_channel.send(comm_msg)
comm_msg_id = comm_msg["header"]["msg_id"]
trace: list = []
display_parent = None
for mtype, ptype, pid, _ in drain(kc, cell_id, log=trace):
if mtype == "display_data":
display_parent = (ptype, pid)
print("\niopub during the cell")
for mtype, ptype, pid, content in trace:
who = "CELL" if pid == cell_id else "COMM" if pid == comm_msg_id else "-"
note = ""
if mtype == "status":
note = content["execution_state"]
elif mtype == "display_data":
note = str(list(content.get("data", {}).keys()))
print(f" {mtype:14s} parent={ptype or '-':22s} [{who}] {note}")
print("\nhook invocations (elapsed, thread)")
report_id = kc.execute(REPORT)
for mtype, _, _, content in drain(kc, report_id):
if mtype == "stream":
for line in content["text"].splitlines():
print(f" {line}")
print(f"\n execute_request {cell_id}")
print(f" comm_msg {comm_msg_id}")
print(f" display_data parent {display_parent}\n")
if display_parent is None:
print("INCONCLUSIVE: no display_data observed")
return 2
if display_parent[1] == cell_id:
print("OK display_data is parented to the cell's execute_request")
return 0
print("BUG display_data is parented to the comm_msg, not to the cell.")
print(" A frontend has no cell for that parent, so the output is dropped.")
return 1
finally:
kc.stop_channels()
km.shutdown_kernel()
if name == "main":
raise SystemExit(main())
Question
Is firing pre_execute / post_execute for comm message handling intended?
A comm message is not a cell execution, and hooks registered on those events have no way to tell the two apart. If the events must fire, it would help to either scope them (a distinct event for comm handling) or give handlers a way to detect that they are running outside an execute_request, so libraries like ipympl can defer their flush rather than publishing into a void.
Environment
Windows 11, Python 3.10.11
ipykernel 7.3.0 (bug) vs 6.31.0 / 6.30.1 (correct), all else unchanged
observed in the wild with JupyterLab 4.6.3, ipympl 0.10.0, ipywidgets 8.1.7, matplotlib 3.9.4, commsOverSubshells left at its default perCommTarget
- 主要语言
- Python
- 星标
- 734
- 派生
- 411
- 平均合并
- 1 天 2 小时
- 30 天内合并 PR
- 9
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
ipython/ipykernel 的其他 Issue
-
难度 1/5 1 小时以内 新手友好度 72/100
-
难度 4/5 3-5 天 新手友好度 68/100
-
难度 3/5 1-2 天 新手友好度 66/100
-
难度 4/5 3-5 天 新手友好度 35/100
查看 ipython/ipykernel 的全部 Issue
相似的 Issue
-
sponsored
难度 2/5 1-3 小时 新手友好度 65/100
-
难度 2/5 1-3 小时 新手友好度 86/100
Diaoul/subliminal#1382 ·
-
难度 1/5 1 小时以内 新手友好度 92/100
-
triage/confirmed
难度 2/5 1-3 小时 新手友好度 88/100
agentscope-ai/agentscope#2775 ·
-
难度 2/5 1-3 小时 新手友好度 84/100