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
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 48/100
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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
- Lenguaje dominante
- Python
- Estrellas
- 734
- Forks
- 411
- Merge medio
- 1 d 2 h
- PR fusionados (30 d)
- 9
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de ipython/ipykernel
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 72/100
-
Dificultad 4/5 3-5 días Aptitud para principiantes 68/100
-
ipython/ipykernel#1550 · 1 comentario · 1 reacción · 1 asignado ·
-
Dificultad 3/5 1-2 días Aptitud para principiantes 66/100
Todos los issues de ipython/ipykernel
Issues similares
-
essnmx good first issue
Dificultad 1/5 Menos de una hora Aptitud para principiantes 95/100
-
[Feature] 奇物选择添加优先级 Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
syfoud/Simulated_Scepter#174 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Giskard-AI/giskard-oss#2840 · 1 comentario ·
-
A claim comment carrying the issue number is silently declined while the workflow reports success Abiertoarea: repo bug perceived difficulty: 2
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
yeti-platform/yeti#1380 ·