moops 0.7.0__tar.gz → 0.7.2__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.7.0 → moops-0.7.2}/PKG-INFO +1 -1
- {moops-0.7.0 → moops-0.7.2}/pyproject.toml +1 -1
- moops-0.7.2/src/moops/_choice_options.py +24 -0
- moops-0.7.2/src/moops/_control_mirroring.py +85 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_naming.py +4 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_options.py +17 -18
- {moops-0.7.0 → moops-0.7.2}/src/moops/group.py +43 -180
- {moops-0.7.0 → moops-0.7.2}/src/moops/interface.py +65 -17
- moops-0.7.0/src/moops/_control_factory.py +0 -19
- {moops-0.7.0 → moops-0.7.2}/README.md +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/__init__.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_embed.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_input_map.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_markdown.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_parse.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_preset_state.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_query_params.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_run.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_run_button.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_ui_workarounds.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_value_resolution.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/_variant.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/presets.py +0 -0
- {moops-0.7.0 → moops-0.7.2}/src/moops/workarounds.py +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def option_values(
|
|
7
|
+
options: typing.Sequence[typing.Any] | dict[str, typing.Any],
|
|
8
|
+
) -> dict[str, typing.Any]:
|
|
9
|
+
if isinstance(options, dict):
|
|
10
|
+
return dict(options)
|
|
11
|
+
return {str(opt): opt for opt in options}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def option_key(options: dict[str, typing.Any], value: typing.Any) -> str:
|
|
15
|
+
if isinstance(value, str) and value in options:
|
|
16
|
+
return value
|
|
17
|
+
return next(
|
|
18
|
+
(key for key, option_value in options.items() if value == option_value),
|
|
19
|
+
str(value),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def option_value(options: dict[str, typing.Any], value: typing.Any) -> typing.Any:
|
|
24
|
+
return options[value] if isinstance(value, str) and value in options else value
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
import marimo as mo
|
|
7
|
+
|
|
8
|
+
from . import _options, interface
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def controls_from(
|
|
12
|
+
group: typing.Any,
|
|
13
|
+
iface: interface.Interface,
|
|
14
|
+
*,
|
|
15
|
+
prefix: str,
|
|
16
|
+
exclude: typing.Iterable[str] = (),
|
|
17
|
+
) -> mo.ui.dictionary:
|
|
18
|
+
"""Create a subgroup of controls mirroring another notebook's interface."""
|
|
19
|
+
child = group.subgroup(prefix)
|
|
20
|
+
excluded = set(exclude)
|
|
21
|
+
controls: dict[str, typing.Any] = {
|
|
22
|
+
name: (
|
|
23
|
+
controls_from(child, ctrl_or_sub, prefix=name)
|
|
24
|
+
if isinstance(ctrl_or_sub, interface.Interface)
|
|
25
|
+
else _create_control(child, iface, ctrl_or_sub)
|
|
26
|
+
)
|
|
27
|
+
for name, ctrl_or_sub in iface.iter_controls()
|
|
28
|
+
if name not in excluded
|
|
29
|
+
}
|
|
30
|
+
result = mo.ui.dictionary(controls)
|
|
31
|
+
# mo.ui.dictionary clones its elements, so result.elements[key] is a
|
|
32
|
+
# different object than controls[key]. Rebind nested dictionary clones
|
|
33
|
+
# to interfaces that track their own live cloned elements.
|
|
34
|
+
for key, original in controls.items():
|
|
35
|
+
_reattach_interface_to_clone(original, result.elements[key])
|
|
36
|
+
# Use the live clones (result.elements) rather than the originals so
|
|
37
|
+
# that cur_values() reads up-to-date widget values.
|
|
38
|
+
result._moops_interface = child.interface(*result.elements.values()) # type: ignore[attr-defined]
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _create_control(
|
|
43
|
+
group: typing.Any,
|
|
44
|
+
iface: interface.Interface,
|
|
45
|
+
input_control: _options.InputControl,
|
|
46
|
+
) -> typing.Any:
|
|
47
|
+
return _create_from_input_control(
|
|
48
|
+
group, input_control, _unprefixed_option(iface, input_control.option)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _create_from_input_control(
|
|
53
|
+
group: typing.Any,
|
|
54
|
+
input_control: _options.InputControl,
|
|
55
|
+
display_option: str,
|
|
56
|
+
) -> typing.Any:
|
|
57
|
+
"""Create a marimo element from an existing InputControl."""
|
|
58
|
+
opt = group._make_opt(label=None, option=display_option)
|
|
59
|
+
cloned = dataclasses.replace(input_control, option=opt.option)
|
|
60
|
+
return group._register_control(opt, cloned, cloned.help_text, None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _unprefixed_option(iface: interface.Interface, option: str) -> str:
|
|
64
|
+
if iface.option_prefix and option.startswith(f"{iface.option_prefix}-"):
|
|
65
|
+
return f"--{option[len(iface.option_prefix) :].lstrip('-')}"
|
|
66
|
+
return option
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _reattach_interface_to_clone(original: typing.Any, clone: typing.Any) -> None:
|
|
70
|
+
moops_iface = getattr(original, "_moops_interface", None)
|
|
71
|
+
if not isinstance(moops_iface, interface.Interface):
|
|
72
|
+
return
|
|
73
|
+
original_elements = getattr(original, "elements", None)
|
|
74
|
+
clone_elements = getattr(clone, "elements", None)
|
|
75
|
+
if isinstance(original_elements, dict) and isinstance(clone_elements, dict):
|
|
76
|
+
typed_original_elements = typing.cast(dict[str, typing.Any], original_elements)
|
|
77
|
+
typed_clone_elements = typing.cast(dict[str, typing.Any], clone_elements)
|
|
78
|
+
for key, original_child in typed_original_elements.items():
|
|
79
|
+
if key in typed_clone_elements:
|
|
80
|
+
_reattach_interface_to_clone(original_child, typed_clone_elements[key])
|
|
81
|
+
controls = tuple(typed_clone_elements.values())
|
|
82
|
+
else:
|
|
83
|
+
controls = moops_iface.controls
|
|
84
|
+
moops_iface.controls = controls
|
|
85
|
+
clone._moops_interface = moops_iface
|
|
@@ -27,6 +27,10 @@ class OptionLabel:
|
|
|
27
27
|
label = option.lstrip("-").replace("-", " ")
|
|
28
28
|
return OptionLabel(label=label, option=option)
|
|
29
29
|
|
|
30
|
+
@property
|
|
31
|
+
def metavar(self) -> str:
|
|
32
|
+
return self.label.upper().replace(" ", "_")
|
|
33
|
+
|
|
30
34
|
def label_with_tooltip(self, help_text: str) -> str:
|
|
31
35
|
return (
|
|
32
36
|
f'<span title="{html.escape(help_text, quote=True)} ({self.option})">'
|
|
@@ -10,7 +10,7 @@ import typing
|
|
|
10
10
|
import marimo as mo
|
|
11
11
|
from hypothesis import strategies as st
|
|
12
12
|
|
|
13
|
-
from . import _parse, _ui_workarounds
|
|
13
|
+
from . import _choice_options, _parse, _ui_workarounds
|
|
14
14
|
|
|
15
15
|
Numeric = int | float
|
|
16
16
|
|
|
@@ -33,6 +33,7 @@ class InputControl(abc.ABC):
|
|
|
33
33
|
|
|
34
34
|
option: str
|
|
35
35
|
help_text: str
|
|
36
|
+
default: typing.Any
|
|
36
37
|
extra_kwargs: dict[str, typing.Any] = dataclasses.field( # type: ignore[assignment]
|
|
37
38
|
default_factory=dict, kw_only=True
|
|
38
39
|
)
|
|
@@ -734,7 +735,9 @@ class MultiSelectControl(ValueControl):
|
|
|
734
735
|
on_change: typing.Callable[[typing.Any], None] | None = None,
|
|
735
736
|
disabled: bool = False,
|
|
736
737
|
) -> typing.Any:
|
|
737
|
-
selected_keys = [
|
|
738
|
+
selected_keys = [
|
|
739
|
+
_choice_options.option_key(self.select_opts, item) for item in value
|
|
740
|
+
]
|
|
738
741
|
if disabled:
|
|
739
742
|
return _ui_workarounds.LockedMultiselect(
|
|
740
743
|
[str(item) for item in value], label
|
|
@@ -821,7 +824,10 @@ class MultiSelectControl(ValueControl):
|
|
|
821
824
|
if self.default:
|
|
822
825
|
line += (
|
|
823
826
|
" (default: "
|
|
824
|
-
+ ", ".join(
|
|
827
|
+
+ ", ".join(
|
|
828
|
+
_choice_options.option_key(self.select_opts, v)
|
|
829
|
+
for v in self.default
|
|
830
|
+
)
|
|
825
831
|
+ ")"
|
|
826
832
|
)
|
|
827
833
|
line += f" (repeat {self.option} to select multiple)"
|
|
@@ -836,21 +842,20 @@ class MultiSelectControl(ValueControl):
|
|
|
836
842
|
return []
|
|
837
843
|
if not values and self._no_flag:
|
|
838
844
|
return [self._no_flag]
|
|
839
|
-
return [
|
|
840
|
-
f"{self.option} {shlex.quote(_option_key(self.select_opts, v))}"
|
|
841
|
-
for v in values
|
|
842
|
-
]
|
|
845
|
+
return [f"{self.option} {shlex.quote(self._key_for(v))}" for v in values]
|
|
843
846
|
|
|
844
847
|
def format_query_value(self, value: typing.Any) -> str | None:
|
|
845
848
|
values = list(value)
|
|
846
|
-
keys = [
|
|
849
|
+
keys = [_choice_options.option_key(self.select_opts, v) for v in values]
|
|
847
850
|
return None if values == self.default else json.dumps(keys)
|
|
848
851
|
|
|
849
852
|
def prompt_interactive(
|
|
850
853
|
self, effective_default: typing.Any = _UNSET
|
|
851
854
|
) -> dict[str, str | None]:
|
|
852
855
|
d = self.default if effective_default is _UNSET else effective_default
|
|
853
|
-
default_keys = {
|
|
856
|
+
default_keys = {
|
|
857
|
+
_choice_options.option_key(self.select_opts, v) for v in (d or [])
|
|
858
|
+
}
|
|
854
859
|
for i, v in enumerate(self.select_opts, 1):
|
|
855
860
|
mark = "*" if v in default_keys else " "
|
|
856
861
|
print(f" {mark}{i}) {v}")
|
|
@@ -861,6 +866,9 @@ class MultiSelectControl(ValueControl):
|
|
|
861
866
|
parts = [p.strip() for p in response.split(",") if p.strip()]
|
|
862
867
|
return {self.option: "\n".join(parts)}
|
|
863
868
|
|
|
869
|
+
def _key_for(self, value: typing.Any) -> str:
|
|
870
|
+
return _choice_options.option_key(self.select_opts, value)
|
|
871
|
+
|
|
864
872
|
|
|
865
873
|
@dataclasses.dataclass
|
|
866
874
|
class DropdownControl(InputControl):
|
|
@@ -1037,14 +1045,5 @@ def _range_stop(
|
|
|
1037
1045
|
return max(steps) if steps else stop
|
|
1038
1046
|
|
|
1039
1047
|
|
|
1040
|
-
def _option_key(options: dict[str, typing.Any], value: typing.Any) -> str:
|
|
1041
|
-
if isinstance(value, str) and value in options:
|
|
1042
|
-
return value
|
|
1043
|
-
for key, option_value in options.items():
|
|
1044
|
-
if value == option_value:
|
|
1045
|
-
return key
|
|
1046
|
-
return str(value)
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
1048
|
def _format_range(value: typing.Iterable[typing.Any]) -> str:
|
|
1050
1049
|
return ",".join(str(v) for v in value)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import dataclasses
|
|
4
3
|
import inspect
|
|
5
4
|
import pathlib
|
|
6
5
|
import typing
|
|
@@ -9,7 +8,8 @@ import warnings
|
|
|
9
8
|
import marimo as mo
|
|
10
9
|
|
|
11
10
|
from . import (
|
|
12
|
-
|
|
11
|
+
_choice_options,
|
|
12
|
+
_control_mirroring,
|
|
13
13
|
_input_map,
|
|
14
14
|
_markdown,
|
|
15
15
|
_naming,
|
|
@@ -276,13 +276,7 @@ class Group:
|
|
|
276
276
|
widget="switch",
|
|
277
277
|
extra_kwargs=kwargs,
|
|
278
278
|
)
|
|
279
|
-
return self.
|
|
280
|
-
input_control.create_marimo_element(
|
|
281
|
-
self._get_value(input_control, value),
|
|
282
|
-
**self._control_kwargs(opt, input_control, help_text, on_change),
|
|
283
|
-
),
|
|
284
|
-
input_control,
|
|
285
|
-
)
|
|
279
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
286
280
|
|
|
287
281
|
def checkbox(
|
|
288
282
|
self,
|
|
@@ -304,13 +298,7 @@ class Group:
|
|
|
304
298
|
widget="checkbox",
|
|
305
299
|
extra_kwargs=kwargs,
|
|
306
300
|
)
|
|
307
|
-
return self.
|
|
308
|
-
input_control.create_marimo_element(
|
|
309
|
-
self._get_value(input_control, value),
|
|
310
|
-
**self._control_kwargs(opt, input_control, help_text, on_change),
|
|
311
|
-
),
|
|
312
|
-
input_control,
|
|
313
|
-
)
|
|
301
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
314
302
|
|
|
315
303
|
def text(
|
|
316
304
|
self,
|
|
@@ -335,18 +323,12 @@ class Group:
|
|
|
335
323
|
opt = self._make_opt(label=label, option=option)
|
|
336
324
|
input_control = _options.TextControl(
|
|
337
325
|
option=opt.option,
|
|
338
|
-
metavar=placeholder or opt.
|
|
326
|
+
metavar=placeholder or opt.metavar,
|
|
339
327
|
help_text=help_text,
|
|
340
328
|
default=value,
|
|
341
329
|
extra_kwargs=kwargs,
|
|
342
330
|
)
|
|
343
|
-
return self.
|
|
344
|
-
input_control.create_marimo_element(
|
|
345
|
-
self._get_value(input_control, value),
|
|
346
|
-
**self._control_kwargs(opt, input_control, help_text, on_change),
|
|
347
|
-
),
|
|
348
|
-
input_control,
|
|
349
|
-
)
|
|
331
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
350
332
|
|
|
351
333
|
def text_area(
|
|
352
334
|
self,
|
|
@@ -366,18 +348,12 @@ class Group:
|
|
|
366
348
|
opt = self._make_opt(label=label, option=option)
|
|
367
349
|
input_control = _options.TextAreaControl(
|
|
368
350
|
option=opt.option,
|
|
369
|
-
metavar=placeholder or opt.
|
|
351
|
+
metavar=placeholder or opt.metavar,
|
|
370
352
|
help_text=help_text,
|
|
371
353
|
default=value,
|
|
372
354
|
extra_kwargs=kwargs,
|
|
373
355
|
)
|
|
374
|
-
return self.
|
|
375
|
-
input_control.create_marimo_element(
|
|
376
|
-
self._get_value(input_control, value),
|
|
377
|
-
**self._control_kwargs(opt, input_control, help_text, on_change),
|
|
378
|
-
),
|
|
379
|
-
input_control,
|
|
380
|
-
)
|
|
356
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
381
357
|
|
|
382
358
|
def file_browser(
|
|
383
359
|
self,
|
|
@@ -423,13 +399,7 @@ class Group:
|
|
|
423
399
|
help_text=help_text,
|
|
424
400
|
extra_kwargs=kwargs,
|
|
425
401
|
)
|
|
426
|
-
return self.
|
|
427
|
-
input_control.create_marimo_element(
|
|
428
|
-
self._get_value(input_control, default),
|
|
429
|
-
**self._control_kwargs(opt, input_control, help_text, on_change),
|
|
430
|
-
),
|
|
431
|
-
input_control,
|
|
432
|
-
)
|
|
402
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
433
403
|
|
|
434
404
|
def number(
|
|
435
405
|
self,
|
|
@@ -448,15 +418,10 @@ class Group:
|
|
|
448
418
|
"""Create a number input UI element that maps to a CLI option."""
|
|
449
419
|
|
|
450
420
|
kwargs = {"step": step, "debounce": debounce, **kwargs}
|
|
451
|
-
opt, input_control
|
|
421
|
+
opt, input_control = self._numeric_input_control(
|
|
452
422
|
start, stop, value, option, help_text, label, "number", kwargs
|
|
453
423
|
)
|
|
454
|
-
return self.
|
|
455
|
-
input_control.create_marimo_element(
|
|
456
|
-
value, **self._control_kwargs(opt, input_control, help_text, on_change)
|
|
457
|
-
),
|
|
458
|
-
input_control,
|
|
459
|
-
)
|
|
424
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
460
425
|
|
|
461
426
|
def slider(
|
|
462
427
|
self,
|
|
@@ -475,15 +440,10 @@ class Group:
|
|
|
475
440
|
"""Create a slider UI element that maps to a CLI option."""
|
|
476
441
|
|
|
477
442
|
kwargs = {"step": step, "debounce": debounce, **kwargs}
|
|
478
|
-
opt, input_control
|
|
443
|
+
opt, input_control = self._numeric_input_control(
|
|
479
444
|
start, stop, value, option, help_text, label, "slider", kwargs
|
|
480
445
|
)
|
|
481
|
-
return self.
|
|
482
|
-
input_control.create_marimo_element(
|
|
483
|
-
value, **self._control_kwargs(opt, input_control, help_text, on_change)
|
|
484
|
-
),
|
|
485
|
-
input_control,
|
|
486
|
-
)
|
|
446
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
487
447
|
|
|
488
448
|
def range_slider(
|
|
489
449
|
self,
|
|
@@ -513,7 +473,7 @@ class Group:
|
|
|
513
473
|
opt = self._make_opt(label=label, option=option)
|
|
514
474
|
input_control = _options.RangeControl.from_slider(
|
|
515
475
|
option=opt.option,
|
|
516
|
-
metavar=opt.
|
|
476
|
+
metavar=opt.metavar,
|
|
517
477
|
help_text=help_text,
|
|
518
478
|
start=start,
|
|
519
479
|
stop=stop,
|
|
@@ -522,13 +482,7 @@ class Group:
|
|
|
522
482
|
step=step,
|
|
523
483
|
extra_kwargs=kwargs,
|
|
524
484
|
)
|
|
525
|
-
return self.
|
|
526
|
-
input_control.create_marimo_element(
|
|
527
|
-
self._get_value(input_control, input_control.default),
|
|
528
|
-
**self._control_kwargs(opt, input_control, help_text, on_change),
|
|
529
|
-
),
|
|
530
|
-
input_control,
|
|
531
|
-
)
|
|
485
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
532
486
|
|
|
533
487
|
def custom(
|
|
534
488
|
self,
|
|
@@ -582,13 +536,13 @@ class Group:
|
|
|
582
536
|
label: str | None,
|
|
583
537
|
widget: typing.Literal["number", "slider"] = "number",
|
|
584
538
|
extra_kwargs: dict[str, typing.Any] | None = None,
|
|
585
|
-
) -> tuple[_naming.OptionLabel, _options.NumberControl
|
|
539
|
+
) -> tuple[_naming.OptionLabel, _options.NumberControl]:
|
|
586
540
|
if value is None:
|
|
587
541
|
value = start
|
|
588
542
|
opt = self._make_opt(label=label, option=option)
|
|
589
543
|
input_control = _options.NumberControl(
|
|
590
544
|
option=opt.option,
|
|
591
|
-
metavar=opt.
|
|
545
|
+
metavar=opt.metavar,
|
|
592
546
|
help_text=help_text,
|
|
593
547
|
default=value,
|
|
594
548
|
start=start,
|
|
@@ -596,7 +550,7 @@ class Group:
|
|
|
596
550
|
widget=widget,
|
|
597
551
|
extra_kwargs=extra_kwargs or {},
|
|
598
552
|
)
|
|
599
|
-
return opt, input_control
|
|
553
|
+
return opt, input_control
|
|
600
554
|
|
|
601
555
|
def dropdown(
|
|
602
556
|
self,
|
|
@@ -618,8 +572,12 @@ class Group:
|
|
|
618
572
|
|
|
619
573
|
assert len(options) > 0, "Dropdown options cannot be empty"
|
|
620
574
|
opt = self._make_opt(label=label, option=option)
|
|
621
|
-
dropdown_opts =
|
|
622
|
-
value =
|
|
575
|
+
dropdown_opts = _choice_options.option_values(options)
|
|
576
|
+
value = (
|
|
577
|
+
_choice_options.option_key(dropdown_opts, value)
|
|
578
|
+
if value is not None
|
|
579
|
+
else None
|
|
580
|
+
)
|
|
623
581
|
if value is None and not allow_select_none:
|
|
624
582
|
value, *_ = [*dropdown_opts]
|
|
625
583
|
input_control = _options.DropdownControl(
|
|
@@ -634,13 +592,7 @@ class Group:
|
|
|
634
592
|
**kwargs,
|
|
635
593
|
},
|
|
636
594
|
)
|
|
637
|
-
return self.
|
|
638
|
-
input_control.create_marimo_element(
|
|
639
|
-
self._get_value(input_control, value),
|
|
640
|
-
**self._control_kwargs(opt, input_control, help_text, on_change),
|
|
641
|
-
),
|
|
642
|
-
input_control,
|
|
643
|
-
)
|
|
595
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
644
596
|
|
|
645
597
|
def multiselect(
|
|
646
598
|
self,
|
|
@@ -657,23 +609,17 @@ class Group:
|
|
|
657
609
|
if value is None:
|
|
658
610
|
value = []
|
|
659
611
|
opt = self._make_opt(label=label, option=option)
|
|
660
|
-
select_opts =
|
|
661
|
-
default = [
|
|
612
|
+
select_opts = _choice_options.option_values(options)
|
|
613
|
+
default = [_choice_options.option_value(select_opts, item) for item in value]
|
|
662
614
|
input_control = _options.MultiSelectControl(
|
|
663
615
|
option=opt.option,
|
|
664
|
-
metavar=opt.
|
|
616
|
+
metavar=opt.metavar,
|
|
665
617
|
help_text=help_text,
|
|
666
618
|
default=default,
|
|
667
619
|
select_opts=select_opts,
|
|
668
620
|
extra_kwargs=kwargs,
|
|
669
621
|
)
|
|
670
|
-
return self.
|
|
671
|
-
input_control.create_marimo_element(
|
|
672
|
-
self._get_value(input_control, default),
|
|
673
|
-
**self._control_kwargs(opt, input_control, help_text, on_change),
|
|
674
|
-
),
|
|
675
|
-
input_control,
|
|
676
|
-
)
|
|
622
|
+
return self._register_control(opt, input_control, help_text, on_change)
|
|
677
623
|
|
|
678
624
|
def controls_from(
|
|
679
625
|
self,
|
|
@@ -689,27 +635,9 @@ class Group:
|
|
|
689
635
|
The controls themselves are created in a subgroup, so their CLI options
|
|
690
636
|
are prefixed in the parent notebook.
|
|
691
637
|
"""
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
name: (
|
|
696
|
-
child.controls_from(ctrl_or_sub, prefix=name)
|
|
697
|
-
if isinstance(ctrl_or_sub, interface.Interface)
|
|
698
|
-
else _control_factory.create_control(child, iface, ctrl_or_sub)
|
|
699
|
-
)
|
|
700
|
-
for name, ctrl_or_sub in iface.iter_controls()
|
|
701
|
-
if name not in excluded
|
|
702
|
-
}
|
|
703
|
-
result = mo.ui.dictionary(controls)
|
|
704
|
-
# mo.ui.dictionary clones its elements, so result.elements[key] is a
|
|
705
|
-
# different object than controls[key]. Rebind nested dictionary clones
|
|
706
|
-
# to interfaces that track their own live cloned elements.
|
|
707
|
-
for key, original in controls.items():
|
|
708
|
-
_reattach_interface_to_clone(original, result.elements[key])
|
|
709
|
-
# Use the live clones (result.elements) rather than the originals so
|
|
710
|
-
# that cur_values() reads up-to-date widget values.
|
|
711
|
-
result._moops_interface = child.interface(*result.elements.values()) # type: ignore[attr-defined]
|
|
712
|
-
return result
|
|
638
|
+
return _control_mirroring.controls_from(
|
|
639
|
+
self, iface, prefix=prefix, exclude=exclude
|
|
640
|
+
)
|
|
713
641
|
|
|
714
642
|
def _make_value_resolver(self) -> _value_resolution.ValueResolver:
|
|
715
643
|
return _value_resolution.ValueResolver(
|
|
@@ -721,49 +649,24 @@ class Group:
|
|
|
721
649
|
default_preset_state=self._preset_state.default,
|
|
722
650
|
)
|
|
723
651
|
|
|
724
|
-
def
|
|
652
|
+
def _register_control(
|
|
725
653
|
self,
|
|
726
654
|
opt: _naming.OptionLabel,
|
|
727
655
|
input_control: _options.InputControl,
|
|
728
656
|
help_text: str,
|
|
729
657
|
on_change: typing.Callable[[typing.Any], None] | None,
|
|
730
|
-
) -> dict[str, typing.Any]:
|
|
731
|
-
return {
|
|
732
|
-
"label": opt.label_with_tooltip(help_text),
|
|
733
|
-
"disabled": self._is_overridden(opt.option) or self._disabled,
|
|
734
|
-
"on_change": self._query_on_change(input_control, on_change),
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
def _get_value(
|
|
738
|
-
self,
|
|
739
|
-
control: _options.InputControl,
|
|
740
|
-
default: typing.Any,
|
|
741
658
|
) -> typing.Any:
|
|
742
|
-
return self._value_resolver.get_value(control, default)
|
|
743
|
-
|
|
744
|
-
def _is_overridden(self, option: str) -> bool:
|
|
745
|
-
return self._value_resolver.is_overridden(option)
|
|
746
|
-
|
|
747
|
-
def _query_on_change(
|
|
748
|
-
self,
|
|
749
|
-
control: _options.InputControl,
|
|
750
|
-
on_change: typing.Callable[[typing.Any], None] | None,
|
|
751
|
-
) -> typing.Callable[[typing.Any], None] | None:
|
|
752
|
-
return self._value_resolver.query_on_change(control, on_change)
|
|
753
|
-
|
|
754
|
-
def _create_from_input_control(
|
|
755
|
-
self,
|
|
756
|
-
input_control: _options.InputControl,
|
|
757
|
-
display_option: str,
|
|
758
|
-
) -> typing.Any:
|
|
759
|
-
"""Create a marimo element from an existing InputControl."""
|
|
760
|
-
opt = self._make_opt(label=None, option=display_option)
|
|
761
|
-
cloned = dataclasses.replace(input_control, option=opt.option)
|
|
762
|
-
value = self._get_value(cloned, getattr(cloned, "default", None))
|
|
763
|
-
ctrl_kwargs = self._control_kwargs(opt, cloned, cloned.help_text, None)
|
|
764
659
|
return self._input_map.register(
|
|
765
|
-
|
|
766
|
-
|
|
660
|
+
input_control.create_marimo_element(
|
|
661
|
+
self._value_resolver.get_value(input_control, input_control.default),
|
|
662
|
+
label=opt.label_with_tooltip(help_text),
|
|
663
|
+
disabled=self._value_resolver.is_overridden(opt.option)
|
|
664
|
+
or self._disabled,
|
|
665
|
+
on_change=self._value_resolver.query_on_change(
|
|
666
|
+
input_control, on_change
|
|
667
|
+
),
|
|
668
|
+
),
|
|
669
|
+
input_control,
|
|
767
670
|
)
|
|
768
671
|
|
|
769
672
|
def _make_opt(
|
|
@@ -776,43 +679,3 @@ class Group:
|
|
|
776
679
|
option=f"{self.option}-{opt.option.lstrip('-')}",
|
|
777
680
|
)
|
|
778
681
|
return opt
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
def _option_values(
|
|
782
|
-
options: typing.Sequence[typing.Any] | dict[str, typing.Any],
|
|
783
|
-
) -> dict[str, typing.Any]:
|
|
784
|
-
if isinstance(options, dict):
|
|
785
|
-
return dict(options)
|
|
786
|
-
return {str(opt): opt for opt in options}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
def _reattach_interface_to_clone(original: typing.Any, clone: typing.Any) -> None:
|
|
790
|
-
moops_iface = getattr(original, "_moops_interface", None)
|
|
791
|
-
if not isinstance(moops_iface, interface.Interface):
|
|
792
|
-
return
|
|
793
|
-
original_elements = getattr(original, "elements", None)
|
|
794
|
-
clone_elements = getattr(clone, "elements", None)
|
|
795
|
-
if isinstance(original_elements, dict) and isinstance(clone_elements, dict):
|
|
796
|
-
typed_original_elements = typing.cast(dict[str, typing.Any], original_elements)
|
|
797
|
-
typed_clone_elements = typing.cast(dict[str, typing.Any], clone_elements)
|
|
798
|
-
for key, original_child in typed_original_elements.items():
|
|
799
|
-
if key in typed_clone_elements:
|
|
800
|
-
_reattach_interface_to_clone(original_child, typed_clone_elements[key])
|
|
801
|
-
controls = tuple(typed_clone_elements.values())
|
|
802
|
-
else:
|
|
803
|
-
controls = moops_iface.controls
|
|
804
|
-
moops_iface.controls = controls
|
|
805
|
-
clone._moops_interface = moops_iface
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
def _option_key(options: dict[str, typing.Any], value: typing.Any) -> str:
|
|
809
|
-
if isinstance(value, str) and value in options:
|
|
810
|
-
return value
|
|
811
|
-
return next(
|
|
812
|
-
(key for key, option_value in options.items() if value == option_value),
|
|
813
|
-
str(value),
|
|
814
|
-
)
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
def _option_value(options: dict[str, typing.Any], value: typing.Any) -> typing.Any:
|
|
818
|
-
return options[value] if isinstance(value, str) and value in options else value
|
|
@@ -13,6 +13,47 @@ from . import _input_map, _options, _parse, _query_params
|
|
|
13
13
|
from .presets import Presets
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _wrap_usage(prefix: str, parts: list[str], width: int = 88) -> str:
|
|
17
|
+
indent = " " * len(prefix)
|
|
18
|
+
lines: list[str] = []
|
|
19
|
+
current = prefix
|
|
20
|
+
first_on_line = True
|
|
21
|
+
for part in parts:
|
|
22
|
+
attempt = current + part if first_on_line else current + " " + part
|
|
23
|
+
if first_on_line or len(attempt) <= width:
|
|
24
|
+
current = attempt
|
|
25
|
+
first_on_line = False
|
|
26
|
+
else:
|
|
27
|
+
lines.append(current)
|
|
28
|
+
current = indent + part
|
|
29
|
+
lines.append(current)
|
|
30
|
+
return "\n".join(lines)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _wrap_help_line(line: str, width: int = 88) -> list[str]:
|
|
34
|
+
if len(line) <= width:
|
|
35
|
+
return [line]
|
|
36
|
+
sep = ": "
|
|
37
|
+
sep_idx = line.find(sep)
|
|
38
|
+
if sep_idx == -1:
|
|
39
|
+
return [line]
|
|
40
|
+
header = line[: sep_idx + 1] # e.g. " --option METAVAR:"
|
|
41
|
+
indent = " "
|
|
42
|
+
result = [header]
|
|
43
|
+
current = indent
|
|
44
|
+
first_on_line = True
|
|
45
|
+
for word in line[sep_idx + len(sep) :].split():
|
|
46
|
+
attempt = current + word if first_on_line else current + " " + word
|
|
47
|
+
if first_on_line or len(attempt) <= width:
|
|
48
|
+
current = attempt
|
|
49
|
+
first_on_line = False
|
|
50
|
+
else:
|
|
51
|
+
result.append(current)
|
|
52
|
+
current = indent + word
|
|
53
|
+
result.append(current)
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
16
57
|
@dataclasses.dataclass
|
|
17
58
|
class Interface:
|
|
18
59
|
"""Controls registered by a subgroup's interface, for passing to the parent."""
|
|
@@ -56,7 +97,7 @@ class Interface:
|
|
|
56
97
|
|
|
57
98
|
def has_prefixed_options(self, state: _parse.ParseState) -> bool:
|
|
58
99
|
"""True if state has CLI options starting with this interface's prefix."""
|
|
59
|
-
prefix = self.option_prefix
|
|
100
|
+
prefix = f"{self.option_prefix}-" if self.option_prefix else "--"
|
|
60
101
|
return any(
|
|
61
102
|
k
|
|
62
103
|
for k in state.args.options
|
|
@@ -95,33 +136,38 @@ class Interface:
|
|
|
95
136
|
usage_parts = list(self._format_usage_parts(_usage_placeholders(self)))
|
|
96
137
|
usage_parts.extend(("[--interactive]", "[-h/--help]"))
|
|
97
138
|
name = self.command.rsplit("/", 1)[-1]
|
|
98
|
-
|
|
139
|
+
prefix = f"Usage: {name} "
|
|
140
|
+
segments = [_wrap_usage(prefix, usage_parts)]
|
|
99
141
|
help_lines = list(self._format_help_lines())
|
|
100
142
|
if help_lines:
|
|
101
143
|
segments.append("\n".join(help_lines))
|
|
102
144
|
return "\n\n".join(segments)
|
|
103
145
|
|
|
104
146
|
def _format_help_lines(self) -> typing.Iterator[str]:
|
|
147
|
+
prev_was_group_with_content = False
|
|
105
148
|
for ctrl in self.controls:
|
|
106
149
|
if (sub_iface := _attached_interface(ctrl)) is not None:
|
|
107
150
|
lines = list(sub_iface._format_help_lines())
|
|
108
151
|
if lines and sub_iface.help_heading:
|
|
109
152
|
yield ""
|
|
110
|
-
yield sub_iface.help_heading
|
|
153
|
+
yield f"{sub_iface.help_heading}:"
|
|
111
154
|
yield from lines
|
|
155
|
+
prev_was_group_with_content = bool(lines)
|
|
112
156
|
else:
|
|
113
157
|
input_control = self.input_map.get(ctrl)
|
|
114
158
|
if input_control is not None and not self._is_overridden(input_control):
|
|
115
|
-
|
|
159
|
+
if prev_was_group_with_content:
|
|
160
|
+
yield ""
|
|
161
|
+
for help_line in input_control.format_help_lines():
|
|
162
|
+
yield from _wrap_help_line(help_line)
|
|
163
|
+
prev_was_group_with_content = False
|
|
116
164
|
|
|
117
165
|
def _format_usage_parts(
|
|
118
166
|
self, placeholders_by_option: dict[str, str]
|
|
119
167
|
) -> typing.Iterator[str]:
|
|
120
168
|
for ctrl in self.controls:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
yield from sub_iface._format_usage_parts(placeholders_by_option)
|
|
124
|
-
else:
|
|
169
|
+
sub_iface = _attached_interface(ctrl)
|
|
170
|
+
if sub_iface is None:
|
|
125
171
|
input_control = self.input_map.get(ctrl)
|
|
126
172
|
if input_control is not None and not self._is_overridden(input_control):
|
|
127
173
|
yield from input_control.format_usage_parts()
|
|
@@ -129,14 +175,16 @@ class Interface:
|
|
|
129
175
|
if placeholder := placeholders_by_option.pop(option, None):
|
|
130
176
|
yield placeholder
|
|
131
177
|
|
|
178
|
+
elif not sub_iface.usage_placeholder:
|
|
179
|
+
yield from sub_iface._format_usage_parts(placeholders_by_option)
|
|
180
|
+
|
|
132
181
|
@property
|
|
133
182
|
def default(self) -> dict[str, typing.Any]:
|
|
134
|
-
result: dict[str, typing.Any] = {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
result[name] = ctrl_or_sub.default # type: ignore
|
|
183
|
+
result: dict[str, typing.Any] = {
|
|
184
|
+
name: ctrl_or_sub.default # type: ignore
|
|
185
|
+
for name, ctrl_or_sub in self.iter_controls()
|
|
186
|
+
if isinstance(ctrl_or_sub, Interface) or hasattr(ctrl_or_sub, "default")
|
|
187
|
+
}
|
|
140
188
|
return result
|
|
141
189
|
|
|
142
190
|
def strategy(self) -> st.SearchStrategy[dict[str, typing.Any]]:
|
|
@@ -312,9 +360,9 @@ class Interface:
|
|
|
312
360
|
items: list[typing.Any] = [
|
|
313
361
|
mo.callout(
|
|
314
362
|
mo.md(
|
|
315
|
-
|
|
316
|
-
"
|
|
317
|
-
f"```\n{
|
|
363
|
+
"This notebook also works as a script:\n\n"
|
|
364
|
+
f"```\n{current_command}\n```\n\n"
|
|
365
|
+
f"<details><summary>Usage</summary>\n\n```\n{self.help()}\n```\n</details>\n\n"
|
|
318
366
|
f"{missing_options_msg}"
|
|
319
367
|
),
|
|
320
368
|
"warn" if missing_options else "info",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
|
-
from . import _options, interface
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def create_control(
|
|
7
|
-
group: typing.Any,
|
|
8
|
-
iface: interface.Interface,
|
|
9
|
-
input_control: _options.InputControl,
|
|
10
|
-
) -> typing.Any:
|
|
11
|
-
return group._create_from_input_control(
|
|
12
|
-
input_control, _unprefixed_option(iface, input_control.option)
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _unprefixed_option(iface: interface.Interface, option: str) -> str:
|
|
17
|
-
if iface.option_prefix and option.startswith(f"{iface.option_prefix}-"):
|
|
18
|
-
return f"--{option[len(iface.option_prefix) :].lstrip('-')}"
|
|
19
|
-
return option
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|