Skip to content

Commit 58e32dc

Browse files
committed
fix: Support notebook arguments when exporting thumbnail
Specifically for the thumbnails, special logic was used to support multiple paths as arguments. Unfortunately, that logic broke when using notebook arguments. Since `--` is not passed through by click when interspersed args are allowed, and every argument is wrongly extracted as a path. In this commit, I knowingly include broken tests. I do not intend to merge this as-is, but I will need the Marimo team input to decide how we want to fix this. Those tests are there simply to illustrate the discussion. The two `_SPLIT_PATH_AND_ARGS` and `_ALLOW_INTERSPERSED_ARGS` are also here purely for illustration. We should inline this once we decide for one approach. I also add two simple tests that should stay, to confirm that the kind of examples from the documentation do run successfully.
1 parent a431d66 commit 58e32dc

File tree

2 files changed

+154
-3
lines changed

2 files changed

+154
-3
lines changed

marimo/_cli/export/thumbnail.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
"install automatically. Requires uv. Only applies when --execute is used."
3636
)
3737
_READINESS_WAIT_TIMEOUT_MS = 30_000
38+
_SPLIT_PATH_AND_ARGS = False
39+
_ALLOW_INTERSPERSED_ARGS = True
3840

3941

4042
def _split_paths_and_args(
@@ -427,7 +429,9 @@ async def _generate_thumbnails(
427429

428430

429431
@click.command(
430-
"thumbnail", help="Generate OpenGraph thumbnails for notebooks."
432+
"thumbnail",
433+
help="Generate OpenGraph thumbnails for notebooks.",
434+
context_settings={"allow_interspersed_args": _ALLOW_INTERSPERSED_ARGS},
431435
)
432436
@click.argument(
433437
"name",
@@ -531,7 +535,11 @@ def thumbnail(
531535
) from None
532536
raise
533537

534-
paths, notebook_args = _split_paths_and_args(str(name), args)
538+
paths, notebook_args = (
539+
_split_paths_and_args(str(name), args)
540+
if _SPLIT_PATH_AND_ARGS
541+
else ([str(name)], args)
542+
)
535543
notebooks = _collect_notebooks([Path(p) for p in paths])
536544
if not notebooks:
537545
raise click.ClickException("No marimo notebooks found.")

tests/_cli/test_cli_export.py

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
import time
1111
from os import path
1212
from pathlib import Path
13-
from typing import TYPE_CHECKING
13+
from typing import TYPE_CHECKING, NamedTuple
1414

15+
import click
1516
import pytest
1617

18+
from marimo._cli.export.thumbnail import _split_paths_and_args
1719
from marimo._dependencies.dependencies import DependencyManager
1820
from marimo._utils import async_path
1921
from marimo._utils.platform import is_windows
@@ -860,3 +862,144 @@ def test_export_pdf_missing_dependencies(
860862
stderr = p.stderr.decode()
861863
assert "nbconvert" in stderr
862864
assert "pip install" in stderr
865+
866+
867+
@pytest.mark.skipif(
868+
not DependencyManager.playwright.has(),
869+
reason="This test requires playwright.",
870+
)
871+
class TestExportThumbnail:
872+
def test_export_thumbnail(self, temp_marimo_file: str) -> None:
873+
p = _run_export("thumbnail", temp_marimo_file)
874+
_assert_success(p)
875+
876+
def test_export_thumbnail_with_args(self, temp_marimo_file: str) -> None:
877+
p = _run_export("thumbnail", temp_marimo_file, "--", "--foo", "123")
878+
_assert_success(p)
879+
880+
881+
class TestSplitPathArgs:
882+
"""
883+
Temporary test suite to illustrate an issue...
884+
885+
It seems we can't support all of:
886+
- export options after path (e.g. `marimo export thumbnail nb.py --overwrite)
887+
- multiple paths (e.g. `marimo export thumbnail nb1.py nb2.py`)
888+
- notebook arguments (e.g. `marimo export thumbnail nb.py -- arg --foo`)
889+
890+
By default, click enables allow_interspersed_args, which allows having export options after the path, but
891+
removes `--` from the arguments. That means the logic of _split_paths_and_args breaks, and every notebook argument
892+
is instead considered a path.
893+
894+
We can disable allow_interspersed_args, which keeps `--` in the arguments and fixes _split_paths_and_args. That way,
895+
we still support multiple paths and notebook arguments, but we can't put export options (e.g. --overwrite) after
896+
arguments (e.g. notebook or folder path).
897+
898+
We can remove the _split_paths_and_args logic and simply trust click with default allow_interspersed_args, to
899+
support export options after path as well as notebook arguments; but then we support only one path at a time. That
900+
approach seems like it would be the most consistent with other export commands.
901+
"""
902+
903+
@staticmethod
904+
def _params(command, expected_paths, expected_args) -> list[NamedTuple]:
905+
test_command = f"`test_command {' '.join(command)}`"
906+
907+
def _bool_sign(b) -> str:
908+
return "+" if b else "-"
909+
910+
return [
911+
pytest.param(
912+
command,
913+
interspersed_args,
914+
split_path_and_args,
915+
expected_paths,
916+
expected_args,
917+
id=f"{test_command} [{_bool_sign(interspersed_args)}i {_bool_sign(split_path_and_args)}s]",
918+
)
919+
for interspersed_args in [True, False]
920+
for split_path_and_args in [True, False]
921+
]
922+
923+
@pytest.mark.parametrize(
924+
(
925+
"command",
926+
"allow_interspersed_args",
927+
"split_paths_and_args",
928+
"expected_paths",
929+
"expected_args",
930+
),
931+
[
932+
*_params(["foo.py"], ["foo.py"], ()),
933+
*_params(["foo.py", "bar.py"], ["foo.py", "bar.py"], ()),
934+
*_params(["--opt", "foo.py"], ["foo.py"], ()),
935+
*_params(["foo.py", "--opt"], ["foo.py"], ()),
936+
*_params(["--opt", "foo.py", "bar.py"], ["foo.py", "bar.py"], ()),
937+
*_params(["foo.py", "--opt", "bar.py"], ["foo.py", "bar.py"], ()),
938+
*_params(["foo.py", "bar.py", "--opt"], ["foo.py", "bar.py"], ()),
939+
*_params(
940+
["foo.py", "--", "a", "-b", "--c=d"],
941+
["foo.py"],
942+
("a", "-b", "--c=d"),
943+
),
944+
*_params(
945+
["foo.py", "bar.py", "--", "a", "-b", "--c=d"],
946+
["foo.py", "bar.py"],
947+
("a", "-b", "--c=d"),
948+
),
949+
*_params(
950+
["--opt", "foo.py", "--", "a", "-b", "--c=d"],
951+
["foo.py"],
952+
("a", "-b", "--c=d"),
953+
),
954+
*_params(
955+
["foo.py", "--opt", "--", "a", "-b", "--c=d"],
956+
["foo.py"],
957+
("a", "-b", "--c=d"),
958+
),
959+
*_params(
960+
["--opt", "foo.py", "bar.py", "--", "a", "-b", "--c=d"],
961+
["foo.py", "bar.py"],
962+
("a", "-b", "--c=d"),
963+
),
964+
*_params(
965+
["foo.py", "--opt", "bar.py", "--", "a", "-b", "--c=d"],
966+
["foo.py", "bar.py"],
967+
("a", "-b", "--c=d"),
968+
),
969+
*_params(
970+
["foo.py", "bar.py", "--opt", "--", "a", "-b", "--c=d"],
971+
["foo.py", "bar.py"],
972+
("a", "-b", "--c=d"),
973+
),
974+
],
975+
)
976+
def test_split_paths_and_args(
977+
self,
978+
command: list[str],
979+
allow_interspersed_args: bool,
980+
split_paths_and_args: bool,
981+
expected_paths: list[str],
982+
expected_args: tuple[str, ...],
983+
) -> None:
984+
@click.command(
985+
"test_command",
986+
context_settings={
987+
"allow_interspersed_args": allow_interspersed_args
988+
},
989+
)
990+
@click.argument("name")
991+
@click.option("--opt/--no-opt", default=False)
992+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
993+
def test_command(
994+
name: str, opt: bool, args: tuple[str, ...]
995+
) -> tuple[str, tuple[str, ...], bool]:
996+
return name, args, opt
997+
998+
name, args, _ = test_command.main(command, standalone_mode=False)
999+
paths, notebook_args = (
1000+
_split_paths_and_args(str(name), args)
1001+
if split_paths_and_args
1002+
else ([str(name)], args)
1003+
)
1004+
assert paths == expected_paths
1005+
assert notebook_args == expected_args

0 commit comments

Comments
 (0)