Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 3 additions & 28 deletions marimo/_messaging/tracebacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,8 @@ def write_traceback(traceback: str) -> None:
# In run mode, only forward to the frontend if show_tracebacks is on.
if in_run_mode and not _show_tracebacks_enabled():
return
# Strip marimo's internal executor.py frame and highlight for the UI
trimmed = _trim_traceback(traceback)
sys.stderr._write_with_mimetype(
_highlight_traceback(trimmed),
_highlight_traceback(traceback),
mimetype="application/vnd.marimo+traceback",
)
else:
Expand All @@ -64,16 +62,15 @@ def write_traceback(traceback: str) -> None:
if in_run_mode and not _show_tracebacks_enabled():
sys.stderr.write(traceback)
return
trimmed = _trim_traceback(traceback)
broadcast_notification(
CellNotification(
cell_id=ctx.cell_id,
console=CellOutput(
channel=CellChannel.STDERR,
mimetype="application/vnd.marimo+traceback",
data=trimmed
data=traceback
if code_mode
else _highlight_traceback(trimmed),
else _highlight_traceback(traceback),
),
),
ctx.stream,
Expand All @@ -83,27 +80,5 @@ def write_traceback(traceback: str) -> None:
sys.stderr.write(traceback)


def _trim_traceback(traceback: str) -> str:
"""
Skip first DefaultExecutor.execute_cell traceback item which all traces start with.
"""

lines = traceback.split("\n")
if (
len(lines) > 2
and lines[0] == "Traceback (most recent call last):"
and (
'/marimo/_runtime/executor.py", line ' in lines[1]
or '\\marimo\\_runtime\\executor.py", line ' in lines[1]
)
and lines[1].endswith(", in execute_cell")
):
for i in range(2, len(lines)):
if lines[i].startswith(" File "):
return "\n".join(lines[:1] + lines[i:])

return traceback


def is_code_highlighting(value: str) -> bool:
return 'class="codehilite"' in value
8 changes: 6 additions & 2 deletions marimo/_runtime/executor/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ def execute_cell(self, cell: CellImpl, glbls: dict[str, Any]) -> Any:
exec(cell.body, glbls)
return eval(cell.last_expr, glbls)
except BaseException as e:
# Raising from BaseException folds in the stack trace prior
# to execution; the Runner classifies via ``__cause__``.
# Strip our own frame so user-facing tracebacks start at user
# code. Runners classify via ``__cause__``.
if e.__traceback__ is not None:
e.__traceback__ = e.__traceback__.tb_next
raise MarimoRuntimeException from e
Comment thread
dmadisetti marked this conversation as resolved.

async def execute_cell_async(
Expand All @@ -59,4 +61,6 @@ async def execute_cell_async(
return await eval(cell.last_expr, glbls)
return eval(cell.last_expr, glbls)
except BaseException as e:
if e.__traceback__ is not None:
e.__traceback__ = e.__traceback__.tb_next
raise MarimoRuntimeException from e
Comment thread
dmadisetti marked this conversation as resolved.
5 changes: 1 addition & 4 deletions marimo/_runtime/runner/hooks_post_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
)
from marimo._messaging.tracebacks import (
_highlight_traceback,
_trim_traceback,
write_traceback,
)
from marimo._messaging.variables import create_variable_value
Expand Down Expand Up @@ -426,9 +425,7 @@ def _broadcast_outputs(
and run_result.exception.__traceback__
):
tb_lines = tb.format_exception(run_result.exception)
formatted_traceback = _highlight_traceback(
_trim_traceback("".join(tb_lines))
)
formatted_traceback = _highlight_traceback("".join(tb_lines))

CellNotificationUtils.broadcast_error(
data=[
Expand Down
9 changes: 0 additions & 9 deletions tests/_messaging/test_tracebacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from marimo._messaging.context import HTTP_REQUEST_CTX, is_code_mode_request
from marimo._messaging.tracebacks import (
_highlight_traceback,
_trim_traceback,
is_code_highlighting,
write_traceback,
)
Expand Down Expand Up @@ -221,11 +220,3 @@ def test_empty_url(self) -> None:
assert is_code_mode_request() is False
finally:
HTTP_REQUEST_CTX.reset(token)

def test_trim(self) -> None:
prefix = "Traceback (most recent call last):\n"
head = ' File ".../marimo/_runtime/executor.py", line 139, in execute_cell\n return eval(cell.last_expr, glbls)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
rest = (
' File ".../__marimo__cell_Hbol_.py", line 2, in <module>\n...\n'
)
assert _trim_traceback(f"{prefix}{head}{rest}") == f"{prefix}{rest}"
49 changes: 49 additions & 0 deletions tests/_runtime/test_executor_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import asyncio
from typing import Any

import pytest

from marimo._runtime.exceptions import MarimoRuntimeException
from marimo._runtime.executor import (
DefaultExecutor,
Expand Down Expand Up @@ -203,6 +205,53 @@ def is_coroutine(self) -> bool:
assert isinstance(a.last_run_result.exception, MarimoRuntimeException)


def _cause_traceback_filenames(exc: BaseException) -> list[str]:
cause = exc.__cause__
assert cause is not None
tb = cause.__traceback__
files: list[str] = []
while tb is not None:
files.append(tb.tb_frame.f_code.co_filename)
tb = tb.tb_next
return files


def test_default_executor_strips_own_frame_from_cause_sync() -> None:
"""``DefaultExecutor.execute_cell`` must not leave its own frame on
the cause's ``__traceback__`` — user-facing tracebacks should begin
at user code (the compiled ``<test>`` source)."""

class _FakeCell:
cell_id = "0"
body = compile("raise ValueError('user bomb')", "<test>", "exec")
last_expr = compile("None", "<test>", "eval")

with pytest.raises(MarimoRuntimeException) as exc_info:
DefaultExecutor().execute_cell(_FakeCell(), {}) # type: ignore[arg-type]

files = _cause_traceback_filenames(exc_info.value)
assert files, "cause traceback unexpectedly empty"
assert not any("executor/executor.py" in f for f in files), files
assert files[0] == "<test>"
Comment thread
dmadisetti marked this conversation as resolved.


async def test_default_executor_strips_own_frame_from_cause_async() -> None:
"""Same as the sync variant, for ``execute_cell_async``."""

class _FakeCell:
cell_id = "0"
body = compile("raise ValueError('user bomb')", "<test>", "exec")
last_expr = compile("None", "<test>", "eval")

with pytest.raises(MarimoRuntimeException) as exc_info:
await DefaultExecutor().execute_cell_async(_FakeCell(), {}) # type: ignore[arg-type]

files = _cause_traceback_filenames(exc_info.value)
assert files, "cause traceback unexpectedly empty"
assert not any("executor/executor.py" in f for f in files), files
assert files[0] == "<test>"
Comment thread
dmadisetti marked this conversation as resolved.


async def test_teardown_runs_for_completed_setups_when_later_setup_raises() -> (
None
):
Expand Down
2 changes: 1 addition & 1 deletion tests/_server/test_scratchpad_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ def test_ctx_create_cell_multiply_defined(session: _Session) -> None:
assert lines == snapshot(
[
"event: stderr",
'data: {"data": "Traceback (most recent call last):\\n File \\"<marimo>/marimo/_runtime/executor.py\\", line N, in execute_cell_async\\n await eval(cell.body, glbls)\\n File \\"<tmp>\\", line 2, in <module>\\n async with cm.get_context() as ctx:\\n File \\"<marimo>/marimo/_code_mode/_context.py\\", line N, in __aexit__\\n self._dry_run_compile(ops)\\n File \\"<marimo>/marimo/_code_mode/_context.py\\", line N, in _dry_run_compile\\n raise RuntimeError(\\nRuntimeError: Multiply-defined names:\\n - \'x\' is already defined in cell \'cell_a\' (cell_a)\\n\\nTo skip validation, use: async with cm.get_context(skip_validation=True) as ctx\\n"}',
'data: {"data": "Traceback (most recent call last):\\n File \\"<tmp>\\", line 2, in <module>\\n async with cm.get_context() as ctx:\\n File \\"<marimo>/marimo/_code_mode/_context.py\\", line N, in __aexit__\\n self._dry_run_compile(ops)\\n File \\"<marimo>/marimo/_code_mode/_context.py\\", line N, in _dry_run_compile\\n raise RuntimeError(\\nRuntimeError: Multiply-defined names:\\n - \'x\' is already defined in cell \'cell_a\' (cell_a)\\n\\nTo skip validation, use: async with cm.get_context(skip_validation=True) as ctx\\n"}',
"",
"event: done",
'data: {"success": false, "output": {"mimetype": "text/plain", "data": ""}}',
Expand Down
Loading