Skip to content

fix(formatter): wrap cells in function scope before ruff formatting#9889

Merged
dmadisetti merged 7 commits into
marimo-team:mainfrom
VishakBaddur:fix/ruff-nested-function-blank-lines
Jun 25, 2026
Merged

fix(formatter): wrap cells in function scope before ruff formatting#9889
dmadisetti merged 7 commits into
marimo-team:mainfrom
VishakBaddur:fix/ruff-nested-function-blank-lines

Conversation

@VishakBaddur

@VishakBaddur VishakBaddur commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

📝 Summary

Closes #9848

RuffFormatter.format() was passing raw cell code directly to ruff format via stdin. Ruff treats stdin as file scope and applies E302 (2 blank lines around top-level function definitions). But when the user runs ruff on the saved .py file, cells are already wrapped in def _(): functions, so ruff applies E301 (1 blank line around nested defs). This creates a conflict - marimo's in-editor formatting and ruff on the saved file disagree on blank lines around nested functions.

Fix: Before sending to ruff, wrap each non-empty cell in def _():\n + indented body. After formatting, drop the first line, dedent, and strip. This makes marimo match what ruff produces on the saved file. Whitespace-only cells are left unwrapped (empty function body is invalid Python). If wrapping causes ruff to reject a cell, it falls back to formatting unwrapped.

📋 Pre-Review Checklist

  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions. (Issue: Extra lines around nested functions conflict with Ruff formatting #9848)
  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • Video or media evidence is provided for any visual changes (optional).

✅ Merge Checklist

  • I have read the contributor guidelines.
  • Documentation has been updated where applicable, including docstrings for API changes. (No public API changes; internal formatter only.)
  • Tests have been added for the changes made.
@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Jun 16, 2026 10:59pm

Request Review

Comment thread marimo/_utils/formatter.py
Comment thread marimo/_utils/formatter.py Outdated
# Wrapping caused ruff to reject this cell; retry unwrapped.
fallback_keys.append(key)

if fallback_keys:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any known code paths trigger this or is this just defensive?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't find a real code path that triggers it(any valid cell body is valid inside def _():), so removed it.

@dmadisetti dmadisetti left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again @VishakBaddur !

Test looks great. I wonder if we can trigger that "failed" branch though
Think you can give a shot at using marimo/ast/_parse.py?

@VishakBaddur

Copy link
Copy Markdown
Contributor Author

Switched the unwrap to use marimo/_ast/parse.py and removed the fallback since I couldn't find a case that actually hits it. Let me know if you think there's an edge case I'm missing!

@dmadisetti dmadisetti left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another comment, sorry. Also realizing- can you add a top level function test? e.g.

@app.function
def foo():
    """Something here"""
Comment thread marimo/_utils/formatter.py Outdated

@dmadisetti dmadisetti left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry found a bug- but clean!

Comment thread marimo/_utils/formatter.py Outdated
# left unwrapped because an empty function body is invalid Python.
wrapped: CellCodes = {}
for key, code in codes.items():
if code.strip():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may fail for pure comments too just realizing- can we add a test?

def _():
    # just a comment

is also a syntax error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed by precomputing skip_wrap, a set of keys whose code has no executable statements (all lines are whitespace or comments). Both the wrap and unwrap loops key off this set, so comment-only cells are never wrapped. I've also added a test covering this case.

Comment thread marimo/_ast/parse.py Outdated
Comment on lines +86 to +87
assert isinstance(fn, ast.FunctionDef)
assert fn.end_lineno is not None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should throw a value error instead of assert here I think.
Assertions are a little obtuse, and although this path should never be hit (type safety sanity check)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced both with raise ValueError

Comment thread marimo/_utils/formatter.py Outdated
wrapped: CellCodes = {}
for key, code in codes.items():
if code.strip():
wrapped[key] = "def _():\n" + textwrap.indent(code, " ")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're making changes, fstring?

@dmadisetti dmadisetti Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, the \n will make that annoying

Suggested change
wrapped[key] = "def _():\n" + textwrap.indent(code, " ")
wrapped[key] = "\n".join("def _():", textwrap.indent(code, " "))

or reuse the helper, which will take care of the comment case (because there will be a return)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used "\n".join(...) as suggested. Regarding the alternative to reuse the helper, if we wrapped comment-only cells and appended a pass to make the function body syntactically valid, the pass would end up preserved in the unwrapped output. Handling them via skip_wrap keeps the final output completely clean.

Comment thread marimo/_utils/formatter.py Outdated
# parse mechanism in marimo/_ast/parse.py (Extractor + fixed_dedent).
result: CellCodes = {}
for key, code in codes.items():
if not code.strip():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just use the helper, then we won't need the conditional (all code contents should be handled)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skip_wrap set removes the need for the inline condition in both loops. wrap loop uses key not in skip_wrap, unwrap loop uses key in skip_wrap. No duplication, no condition repeated inline

@dmadisetti dmadisetti left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@dmadisetti dmadisetti merged commit 7ae012e into marimo-team:main Jun 25, 2026
39 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.23.11-dev26

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

2 participants