Skip to content

Commit 502af73

Browse files
fix(mpl): contain comm callback errors in mpl_interactive (#9532)
## Summary A raising callback inside `mpl_interactive._handle_comm_msg` (e.g. matplotlib's `AttributeError: 'Axes' object has no attribute '_pan_start'` from `end_pan` without a prior `start_pan`) was propagating up through `MarimoComm.handle_msg` → `WIDGET_COMM_MANAGER.receive_comm_message` → the kernel's request handler, terminating `asyncio.run` and killing the kernel subprocess. The uvicorn parent kept serving so the UI just reported "stopped responding". This wraps the `handle_json` and `_handle_download` calls in `try/except Exception` and logs via `LOGGER.exception` so a buggy callback is contained while the traceback still surfaces for diagnosis. `BaseException` is intentionally not caught. Fixes the kernel-crash half of the matplotlib pan/zoom issue (see linked Linear issue). The underlying matplotlib `_pan_start` bug is tracked separately. ## Test plan - [x] Manual repro via the linked Linear repro (pan → re-run cell → zoom → pan): exception logged, kernel stays alive, figure remains responsive. - [x] Direct programmatic injection: `_handle_comm_msg` with a raising `handle_json` returns cleanly and a follow-up message is processed. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent fb38a25 commit 502af73

1 file changed

Lines changed: 26 additions & 14 deletions

File tree

marimo/_plugins/ui/_impl/from_mpl_interactive.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -299,23 +299,35 @@ def _handle_comm_msg(self, msg: dict[str, Any]) -> None:
299299
self._handle_download(fmt)
300300
return
301301
else:
302-
# Forward to figure manager (mouse events, toolbar actions, etc.)
303-
self._figure_manager.handle_json(event) # type: ignore[no-untyped-call]
302+
# Forward to figure manager (mouse events, toolbar actions, etc.).
303+
# Swallow callback exceptions so a buggy handler can't kill the
304+
# kernel subprocess; the exception propagates up through the comm
305+
# manager and terminates asyncio.run otherwise.
306+
try:
307+
self._figure_manager.handle_json(event) # type: ignore[no-untyped-call]
308+
except Exception:
309+
LOGGER.exception(
310+
"mpl_interactive handle_json failed for event type %s",
311+
msg_type,
312+
)
304313

305314
def _handle_download(self, fmt: str) -> None:
306315
"""Render figure to the requested format and send back via comm."""
307-
buf = io.BytesIO()
308-
self._figure_manager.canvas.figure.savefig(
309-
buf, format=fmt, bbox_inches="tight"
310-
)
311-
blob = buf.getvalue()
312-
self._comm.send(
313-
data={
314-
"method": "custom",
315-
"content": {"type": "download", "format": fmt},
316-
},
317-
buffers=[blob],
318-
)
316+
try:
317+
buf = io.BytesIO()
318+
self._figure_manager.canvas.figure.savefig(
319+
buf, format=fmt, bbox_inches="tight"
320+
)
321+
blob = buf.getvalue()
322+
self._comm.send(
323+
data={
324+
"method": "custom",
325+
"content": {"type": "download", "format": fmt},
326+
},
327+
buffers=[blob],
328+
)
329+
except Exception:
330+
LOGGER.exception("mpl_interactive download failed (fmt=%s)", fmt)
319331

320332
def _convert_value(
321333
self, value: ModelIdRef | dict[str, Any]

0 commit comments

Comments
 (0)