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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix: recognize class variables used within class scope as non-externa…
…l dependencies (#7265)

## Summary

Fixes #7265

When a class had a class variable (like `A: int = 1`) and that variable was referenced within the class scope (e.g., in method default parameters or other class variable definitions), marimo incorrectly concluded the class definition could not be reused. This was because `A` was being treated as an external dependency rather than a class-scoped variable.
  • Loading branch information
mscolnick committed Nov 24, 2025
commit 37dbef5972c91ed9ab6591d3af5ad12e47136b67
7 changes: 6 additions & 1 deletion marimo/_ast/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,12 @@ def _visit_and_get_refs(
# Thus, if it has been declared, it's not "unbounded"
class_def.add(var)
else:
unbounded_refs |= data.required_refs
# For non-function/non-class variables (e.g., class attributes),
# exclude references to variables already defined in class scope
unbounded_refs |= data.required_refs - class_def
# Add the variable to class_def so that later references
# to it don't create unbounded refs
class_def.add(var)
Copy link

Choose a reason for hiding this comment

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

Bug: Line executed multiple times inside loop

The statement unbounded_refs |= mock_visitor.refs - ignore_refs is executed inside the outer for var in mock_visitor.variable_data: loop, causing it to run once per class variable instead of once total. This incorrectly adds mock_visitor.refs - ignore_refs to unbounded_refs multiple times. The line needs to be moved outside the outer loop to execute only after all variables have been processed.

Fix in Cursor Fix in Web

unbounded_refs |= mock_visitor.refs - ignore_refs

# Handle function/class refs that are evaluated in the outer scope
Expand Down
73 changes: 73 additions & 0 deletions marimo/_smoke_tests/parse/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import marimo

__generated_with = "0.18.0"
app = marimo.App(width="medium")


@app.class_definition
# This should be reusable
class One:
A: int = 1

def run(a: A = 1) -> int:
return a


@app.class_definition
# This should be a pure-class
class Two:
A: int = 1

def run(a: int = 1) -> int:
return a


@app.cell
def _():
C = int
return (C,)


@app.cell
def _():
value = 1
return (value,)


@app.cell
def _(C):
# This should NOT be reusable (depends on C)
class Three:
A: int = 1
B: int = 1

def run(a: C = 1) -> int:
return a
return


@app.cell
def _(C):
# This should NOT be reusable (depends on C)
class Four:
A: int = 1
B: C = 1

def run(a: B = 1) -> int:
return a
return


@app.cell
def _(value):
# This should NOT be reusable (depends on value)
class Five:
A: int = 1

def run(a: A = value) -> int:
return a
return


if __name__ == "__main__":
app.run()
45 changes: 45 additions & 0 deletions tests/_ast/test_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1593,3 +1593,48 @@ def test_sql_pivot_unpivot_commands(

assert v.defs == {"df"}, f"Failed for: {description}"
assert v.refs == expected_refs, f"Failed for: {description}"


def test_class_with_class_var_in_method_default() -> None:
"""Test that class variables used in method defaults don't create unbounded refs (issue #7265)."""
code = cleandoc(
"""
class K:
A: int = 1
def b(a: int = A) -> int:
return a
"""
)
v = visitor.ScopedVisitor()
mod = ast.parse(code)
v.visit(mod)

# K should be a def
assert v.defs == set(["K"])
# A should NOT be a ref (it's defined in class scope), but int is (type annotation)
assert v.refs == set(["int"])
# K should have unbounded_refs only for int, not A
assert v.variable_data["K"][0].unbounded_refs == set(["int"])


def test_class_with_class_var_referencing_another() -> None:
"""Test that class variables used in other class variable definitions don't create unbounded refs (issue #7265)."""
code = cleandoc(
"""
class K:
A: int = 1
B: int = A
def b(a: int = B) -> int:
return a
"""
)
v = visitor.ScopedVisitor()
mod = ast.parse(code)
v.visit(mod)

# K should be a def
assert v.defs == set(["K"])
# A should NOT be a ref (it's defined in class scope), but int is (type annotation)
assert v.refs == set(["int"])
# K should have unbounded_refs only for int, not A
assert v.variable_data["K"][0].unbounded_refs == set(["int"])
Loading