fix: don't transform main guard inside strings/comments in convert#10237
fix: don't transform main guard inside strings/comments in convert#10237vidigoat wants to merge 1 commit into
Conversation
marimo convert located the `if __name__ == "__main__":` block with a naive string search, so the text appearing inside a string literal or comment was treated as a real guard. The cell was then sliced mid-token and rewritten, producing invalid code that was emitted as an unparsable cell. Detect the guard via the AST instead: find a top-level `if` whose test is `__name__ == "__main__"` (in either order) with no `else`, and rewrite only that. Cells that don't parse are left untouched. This also fixes the single-quoted and reversed-comparison forms, which were silently not transformed before.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
| # Absolute offset of each line start (ast linenos are 1-based and | ||
| # count physical "\n"-separated lines). | ||
| line_starts = [0] | ||
| for line in cell.code.split("\n"): | ||
| line_starts.append(line_starts[-1] + len(line) + 1) | ||
|
|
||
| start = line_starts[guard.lineno - 1] + guard.col_offset | ||
| test_end_lineno = guard.test.end_lineno or guard.lineno | ||
| test_end_col = guard.test.end_col_offset or 0 | ||
| test_end = line_starts[test_end_lineno - 1] + test_end_col | ||
| # The colon that closes the `if` header is the first ":" after the | ||
| # end of the test expression (comments cannot appear before it). | ||
| colon = cell.code.index(":", test_end) | ||
|
|
||
| before_main = cell.code[:start].strip() | ||
|
|
||
| # replace the if __name__ == "__main__": with def _main_(): | ||
| main_block = "def _main_():" + cell.code[colon + 1 :] + "\n\n_main_()" | ||
|
|
||
| if before_main: | ||
| cell.code = before_main + "\n\n" + main_block | ||
| else: | ||
| cell.code = main_block |
There was a problem hiding this comment.
I think an AST transform may be better than this. I'm wondering if replacing with def _main_ makes sense either.
I reckon if mo.app_meta().mode == "script" almost makes more sense?
|
Thanks @dmadisetti! Two things: On the AST point — the PR does use AST to find the guard (that's the actual fix here; the old code string-matched On It's still a draft, so happy to settle the approach first before polishing. |
Agreed here. Cool I think this makes sense |
|
Thanks @dmadisetti! Glad the string-splice reasoning makes sense. The one open question is the target: keep |
Avoids mangling cells when the guard text appears inside a string or comment. Also handles single-quoted and reversed comparisons. Based on work by @vidigoat in marimo-team#10237. Co-authored-by: Vidit Patankar <vidit.patankar16@gmail.com>
This pull request was authored by a coding agent.
📝 Summary
marimo convertcorrupts valid Python scripts when the textif __name__ == "__main__":appears inside a string literal or a comment._transform_main_blockslocated the guard with a naivecell.code.split('if __name__ == "__main__":', 1), so any occurrence of that text — even inside a string or comment — was treated as a real guard. The cell was sliced mid-token and rewritten todef _main_(): …, producing syntactically invalid code that is then emitted as anapp._unparsable_cell(...)blob.This affects
marimo convert script.py,marimo editon a non-marimo.pyfile, and the server template API — both the plain-script and jupytext/pypercent paths.🔍 Reproduction (before this PR)
Output on
mainslices the string literal and emits a mangledapp._unparsable_cell(...). The same happens for a comment that mentions the guard.🔧 Fix
Detect the guard via the AST: find a top-level
ast.Ifwhose test is__name__ == "__main__"(either operand order) with noelse, and rewrite only that statement using line/column offsets. Cells that don't parse are left untouched (they're emitted as unparsable cells downstream anyway).As a bonus this also fixes two cases that were silently not transformed before: the single-quoted form (
if __name__ == '__main__':) and the reversed comparison (if "__main__" == __name__:).✅ Tests
TestTransformMainBlocks(6 cases: guard-in-string, guard-in-comment, single quotes, reversed comparison, and that a real guard still transforms).uv run --python 3.13 --group test pytest tests/_convert tests/_cli/test_cli_convert.py→ 363 passed, 2 skipped (env), 1 xfail (pre-existing, unrelated).ruff check,ruff format --check, andmypyall clean on the changed files.📋 Checklist