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
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
調査の方向性
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時間
- マージ済み PR(30日)
- 9
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
ipython/ipykernel のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
-
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
-
難易度 4/5 3〜5日 初心者へのやさしさ 68/100
-
難易度 3/5 1〜2日 初心者へのやさしさ 66/100
ipython/ipykernel の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
anthropics/skills#1811 · コメント 1 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
speaches-ai/speaches#678 ·
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
datalayer/mcp-compose#42 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
conda-forge/spacy-feedstock#177 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
UKGovernmentBEIS/inspect_evals#2523 ·