moops 0.3.1__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {moops-0.3.1 → moops-0.3.3}/PKG-INFO +1 -1
- {moops-0.3.1 → moops-0.3.3}/pyproject.toml +2 -1
- {moops-0.3.1 → moops-0.3.3}/src/moops/__init__.py +2 -1
- {moops-0.3.1 → moops-0.3.3}/src/moops/_options.py +20 -18
- {moops-0.3.1 → moops-0.3.3}/src/moops/_query_params.py +1 -3
- moops-0.3.3/src/moops/_run_button.py +20 -0
- {moops-0.3.1 → moops-0.3.3}/src/moops/embed.py +5 -1
- {moops-0.3.1 → moops-0.3.3}/src/moops/group.py +31 -6
- {moops-0.3.1 → moops-0.3.3}/README.md +0 -0
- {moops-0.3.1 → moops-0.3.3}/src/moops/_input_map.py +0 -0
- {moops-0.3.1 → moops-0.3.3}/src/moops/_markdown.py +0 -0
- {moops-0.3.1 → moops-0.3.3}/src/moops/_naming.py +0 -0
- {moops-0.3.1 → moops-0.3.3}/src/moops/_parse.py +0 -0
- {moops-0.3.1 → moops-0.3.3}/src/moops/_run.py +0 -0
- {moops-0.3.1 → moops-0.3.3}/src/moops/interface.py +0 -0
- {moops-0.3.1 → moops-0.3.3}/src/moops/presets.py +0 -0
- {moops-0.3.1 → moops-0.3.3}/src/moops/testing.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "moops"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Write Marimo notebooks that also work as CLI scripts, with unified UI controls"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
readme = "README.md"
|
|
@@ -40,6 +40,7 @@ dev-dependencies = [
|
|
|
40
40
|
"pytest>=9.0.3",
|
|
41
41
|
"vulture>=2.16",
|
|
42
42
|
"pymarkdownlnt>=0.9.37",
|
|
43
|
+
"matplotlib>=3.10.9",
|
|
43
44
|
]
|
|
44
45
|
|
|
45
46
|
[tool.pytest.ini_options]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from importlib.metadata import version
|
|
2
2
|
|
|
3
3
|
from ._run import run
|
|
4
|
+
from ._run_button import run_button
|
|
4
5
|
from .group import Group
|
|
5
6
|
from .interface import Interface
|
|
6
7
|
from .presets import Presets
|
|
7
8
|
|
|
8
9
|
__version__ = version("moops")
|
|
9
10
|
|
|
10
|
-
__all__ = ["Group", "Interface", "Presets", "__version__", "run"]
|
|
11
|
+
__all__ = ["Group", "Interface", "Presets", "__version__", "run", "run_button"]
|
|
@@ -36,7 +36,7 @@ class InputControl(abc.ABC):
|
|
|
36
36
|
|
|
37
37
|
def options(self) -> set[str]:
|
|
38
38
|
"""Value options for this control."""
|
|
39
|
-
return
|
|
39
|
+
return {self.option}
|
|
40
40
|
|
|
41
41
|
def flags(self) -> set[str]:
|
|
42
42
|
"""Flags for this control."""
|
|
@@ -98,6 +98,9 @@ class InputControl(abc.ABC):
|
|
|
98
98
|
class FlagControl(InputControl):
|
|
99
99
|
default: bool = False
|
|
100
100
|
|
|
101
|
+
def options(self) -> set[str]:
|
|
102
|
+
return set()
|
|
103
|
+
|
|
101
104
|
def flags(self) -> set[str]:
|
|
102
105
|
return {self.option}
|
|
103
106
|
|
|
@@ -157,9 +160,6 @@ class ValueControl(InputControl):
|
|
|
157
160
|
|
|
158
161
|
metavar: str
|
|
159
162
|
|
|
160
|
-
def options(self) -> set[str]:
|
|
161
|
-
return {self.option}
|
|
162
|
-
|
|
163
163
|
def format_usage_parts(self) -> list[str]:
|
|
164
164
|
return [f"[{self.option} {self.metavar}]"]
|
|
165
165
|
|
|
@@ -536,13 +536,10 @@ class RangeControl(ValueControl):
|
|
|
536
536
|
|
|
537
537
|
@dataclasses.dataclass
|
|
538
538
|
class DropdownControl(InputControl):
|
|
539
|
-
|
|
539
|
+
dropdown_opts: dict[str, typing.Any]
|
|
540
540
|
supports_none: bool
|
|
541
541
|
default: str | None
|
|
542
542
|
|
|
543
|
-
def options(self) -> set[str]:
|
|
544
|
-
return {self.option}
|
|
545
|
-
|
|
546
543
|
@property
|
|
547
544
|
def has_no_flag(self) -> bool:
|
|
548
545
|
return self.supports_none and self.default is not None
|
|
@@ -564,26 +561,28 @@ class DropdownControl(InputControl):
|
|
|
564
561
|
raw = args.value_for(self.option)
|
|
565
562
|
if raw is None:
|
|
566
563
|
return None
|
|
567
|
-
if raw not in self.
|
|
564
|
+
if raw not in self.dropdown_opts:
|
|
568
565
|
return ParseError(
|
|
569
566
|
f"Option {self.option} must be one of"
|
|
570
|
-
f" {self.
|
|
567
|
+
f" {list(self.dropdown_opts)!r}, got: {raw!r}"
|
|
571
568
|
)
|
|
572
569
|
return ParseResult(raw)
|
|
573
570
|
|
|
574
571
|
def parse_query_value(self, value: str) -> ParseResult | ParseError:
|
|
575
572
|
if not value and self.supports_none:
|
|
576
573
|
return ParseResult(None)
|
|
577
|
-
if value not in self.
|
|
574
|
+
if value not in self.dropdown_opts:
|
|
578
575
|
return ParseError(
|
|
579
576
|
f"Query parameter for {self.option} must be one of"
|
|
580
|
-
f" {self.
|
|
577
|
+
f" {list(self.dropdown_opts)!r}, got: {value!r}"
|
|
581
578
|
)
|
|
582
579
|
return ParseResult(value)
|
|
583
580
|
|
|
584
581
|
def strategy(self) -> st.SearchStrategy:
|
|
585
582
|
return st.sampled_from(
|
|
586
|
-
[None, *self.
|
|
583
|
+
[None, *self.dropdown_opts.keys()]
|
|
584
|
+
if self.supports_none
|
|
585
|
+
else list(self.dropdown_opts.keys())
|
|
587
586
|
)
|
|
588
587
|
|
|
589
588
|
def format_usage_parts(self) -> list[str]:
|
|
@@ -592,7 +591,7 @@ class DropdownControl(InputControl):
|
|
|
592
591
|
return [f"[{self.option} {self._values_text()}]"]
|
|
593
592
|
|
|
594
593
|
def _values_text(self) -> str:
|
|
595
|
-
return "{" + "|".join(self.
|
|
594
|
+
return "{" + "|".join(self.dropdown_opts) + "}"
|
|
596
595
|
|
|
597
596
|
def format_help_lines(self) -> list[str]:
|
|
598
597
|
line = f" {self.option} {self._values_text()}: {self.help_text}"
|
|
@@ -614,13 +613,16 @@ class DropdownControl(InputControl):
|
|
|
614
613
|
def format_query_value(self, value: typing.Any) -> str | None:
|
|
615
614
|
if value == self.default:
|
|
616
615
|
return None
|
|
617
|
-
return
|
|
616
|
+
return next(
|
|
617
|
+
(k for k, v in self.dropdown_opts.items() if v == value),
|
|
618
|
+
"" if value is None else str(value),
|
|
619
|
+
)
|
|
618
620
|
|
|
619
621
|
def prompt_interactive(
|
|
620
622
|
self, effective_default: typing.Any = _UNSET
|
|
621
623
|
) -> dict[str, str | None]:
|
|
622
624
|
d = self.default if effective_default is _UNSET else effective_default
|
|
623
|
-
choices = (["none"] if self.supports_none else [])
|
|
625
|
+
choices = [*(["none"] if self.supports_none else []), *self.dropdown_opts]
|
|
624
626
|
for i, v in enumerate(choices, 1):
|
|
625
627
|
print(f" {i}) {v}")
|
|
626
628
|
default_display = f" [{d if d is not None else 'none'}]"
|
|
@@ -634,12 +636,12 @@ class DropdownControl(InputControl):
|
|
|
634
636
|
idx = int(response) - 1
|
|
635
637
|
select_none = self.supports_none and idx == 0
|
|
636
638
|
chosen = choices[idx]
|
|
637
|
-
elif response in self.
|
|
639
|
+
elif response in self.dropdown_opts:
|
|
638
640
|
chosen = response
|
|
639
641
|
elif (
|
|
640
642
|
response.lower() == "none"
|
|
641
643
|
and self.supports_none
|
|
642
|
-
and "none" not in self.
|
|
644
|
+
and "none" not in self.dropdown_opts
|
|
643
645
|
):
|
|
644
646
|
select_none = True
|
|
645
647
|
else:
|
|
@@ -80,9 +80,7 @@ class QueryParams:
|
|
|
80
80
|
def _is_user_key(self, key: str) -> bool:
|
|
81
81
|
if key == "file":
|
|
82
82
|
return False
|
|
83
|
-
|
|
84
|
-
return True
|
|
85
|
-
return key.startswith(f"{self.prefix}.")
|
|
83
|
+
return not self.prefix or key.startswith(f"{self.prefix}.")
|
|
86
84
|
|
|
87
85
|
def _set(self, key: str, value: str | None) -> None:
|
|
88
86
|
params = self.params
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import marimo as mo
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _RunButtonStub:
|
|
7
|
+
"""Returned by run_button() outside notebooks; always considered clicked."""
|
|
8
|
+
|
|
9
|
+
value: bool = True
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_button(**kwargs: typing.Any) -> mo.ui.run_button | _RunButtonStub:
|
|
13
|
+
"""Create a run button that gates notebook execution.
|
|
14
|
+
|
|
15
|
+
In CLI context, always returns a stub with .value = True so code that
|
|
16
|
+
checks `mo.stop(not btn.value)` runs unconditionally.
|
|
17
|
+
"""
|
|
18
|
+
if mo.running_in_notebook():
|
|
19
|
+
return mo.ui.run_button(**kwargs)
|
|
20
|
+
return _RunButtonStub()
|
|
@@ -3,6 +3,8 @@ import typing
|
|
|
3
3
|
|
|
4
4
|
import marimo as mo
|
|
5
5
|
|
|
6
|
+
from . import interface
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
class _Embed(typing.Protocol):
|
|
8
10
|
defs: typing.Mapping[str, typing.Any]
|
|
@@ -50,7 +52,9 @@ class Passthrough:
|
|
|
50
52
|
def __init__(self, source: _Embed | dict[str, typing.Any]) -> None:
|
|
51
53
|
self.defs = {
|
|
52
54
|
"result": (source if isinstance(source, dict) else source.defs)["result"],
|
|
53
|
-
"interface":
|
|
55
|
+
"interface": interface.Interface(
|
|
56
|
+
controls=typing.cast(tuple[typing.Any], ())
|
|
57
|
+
),
|
|
54
58
|
}
|
|
55
59
|
self.output = None
|
|
56
60
|
|
|
@@ -11,6 +11,7 @@ import weakref
|
|
|
11
11
|
import marimo as mo
|
|
12
12
|
|
|
13
13
|
from . import _input_map, _markdown, _naming, _options, _parse, _query_params, interface
|
|
14
|
+
from ._run_button import run_button
|
|
14
15
|
from .interface import FileBrowserWithInitialSelection
|
|
15
16
|
from .presets import Presets
|
|
16
17
|
|
|
@@ -76,6 +77,17 @@ class Group:
|
|
|
76
77
|
"""
|
|
77
78
|
if markdown_heading_offset < 0:
|
|
78
79
|
raise ValueError("markdown_heading_offset must be non-negative")
|
|
80
|
+
_frame = inspect.currentframe()
|
|
81
|
+
_caller = _frame.f_back if _frame else None
|
|
82
|
+
if _caller is not None and bool(_caller.f_code.co_flags & inspect.CO_COROUTINE):
|
|
83
|
+
warnings.warn(
|
|
84
|
+
f"args.subgroup('{prefix}') called inside an async cell, "
|
|
85
|
+
"likely the cell making the embed it is used for. "
|
|
86
|
+
"Each cell re-run creates a new Group object, "
|
|
87
|
+
"which causes the embedded notebook to reload and lose widget state. "
|
|
88
|
+
"Move args.subgroup() to a separate sync cell instead.",
|
|
89
|
+
stacklevel=2,
|
|
90
|
+
)
|
|
79
91
|
child = type(self)([prefix])
|
|
80
92
|
child._state = self._state
|
|
81
93
|
child._cli_map = _input_map.InputMap()
|
|
@@ -509,6 +521,17 @@ class Group:
|
|
|
509
521
|
)
|
|
510
522
|
return self._cli_map.register(wrapped, cli)
|
|
511
523
|
|
|
524
|
+
@staticmethod
|
|
525
|
+
def run_button(
|
|
526
|
+
**kwargs: typing.Any,
|
|
527
|
+
):
|
|
528
|
+
"""Create a run button that gates notebook execution.
|
|
529
|
+
|
|
530
|
+
In CLI context, always returns a stub with .value = True so code that
|
|
531
|
+
checks `mo.stop(not btn.value)` runs unconditionally.
|
|
532
|
+
"""
|
|
533
|
+
return run_button(**kwargs)
|
|
534
|
+
|
|
512
535
|
def _numeric_cli(
|
|
513
536
|
self,
|
|
514
537
|
start: float | None,
|
|
@@ -544,20 +567,22 @@ class Group:
|
|
|
544
567
|
|
|
545
568
|
assert len(options) > 0, "Dropdown options cannot be empty"
|
|
546
569
|
opt = self._make_opt(label=label, option=option)
|
|
547
|
-
keys = list(options)
|
|
548
570
|
if value is None and not allow_select_none:
|
|
549
|
-
value, *_ =
|
|
571
|
+
value, *_ = [*options]
|
|
550
572
|
cli = _options.DropdownControl(
|
|
551
573
|
option=opt.option,
|
|
552
|
-
|
|
574
|
+
dropdown_opts=options
|
|
575
|
+
if isinstance(options, dict)
|
|
576
|
+
else {opt: opt for opt in options},
|
|
553
577
|
supports_none=allow_select_none,
|
|
554
578
|
default=value,
|
|
555
579
|
help_text=help_text,
|
|
556
580
|
)
|
|
557
581
|
if self._is_overridden(opt.option):
|
|
558
|
-
# mo.ui.dropdown doesn't support disabled;
|
|
559
|
-
#
|
|
560
|
-
#
|
|
582
|
+
# mo.ui.dropdown doesn't support disabled;
|
|
583
|
+
# (see https://github.com/marimo-team/marimo/issues/9579)
|
|
584
|
+
# So we filter to one option as a workaround
|
|
585
|
+
# so the user can't change the value.
|
|
561
586
|
override = self._overrides[self._override_key(opt.option)]
|
|
562
587
|
options = (
|
|
563
588
|
{override: None if override is None else options[override]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|