Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions marimo/_ast/cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ class CellImpl:
# unique id
cell_id: CellId_t

# Markdown content of the cell if it exists
markdown: Optional[str] = None

# Mutable fields
# explicit configuration of cell
config: CellConfig = dataclasses.field(default_factory=CellConfig)
Expand Down
49 changes: 49 additions & 0 deletions marimo/_ast/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,54 @@ def fix_source_position(node: Any, source_position: SourcePosition) -> Any:
return node


def const_string(args: list[ast.stmt]) -> str:
(inner,) = args
if hasattr(inner, "values"):
(inner,) = inner.values
return f"{inner.value}" # type: ignore[attr-defined]


def const_or_id(args: ast.stmt) -> str:
if hasattr(args, "value"):
return f"{args.value}" # type: ignore[attr-defined]
return f"{args.id}" # type: ignore[attr-defined]


def extract_markdown(code: str) -> Optional[str]:
markdown_lines = [
line for line in code.strip().split("\n") if line.startswith("mo.md(")
]
if len(markdown_lines) > 1:
Copy link
Contributor

Choose a reason for hiding this comment

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

this could be slightly cheaper some(line.startswith("mo.md(") for line in code.strip().split("\n")

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice- we don't actually need this check at all, ast parse handles this case.

Copy link
Contributor

Choose a reason for hiding this comment

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

but isn't ast.parse more expensive. should we have a cheap check first and then actual parsing?

Copy link
Contributor

Choose a reason for hiding this comment

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

the cheap one could even be "mo.md(" in code

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We have the have the ast already- so in those cases just directly use that, while allowing for the early stopping if extracted directly from code.

return None

code = code.strip()
# Attribute Error handled by the outer try/except block.
# Wish there was a more compact to ignore ignore[attr-defined] for all.
try:
(body,) = ast.parse(code).body
if body.value.func.attr == "md": # type: ignore[attr-defined]
value = body.value # type: ignore[attr-defined]
else:
return None
assert value.func.value.id == "mo"
md_lines = const_string(value.args).split("\n")
except (AssertionError, AttributeError, ValueError, SyntaxError):
# No reason to explicitly catch exceptions if we can't parse out
# markdown. Just handle it as a code block.
return None

# Dedent behavior is a little different that in marimo js, so handle
# accordingly.
md_lines = [line.rstrip() for line in md_lines]
md = (
textwrap.dedent(md_lines[0])
+ "\n"
+ textwrap.dedent("\n".join(md_lines[1:]))
)
md = md.strip()
return md


def compile_cell(
code: str,
cell_id: CellId_t,
Expand Down Expand Up @@ -310,6 +358,7 @@ def compile_cell(
body=body,
last_expr=last_expr,
cell_id=cell_id,
markdown=extract_markdown(code),
_test=is_test,
)

Expand Down
46 changes: 3 additions & 43 deletions marimo/_server/export/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import ast
import os
import re
from textwrap import dedent
from typing import Optional, Union

from marimo._ast.cell import Cell, CellImpl
from marimo._ast.compiler import const_or_id, extract_markdown


def format_filename_title(filename: str) -> str:
Expand All @@ -31,54 +31,14 @@ def get_download_filename(filename: Optional[str], extension: str) -> str:
return f"{os.path.splitext(basename)[0]}.{extension}"


def _const_string(args: list[ast.stmt]) -> str:
(inner,) = args
if hasattr(inner, "values"):
(inner,) = inner.values
return f"{inner.value}" # type: ignore[attr-defined]


def _const_or_id(args: ast.stmt) -> str:
if hasattr(args, "value"):
return f"{args.value}" # type: ignore[attr-defined]
return f"{args.id}" # type: ignore[attr-defined]


def get_markdown_from_cell(
cell: Union[CellImpl, Cell], code: str
) -> Optional[str]:
"""Attempt to extract markdown from a cell, or return None"""

if not (cell.refs == {"mo"} and not cell.defs):
return None
markdown_lines = [
line for line in code.strip().split("\n") if line.startswith("mo.md(")
]
if len(markdown_lines) > 1:
return None

code = code.strip()
# Attribute Error handled by the outer try/except block.
# Wish there was a more compact to ignore ignore[attr-defined] for all.
try:
(body,) = ast.parse(code).body
if body.value.func.attr == "md": # type: ignore[attr-defined]
value = body.value # type: ignore[attr-defined]
else:
return None
assert value.func.value.id == "mo"
md_lines = _const_string(value.args).split("\n")
except (AssertionError, AttributeError, ValueError, SyntaxError):
# No reason to explicitly catch exceptions if we can't parse out
# markdown. Just handle it as a code block.
return None

# Dedent behavior is a little different that in marimo js, so handle
# accordingly.
md_lines = [line.rstrip() for line in md_lines]
md = dedent(md_lines[0]) + "\n" + dedent("\n".join(md_lines[1:]))
md = md.strip()
return md
return extract_markdown(code)


def get_sql_options_from_cell(code: str) -> Optional[dict[str, str]]:
Expand All @@ -96,7 +56,7 @@ def get_sql_options_from_cell(code: str) -> Optional[dict[str, str]]:
return None
if value.keywords:
for keyword in value.keywords: # type: ignore[attr-defined]
options[keyword.arg] = _const_or_id(keyword.value) # type: ignore[attr-defined]
options[keyword.arg] = const_or_id(keyword.value) # type: ignore[attr-defined]
output = options.pop("output", "True").lower()
if output == "false":
options["hide_output"] = "True"
Expand Down
Loading