Skip to content

Commit

Permalink
fix: semicolon supression (#3830)
Browse files Browse the repository at this point in the history
## 📝 Summary

Replaces #3806 

## 🔍 Description of Changes

Resolves #3726

@mscolnick

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
dmadisetti and pre-commit-ci[bot] authored Feb 18, 2025
1 parent 39673fb commit dd03b87
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 1 deletion.
26 changes: 25 additions & 1 deletion marimo/_ast/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ def get_filename(cell_id: CellId_t, suffix: str = "") -> str:
return os.path.join(get_tmpdir(), basename + suffix + ".py")


def ends_with_semicolon(code: str) -> bool:
"""Returns True if the cell's code ends with a semicolon, ignoring whitespace and comments.
Args:
code: The cell's source code
Returns:
bool: True if the last non-comment line ends with a semicolon
"""
# Tokenize to check for semicolon
tokens = tokenize(io.BytesIO(code.strip().encode("utf-8")).readline)
for token in reversed(list(tokens)):
if token.type in (
token_types.ENDMARKER,
token_types.NEWLINE,
token_types.NL,
token_types.COMMENT,
):
continue
return token.string == ";"
return False


def cache(filename: str, code: str) -> None:
# Generate a cache entry in Python's linecache
linecache.cache[filename] = (
Expand Down Expand Up @@ -139,7 +161,9 @@ def compile_cell(

expr: ast.Expression
final_expr = module.body[-1]
if isinstance(final_expr, ast.Expr):
# Use final expression if it exists doesn't end in a
# semicolon. Evaluates expression to "None" otherwise.
if isinstance(final_expr, ast.Expr) and not ends_with_semicolon(code):
expr = ast.Expression(module.body.pop().value)
expr.lineno = final_expr.lineno
else:
Expand Down
80 changes: 80 additions & 0 deletions tests/_ast/test_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,83 @@ def test_cell_id_from_filename() -> None:
)

assert compiler.cell_id_from_filename("random_file.py") is None


class TestSemicolon:
@staticmethod
def test_return() -> None:
# fmt: off
def f() -> None:
1 # noqa: B018
# fmt: on

cell = compiler.cell_factory(f, cell_id="0")
assert eval(cell._cell.last_expr) == 1

@staticmethod
def test_return_suppressed() -> None:
# fmt: off
def f() -> None:
1; # noqa: B018 E703
# fmt: on

cell = compiler.cell_factory(f, cell_id="0")
assert eval(cell._cell.last_expr) is None

@staticmethod
def test_return_last() -> None:
# fmt: off
def f() -> None:
1; 2; 3 # noqa: B018 E702
# fmt: on

cell = compiler.cell_factory(f, cell_id="0")
assert eval(cell._cell.last_expr) == 3

@staticmethod
def test_return_last_suppressed() -> None:
# fmt: off
def f() -> None:
1; 2; 3; # noqa: B018 E702 E703
# fmt: on

cell = compiler.cell_factory(f, cell_id="0")
assert eval(cell._cell.last_expr) is None

@staticmethod
def test_return_comment() -> None:
def f() -> None:
1 # noqa: B018 # Has a comment;

cell = compiler.cell_factory(f, cell_id="0")
assert eval(cell._cell.last_expr) == 1

@staticmethod
def test_return_comment_suppressed() -> None:
# fmt: off
def f() -> None:
1; # noqa: B018 E703 # Has a comment
# fmt: on

cell = compiler.cell_factory(f, cell_id="0")
assert eval(cell._cell.last_expr) is None

@staticmethod
def test_return_string_semicolon() -> None:
def f() -> None:
"#; splits on ;# are less than ideal" # noqa: B018 Contains a ;#

cell = compiler.cell_factory(f, cell_id="0")
assert (
eval(cell._cell.last_expr) == "#; splits on ;# are less than ideal"
)

@staticmethod
def test_return_string_semicolon_suppressed() -> None:
# fmt: off
def f() -> None:
"#; splits on ;# are less than ideal"; # noqa: B018 E703 Contains a ;#
# fmt: on

cell = compiler.cell_factory(f, cell_id="0")
assert eval(cell._cell.last_expr) is None

0 comments on commit dd03b87

Please sign in to comment.