Skip to content

Commit f7513e3

Browse files
authored
fix: support type aliases defined within class scope (#7089) (#7284)
Fixes #7089 Classes with embedded type aliases (using `TypeAlias` annotation or Python 3.12+'s `type` statement) were incorrectly nested inside cells instead of being recognized as top-level definitions. Type aliases like `type MyEmbeddedType = int` defined within a class and used in methods (e.g., `def __init__(self, value: MyEmbeddedType)`) should be recognized as class-scoped, not external dependencies.
1 parent 99b9858 commit f7513e3

File tree

3 files changed

+260
-1
lines changed

3 files changed

+260
-1
lines changed

‎marimo/_ast/visitor.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,10 @@ def _visit_and_get_refs(
533533
else:
534534
# For non-function/non-class variables (e.g., class attributes),
535535
# exclude references to variables already defined in class scope
536-
unbounded_refs |= data.required_refs - class_def
536+
# Also exclude self-references (e.g., TypeAlias can reference itself)
537+
unbounded_refs |= (
538+
data.required_refs - class_def - {var}
539+
)
537540
# Add the variable to class_def so that later references
538541
# to it don't create unbounded refs
539542
class_def.add(var)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import marimo
2+
3+
__generated_with = "0.18.0"
4+
app = marimo.App(width="medium")
5+
6+
with app.setup:
7+
from typing import TypeAlias
8+
9+
MySetupTypeAlias: TypeAlias = int
10+
type MySetupType = int
11+
12+
13+
@app.class_definition
14+
class SetupTypeAlias:
15+
def __init__(self, value: MySetupTypeAlias):
16+
self.value = value
17+
18+
19+
@app.class_definition
20+
class SetupType:
21+
def __init__(self, value: MySetupType):
22+
self.value = value
23+
24+
25+
@app.class_definition
26+
class EmbeddedTypeAlias:
27+
MyEmbeddedTypeAlias: TypeAlias = int
28+
29+
def __init__(self, value: MyEmbeddedTypeAlias):
30+
self.value = value
31+
32+
33+
@app.class_definition
34+
class EmbeddedType:
35+
type MyEmbeddedType = int
36+
37+
def __init__(self, value: MyEmbeddedType):
38+
self.value = value
39+
40+
41+
if __name__ == "__main__":
42+
app.run()

‎tests/_ast/test_visitor.py‎

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,3 +1638,217 @@ def b(a: int = B) -> int:
16381638
assert v.refs == set(["int"])
16391639
# K should have unbounded_refs only for int, not A
16401640
assert v.variable_data["K"][0].unbounded_refs == set(["int"])
1641+
1642+
1643+
def test_class_with_typealias_annotation() -> None:
1644+
"""Test that TypeAlias annotations in class scope don't create unbounded refs (issue #7089)."""
1645+
code = cleandoc(
1646+
"""
1647+
class EmbeddedTypeAlias:
1648+
MyEmbeddedTypeAlias: TypeAlias = int
1649+
1650+
def __init__(self, value: MyEmbeddedTypeAlias):
1651+
self.value = value
1652+
"""
1653+
)
1654+
v = visitor.ScopedVisitor()
1655+
mod = ast.parse(code)
1656+
v.visit(mod)
1657+
1658+
# EmbeddedTypeAlias should be a def
1659+
assert v.defs == set(["EmbeddedTypeAlias"])
1660+
# MyEmbeddedTypeAlias should NOT be a ref (it's defined in class scope)
1661+
# TypeAlias and int are refs (type annotations)
1662+
assert v.refs == set(["TypeAlias", "int"])
1663+
# Should have unbounded_refs only for TypeAlias and int, not MyEmbeddedTypeAlias
1664+
assert v.variable_data["EmbeddedTypeAlias"][0].unbounded_refs == set(
1665+
["TypeAlias", "int"]
1666+
)
1667+
1668+
1669+
@pytest.mark.skipif("sys.version_info < (3, 12)")
1670+
def test_class_with_type_statement() -> None:
1671+
"""Test that type statements in class scope don't create unbounded refs (issue #7089)."""
1672+
code = cleandoc(
1673+
"""
1674+
class EmbeddedType:
1675+
type MyEmbeddedType = int
1676+
1677+
def __init__(self, value: MyEmbeddedType):
1678+
self.value = value
1679+
"""
1680+
)
1681+
v = visitor.ScopedVisitor()
1682+
mod = ast.parse(code)
1683+
v.visit(mod)
1684+
1685+
# EmbeddedType should be a def
1686+
assert v.defs == set(["EmbeddedType"])
1687+
# MyEmbeddedType should NOT be a ref (it's defined in class scope via type statement)
1688+
# int is a ref (used in type statement)
1689+
assert v.refs == set(["int"])
1690+
# Should have unbounded_refs only for int, not MyEmbeddedType
1691+
assert v.variable_data["EmbeddedType"][0].unbounded_refs == set(["int"])
1692+
1693+
1694+
def test_class_with_external_var_in_method_default() -> None:
1695+
"""Test that external variables in method defaults ARE flagged as unbounded refs."""
1696+
code = cleandoc(
1697+
"""
1698+
class K:
1699+
A: int = 1
1700+
def b(a: int = A, c: int = EXTERNAL) -> int:
1701+
return a + c
1702+
"""
1703+
)
1704+
v = visitor.ScopedVisitor()
1705+
mod = ast.parse(code)
1706+
v.visit(mod)
1707+
1708+
# K should be a def
1709+
assert v.defs == set(["K"])
1710+
# EXTERNAL should be a ref (not defined in class)
1711+
assert v.refs == set(["int", "EXTERNAL"])
1712+
# K should have unbounded_refs for EXTERNAL, but not A
1713+
assert v.variable_data["K"][0].unbounded_refs == set(["int", "EXTERNAL"])
1714+
1715+
1716+
def test_class_with_external_var_in_class_var() -> None:
1717+
"""Test that external variables used in class variables ARE flagged as unbounded refs."""
1718+
code = cleandoc(
1719+
"""
1720+
class K:
1721+
A: int = 1
1722+
B: int = A + EXTERNAL
1723+
def b(a: int = 1) -> int:
1724+
return a
1725+
"""
1726+
)
1727+
v = visitor.ScopedVisitor()
1728+
mod = ast.parse(code)
1729+
v.visit(mod)
1730+
1731+
# K should be a def
1732+
assert v.defs == set(["K"])
1733+
# EXTERNAL should be a ref (not defined in class)
1734+
assert v.refs == set(["int", "EXTERNAL"])
1735+
# K should have unbounded_refs for EXTERNAL, but not A
1736+
assert v.variable_data["K"][0].unbounded_refs == set(["int", "EXTERNAL"])
1737+
1738+
1739+
def test_class_with_external_type_in_typealias() -> None:
1740+
"""Test that external types in TypeAlias ARE flagged as unbounded refs."""
1741+
code = cleandoc(
1742+
"""
1743+
class K:
1744+
MyType: TypeAlias = ExternalType
1745+
1746+
def __init__(self, value: MyType):
1747+
self.value = value
1748+
"""
1749+
)
1750+
v = visitor.ScopedVisitor()
1751+
mod = ast.parse(code)
1752+
v.visit(mod)
1753+
1754+
# K should be a def
1755+
assert v.defs == set(["K"])
1756+
# ExternalType and TypeAlias should be refs (not defined in class)
1757+
assert v.refs == set(["TypeAlias", "ExternalType"])
1758+
# K should have unbounded_refs for both TypeAlias and ExternalType, but not MyType
1759+
assert v.variable_data["K"][0].unbounded_refs == set(
1760+
["TypeAlias", "ExternalType"]
1761+
)
1762+
1763+
1764+
@pytest.mark.skipif("sys.version_info < (3, 12)")
1765+
def test_class_with_external_type_in_type_statement() -> None:
1766+
"""Test that external types in type statement ARE flagged as unbounded refs."""
1767+
code = cleandoc(
1768+
"""
1769+
class K:
1770+
type MyType = ExternalType
1771+
1772+
def __init__(self, value: MyType):
1773+
self.value = value
1774+
"""
1775+
)
1776+
v = visitor.ScopedVisitor()
1777+
mod = ast.parse(code)
1778+
v.visit(mod)
1779+
1780+
# K should be a def
1781+
assert v.defs == set(["K"])
1782+
# ExternalType should be a ref (not defined in class)
1783+
assert v.refs == set(["ExternalType"])
1784+
# K should have unbounded_refs for ExternalType, but not MyType
1785+
assert v.variable_data["K"][0].unbounded_refs == set(["ExternalType"])
1786+
1787+
1788+
def test_class_with_method_using_external_var() -> None:
1789+
"""Test that methods using external variables in body track them as required_refs."""
1790+
code = cleandoc(
1791+
"""
1792+
class K:
1793+
def method(self) -> int:
1794+
return EXTERNAL_VAR
1795+
"""
1796+
)
1797+
v = visitor.ScopedVisitor()
1798+
mod = ast.parse(code)
1799+
v.visit(mod)
1800+
1801+
# K should be a def
1802+
assert v.defs == set(["K"])
1803+
# EXTERNAL_VAR should be a ref (used in method body)
1804+
assert v.refs == set(["EXTERNAL_VAR", "int"])
1805+
# K should have EXTERNAL_VAR in required_refs (method needs it)
1806+
assert v.variable_data["K"][0].required_refs == set(
1807+
["EXTERNAL_VAR", "int"]
1808+
)
1809+
# But not in unbounded_refs (it's in method body, not signature)
1810+
assert v.variable_data["K"][0].unbounded_refs == set(["int"])
1811+
1812+
1813+
def test_class_with_forward_reference_to_method() -> None:
1814+
"""Test that class variables can reference methods defined earlier (valid)."""
1815+
code = cleandoc(
1816+
"""
1817+
class A:
1818+
def method(self):
1819+
return 42
1820+
bound = method
1821+
"""
1822+
)
1823+
v = visitor.ScopedVisitor()
1824+
mod = ast.parse(code)
1825+
v.visit(mod)
1826+
1827+
# A should be a def
1828+
assert v.defs == set(["A"])
1829+
# No external refs - method is defined within class scope
1830+
assert v.refs == set()
1831+
# A should have no unbounded refs (forward reference is valid)
1832+
assert v.variable_data["A"][0].unbounded_refs == set()
1833+
1834+
1835+
def test_class_with_backward_reference_to_method() -> None:
1836+
"""Test that class variables cannot reference methods defined later (invalid for top-level)."""
1837+
code = cleandoc(
1838+
"""
1839+
class B:
1840+
bound = method
1841+
def method(self):
1842+
return 42
1843+
"""
1844+
)
1845+
v = visitor.ScopedVisitor()
1846+
mod = ast.parse(code)
1847+
v.visit(mod)
1848+
1849+
# B should be a def
1850+
assert v.defs == set(["B"])
1851+
# method is referenced before it's defined, so it's an external ref
1852+
assert v.refs == set(["method"])
1853+
# B should have method in unbounded refs (backward reference is invalid)
1854+
assert v.variable_data["B"][0].unbounded_refs == set(["method"])

0 commit comments

Comments
 (0)