Skip to content

Commit 3bc4d8d

Browse files
authored
fix: recognize class variables used within class scope as non-external dependencies (#7265) (#7273)
## 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.
1 parent f8edbd6 commit 3bc4d8d

File tree

3 files changed

+124
-1
lines changed

3 files changed

+124
-1
lines changed

‎marimo/_ast/visitor.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,12 @@ def _visit_and_get_refs(
531531
# Thus, if it has been declared, it's not "unbounded"
532532
class_def.add(var)
533533
else:
534-
unbounded_refs |= data.required_refs
534+
# For non-function/non-class variables (e.g., class attributes),
535+
# exclude references to variables already defined in class scope
536+
unbounded_refs |= data.required_refs - class_def
537+
# Add the variable to class_def so that later references
538+
# to it don't create unbounded refs
539+
class_def.add(var)
535540
unbounded_refs |= mock_visitor.refs - ignore_refs
536541

537542
# Handle function/class refs that are evaluated in the outer scope
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import marimo
2+
3+
__generated_with = "0.18.0"
4+
app = marimo.App(width="medium")
5+
6+
7+
@app.class_definition
8+
# This should be reusable
9+
class One:
10+
A: int = 1
11+
12+
def run(a: A = 1) -> int:
13+
return a
14+
15+
16+
@app.class_definition
17+
# This should be a pure-class
18+
class Two:
19+
A: int = 1
20+
21+
def run(a: int = 1) -> int:
22+
return a
23+
24+
25+
@app.cell
26+
def _():
27+
C = int
28+
return (C,)
29+
30+
31+
@app.cell
32+
def _():
33+
value = 1
34+
return (value,)
35+
36+
37+
@app.cell
38+
def _(C):
39+
# This should NOT be reusable (depends on C)
40+
class Three:
41+
A: int = 1
42+
B: int = 1
43+
44+
def run(a: C = 1) -> int:
45+
return a
46+
return
47+
48+
49+
@app.cell
50+
def _(C):
51+
# This should NOT be reusable (depends on C)
52+
class Four:
53+
A: int = 1
54+
B: C = 1
55+
56+
def run(a: B = 1) -> int:
57+
return a
58+
return
59+
60+
61+
@app.cell
62+
def _(value):
63+
# This should NOT be reusable (depends on value)
64+
class Five:
65+
A: int = 1
66+
67+
def run(a: A = value) -> int:
68+
return a
69+
return
70+
71+
72+
if __name__ == "__main__":
73+
app.run()

‎tests/_ast/test_visitor.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,3 +1593,48 @@ def test_sql_pivot_unpivot_commands(
15931593

15941594
assert v.defs == {"df"}, f"Failed for: {description}"
15951595
assert v.refs == expected_refs, f"Failed for: {description}"
1596+
1597+
1598+
def test_class_with_class_var_in_method_default() -> None:
1599+
"""Test that class variables used in method defaults don't create unbounded refs (issue #7265)."""
1600+
code = cleandoc(
1601+
"""
1602+
class K:
1603+
A: int = 1
1604+
def b(a: int = A) -> int:
1605+
return a
1606+
"""
1607+
)
1608+
v = visitor.ScopedVisitor()
1609+
mod = ast.parse(code)
1610+
v.visit(mod)
1611+
1612+
# K should be a def
1613+
assert v.defs == set(["K"])
1614+
# A should NOT be a ref (it's defined in class scope), but int is (type annotation)
1615+
assert v.refs == set(["int"])
1616+
# K should have unbounded_refs only for int, not A
1617+
assert v.variable_data["K"][0].unbounded_refs == set(["int"])
1618+
1619+
1620+
def test_class_with_class_var_referencing_another() -> None:
1621+
"""Test that class variables used in other class variable definitions don't create unbounded refs (issue #7265)."""
1622+
code = cleandoc(
1623+
"""
1624+
class K:
1625+
A: int = 1
1626+
B: int = A
1627+
def b(a: int = B) -> int:
1628+
return a
1629+
"""
1630+
)
1631+
v = visitor.ScopedVisitor()
1632+
mod = ast.parse(code)
1633+
v.visit(mod)
1634+
1635+
# K should be a def
1636+
assert v.defs == set(["K"])
1637+
# A should NOT be a ref (it's defined in class scope), but int is (type annotation)
1638+
assert v.refs == set(["int"])
1639+
# K should have unbounded_refs only for int, not A
1640+
assert v.variable_data["K"][0].unbounded_refs == set(["int"])

0 commit comments

Comments
 (0)