mininterface 1.2.1__tar.gz → 1.3.0__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.
- {mininterface-1.2.1 → mininterface-1.3.0}/PKG-INFO +6 -6
- {mininterface-1.2.1 → mininterface-1.3.0}/README.md +4 -4
- mininterface-1.3.0/mininterface/__init__.py +29 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/auxiliary.py +16 -78
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/cli_parser.py +2 -27
- mininterface-1.3.0/mininterface/_lib/config_file.py +97 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/dataclass_creation.py +13 -5
- mininterface-1.3.0/mininterface/_lib/dict_utils.py +17 -0
- mininterface-1.3.0/mininterface/_lib/docstrings.py +101 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/form_dict.py +34 -50
- mininterface-1.3.0/mininterface/_lib/form_types.py +7 -0
- mininterface-1.3.0/mininterface/_lib/ipc_command.py +20 -0
- mininterface-1.3.0/mininterface/_lib/redirectable.py +105 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/run.py +67 -42
- mininterface-1.3.0/mininterface/_lib/subprocess_base.py +569 -0
- mininterface-1.3.0/mininterface/_lib/subprocess_child_base.py +248 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/tyro_patches.py +22 -9
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_mininterface/__init__.py +12 -9
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_mininterface/adaptor.py +13 -7
- mininterface-1.3.0/mininterface/_textual_interface/__init__.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/adaptor.py +38 -31
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/button_contents.py +6 -5
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/facet.py +7 -12
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/file_picker_input.py +2 -2
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/form_contents.py +9 -9
- mininterface-1.2.1/mininterface/_textual_interface/__init__.py → mininterface-1.3.0/mininterface/_textual_interface/interface.py +14 -13
- mininterface-1.3.0/mininterface/_textual_interface/style.tcss +71 -0
- mininterface-1.3.0/mininterface/_textual_interface/subprocess_adaptor.py +39 -0
- mininterface-1.3.0/mininterface/_textual_interface/subprocess_child.py +381 -0
- mininterface-1.3.0/mininterface/_tk_interface/__init__.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/adaptor.py +39 -9
- mininterface-1.2.1/mininterface/_tk_interface/__init__.py → mininterface-1.3.0/mininterface/_tk_interface/interface.py +17 -20
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/secret_entry.py +1 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/select_input.py +12 -1
- mininterface-1.3.0/mininterface/_tk_interface/subprocess_adaptor.py +89 -0
- mininterface-1.3.0/mininterface/_tk_interface/subprocess_child.py +349 -0
- mininterface-1.3.0/mininterface/_web_interface/__init__.py +90 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/exceptions.py +14 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/facet/__init__.py +4 -4
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/interfaces.py +2 -2
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/select_tag.py +11 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/tag.py +12 -19
- {mininterface-1.2.1 → mininterface-1.3.0}/pyproject.toml +2 -2
- mininterface-1.2.1/mininterface/__init__.py +0 -7
- mininterface-1.2.1/mininterface/_lib/config_file.py +0 -99
- mininterface-1.2.1/mininterface/_lib/redirectable.py +0 -62
- mininterface-1.2.1/mininterface/_textual_interface/style.tcss +0 -50
- mininterface-1.2.1/mininterface/_web_interface/__init__.py +0 -92
- mininterface-1.2.1/mininterface/_web_interface/app.py +0 -43
- mininterface-1.2.1/mininterface/_web_interface/child_adaptor.py +0 -90
- mininterface-1.2.1/mininterface/_web_interface/parent_adaptor.py +0 -83
- {mininterface-1.2.1 → mininterface-1.3.0}/LICENSE +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/__main__.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/argparse_support.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/cli_flags.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/cli_utils.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/future_compatibility.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/shortcuts.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/showcase.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/start.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_mininterface/mixin.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_text_interface/__init__.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_text_interface/adaptor.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_text_interface/timeout.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/textual_app.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/timeout.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/widgets.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/date_entry.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/external_fix.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/facet.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/timeout.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/utils.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/cli.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/experimental.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/settings.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/alias.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/callback_tag.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/datetime_tag.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/flag.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/internal.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/path_tag.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/secret_tag.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/tag_factory.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/type_stubs.py +0 -0
- {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: CLI & dialog toolkit – a minimal interface to Python application (GUI, TUI, CLI + config files, web)
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
License-File: LICENSE
|
|
@@ -27,7 +27,7 @@ Requires-Dist: humanize ; extra == "basic" or extra == "img" or extra == "tui" o
|
|
|
27
27
|
Requires-Dist: pillow ; extra == "img" or extra == "gui" or extra == "ui" or extra == "all"
|
|
28
28
|
Requires-Dist: pyyaml ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
|
|
29
29
|
Requires-Dist: simple_term_menu
|
|
30
|
-
Requires-Dist: textual (
|
|
30
|
+
Requires-Dist: textual (>=2.0.0,<9.0.0) ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
|
|
31
31
|
Requires-Dist: textual-serve ; extra == "web" or extra == "ui" or extra == "all"
|
|
32
32
|
Requires-Dist: textual_imageview ; extra == "img" or extra == "tui" or extra == "ui" or extra == "all"
|
|
33
33
|
Requires-Dist: tkcalendar ; extra == "gui" or extra == "ui" or extra == "all"
|
|
@@ -191,8 +191,8 @@ These projects have the code base reduced thanks to the mininterface:
|
|
|
191
191
|
Take a look at the following example.
|
|
192
192
|
|
|
193
193
|
1. We define any Env class.
|
|
194
|
-
2. Then, we initialize mininterface with [`run(Env)`]
|
|
195
|
-
3. Then, we use various dialog methods, like [`confirm`]
|
|
194
|
+
2. Then, we initialize mininterface with [`run(Env)`](https://cz-nic.github.io/mininterface/run/) – the missing fields will be prompted for
|
|
195
|
+
3. Then, we use various dialog methods, like [`confirm`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.confirm), [`select`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.select) or [`form`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.form).
|
|
196
196
|
|
|
197
197
|
Below, you find the screenshots how the program looks in various environments ([graphic](Interfaces.md#guiinterface-or-tkinterface-or-gui) interface, [web](Interfaces.md#webinterface-or-web) interface...).
|
|
198
198
|
|
|
@@ -273,7 +273,7 @@ usage: program.py [-h] [OPTIONS]
|
|
|
273
273
|
|
|
274
274
|
You want to try out the Mininterface with your current [`ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)?
|
|
275
275
|
|
|
276
|
-
You're using positional arguments, subparsers, types in the ArgumentParser... Mininterface will give you immediate benefit. Just wrap it inside the [`run`]
|
|
276
|
+
You're using positional arguments, subparsers, types in the ArgumentParser... Mininterface will give you immediate benefit. Just wrap it inside the [`run`](https://cz-nic.github.io/mininterface/run/) method.
|
|
277
277
|
|
|
278
278
|
```python
|
|
279
279
|
#!/usr/bin/env python3
|
|
@@ -330,7 +330,7 @@ Then, a `.form()` call will create a dialog with all the fields.
|
|
|
330
330
|
|
|
331
331
|

|
|
332
332
|
|
|
333
|
-
You will access the arguments through [`m.env`]
|
|
333
|
+
You will access the arguments through [`m.env`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.env)
|
|
334
334
|
|
|
335
335
|
```python
|
|
336
336
|
print(m.env.time) # -> 14:21
|
|
@@ -151,8 +151,8 @@ These projects have the code base reduced thanks to the mininterface:
|
|
|
151
151
|
Take a look at the following example.
|
|
152
152
|
|
|
153
153
|
1. We define any Env class.
|
|
154
|
-
2. Then, we initialize mininterface with [`run(Env)`]
|
|
155
|
-
3. Then, we use various dialog methods, like [`confirm`]
|
|
154
|
+
2. Then, we initialize mininterface with [`run(Env)`](https://cz-nic.github.io/mininterface/run/) – the missing fields will be prompted for
|
|
155
|
+
3. Then, we use various dialog methods, like [`confirm`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.confirm), [`select`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.select) or [`form`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.form).
|
|
156
156
|
|
|
157
157
|
Below, you find the screenshots how the program looks in various environments ([graphic](Interfaces.md#guiinterface-or-tkinterface-or-gui) interface, [web](Interfaces.md#webinterface-or-web) interface...).
|
|
158
158
|
|
|
@@ -233,7 +233,7 @@ usage: program.py [-h] [OPTIONS]
|
|
|
233
233
|
|
|
234
234
|
You want to try out the Mininterface with your current [`ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)?
|
|
235
235
|
|
|
236
|
-
You're using positional arguments, subparsers, types in the ArgumentParser... Mininterface will give you immediate benefit. Just wrap it inside the [`run`]
|
|
236
|
+
You're using positional arguments, subparsers, types in the ArgumentParser... Mininterface will give you immediate benefit. Just wrap it inside the [`run`](https://cz-nic.github.io/mininterface/run/) method.
|
|
237
237
|
|
|
238
238
|
```python
|
|
239
239
|
#!/usr/bin/env python3
|
|
@@ -290,7 +290,7 @@ Then, a `.form()` call will create a dialog with all the fields.
|
|
|
290
290
|
|
|
291
291
|

|
|
292
292
|
|
|
293
|
-
You will access the arguments through [`m.env`]
|
|
293
|
+
You will access the arguments through [`m.env`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.env)
|
|
294
294
|
|
|
295
295
|
```python
|
|
296
296
|
print(m.env.time) # -> 14:21
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
__all__ = ["run", "Mininterface", "Tag", "Cancelled", "Validation", "Options"]
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def __getattr__(name: str):
|
|
5
|
+
if name == "run":
|
|
6
|
+
from ._lib.run import run
|
|
7
|
+
globals()["run"] = run
|
|
8
|
+
return run
|
|
9
|
+
if name == "Mininterface":
|
|
10
|
+
from ._mininterface import Mininterface
|
|
11
|
+
globals()["Mininterface"] = Mininterface
|
|
12
|
+
return Mininterface
|
|
13
|
+
if name == "Cancelled":
|
|
14
|
+
from .exceptions import Cancelled
|
|
15
|
+
globals()["Cancelled"] = Cancelled
|
|
16
|
+
return Cancelled
|
|
17
|
+
if name == "Tag":
|
|
18
|
+
from .tag import Tag
|
|
19
|
+
globals()["Tag"] = Tag
|
|
20
|
+
return Tag
|
|
21
|
+
if name == "Options":
|
|
22
|
+
from .tag.alias import Options
|
|
23
|
+
globals()["Options"] = Options
|
|
24
|
+
return Options
|
|
25
|
+
if name == "Validation":
|
|
26
|
+
from .tag.alias import Validation
|
|
27
|
+
globals()["Validation"] = Validation
|
|
28
|
+
return Validation
|
|
29
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -1,51 +1,23 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
import re
|
|
4
|
-
from argparse import ArgumentParser
|
|
5
3
|
from dataclasses import fields, is_dataclass
|
|
6
4
|
from functools import lru_cache
|
|
7
5
|
from types import UnionType
|
|
8
|
-
from typing import
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Iterable,
|
|
9
|
+
Union,
|
|
10
|
+
Literal,
|
|
11
|
+
get_args,
|
|
12
|
+
get_origin,
|
|
13
|
+
get_type_hints,
|
|
14
|
+
)
|
|
9
15
|
|
|
10
16
|
from annotated_types import Ge, Gt, Le, Len, Lt, MultipleOf
|
|
17
|
+
from .dict_utils import T, KT, common_iterables, flatten
|
|
11
18
|
|
|
12
19
|
logger = logging.getLogger(__name__)
|
|
13
20
|
|
|
14
|
-
try:
|
|
15
|
-
from tyro.extras import get_parser
|
|
16
|
-
except ImportError:
|
|
17
|
-
get_parser = None
|
|
18
|
-
|
|
19
|
-
try:
|
|
20
|
-
from humanize import naturalsize as naturalsize_
|
|
21
|
-
except ImportError:
|
|
22
|
-
naturalsize_ = None
|
|
23
|
-
|
|
24
|
-
T = TypeVar("T")
|
|
25
|
-
KT = str
|
|
26
|
-
common_iterables = list, tuple, set
|
|
27
|
-
""" collections, and not a str """
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]] = None) -> Iterable[T]:
|
|
31
|
-
"""Recursively traverse whole dict"""
|
|
32
|
-
for k, v in d.items():
|
|
33
|
-
if isinstance(v, dict):
|
|
34
|
-
if include_keys:
|
|
35
|
-
yield from include_keys(k)
|
|
36
|
-
yield from flatten(v)
|
|
37
|
-
else:
|
|
38
|
-
yield v
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# NOTE: Not used.
|
|
42
|
-
def flatten_keys(d: dict[KT, T | dict]) -> Iterable[tuple[KT, T]]:
|
|
43
|
-
"""Recursively traverse whole dict"""
|
|
44
|
-
for k, v in d.items():
|
|
45
|
-
if isinstance(v, dict):
|
|
46
|
-
yield from flatten_keys(v)
|
|
47
|
-
else:
|
|
48
|
-
yield k, v
|
|
49
21
|
|
|
50
22
|
|
|
51
23
|
def guess_type(val: T) -> type[T]:
|
|
@@ -70,42 +42,6 @@ def get_terminal_size():
|
|
|
70
42
|
return 0, 0
|
|
71
43
|
|
|
72
44
|
|
|
73
|
-
def get_descriptions(parser: ArgumentParser) -> dict:
|
|
74
|
-
"""Load descriptions from the parser. Strip argparse info about the default value as it will be editable in the form."""
|
|
75
|
-
# clean-up tyro stuff that may have a meaning in the CLI, but not in the UI
|
|
76
|
-
return {
|
|
77
|
-
re.sub(r"\s\(positional\)$", "", action.dest).replace("-", "_"): re.sub(
|
|
78
|
-
r"\((default|fixed to|required).*\)", "", action.help or ""
|
|
79
|
-
)
|
|
80
|
-
for action in parser._actions
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@lru_cache
|
|
85
|
-
def _get_parser(obj):
|
|
86
|
-
if get_parser:
|
|
87
|
-
return get_parser(obj)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def get_description(obj, param: str) -> str:
|
|
91
|
-
if p := _get_parser(obj):
|
|
92
|
-
try:
|
|
93
|
-
d = get_descriptions(p)[param].strip()
|
|
94
|
-
except KeyError: # either fetching failed or user added no description
|
|
95
|
-
return ""
|
|
96
|
-
else:
|
|
97
|
-
if d.replace("-", "_") == param:
|
|
98
|
-
# field `bot_id` is reported as `bot-id` in tyro
|
|
99
|
-
return ""
|
|
100
|
-
return d
|
|
101
|
-
else:
|
|
102
|
-
# We are missing mininterface[basic] requirement. Tyro is missing.
|
|
103
|
-
# Without tyro, we are not able to evaluate the class: m.form(Env),
|
|
104
|
-
# we can still evaluate its instance: m.form(Env()).
|
|
105
|
-
# However, without descriptions.
|
|
106
|
-
return ""
|
|
107
|
-
|
|
108
|
-
|
|
109
45
|
def yield_annotations(dataclass):
|
|
110
46
|
yield from (cl.__annotations__ for cl in dataclass.__mro__ if is_dataclass(cl))
|
|
111
47
|
|
|
@@ -303,9 +239,11 @@ def dict_diff(a: dict, b: dict) -> dict:
|
|
|
303
239
|
|
|
304
240
|
def naturalsize(value: float | str, *args) -> str:
|
|
305
241
|
"""For a bare interface, humanize might not be installed."""
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
242
|
+
try:
|
|
243
|
+
from humanize import naturalsize as _naturalsize
|
|
244
|
+
return _naturalsize(value, *args)
|
|
245
|
+
except ImportError:
|
|
246
|
+
return str(value)
|
|
309
247
|
|
|
310
248
|
|
|
311
249
|
def validate_annotated_type(meta, value) -> bool:
|
|
@@ -358,7 +296,7 @@ def strip_none(annotation):
|
|
|
358
296
|
args = tuple(arg for arg in get_args(annotation) if arg is not type(None))
|
|
359
297
|
if len(args) == 1:
|
|
360
298
|
return args[0]
|
|
361
|
-
return Union[args]
|
|
299
|
+
return Union[args]
|
|
362
300
|
|
|
363
301
|
return annotation
|
|
364
302
|
|
|
@@ -16,10 +16,10 @@ from ..cli import Command
|
|
|
16
16
|
from ..settings import CliSettings
|
|
17
17
|
|
|
18
18
|
from ..exceptions import Cancelled
|
|
19
|
+
from .auxiliary import flatten
|
|
19
20
|
from .auxiliary import (
|
|
20
21
|
get_or_create_parent_dict,
|
|
21
22
|
remove_empty_dicts,
|
|
22
|
-
flatten,
|
|
23
23
|
)
|
|
24
24
|
from .dataclass_creation import (
|
|
25
25
|
_unwrap_annotated,
|
|
@@ -65,23 +65,6 @@ except ImportError:
|
|
|
65
65
|
raise DependencyRequired("basic")
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def assure_args(args: Optional[Sequence[str]] = None):
|
|
69
|
-
if args is None:
|
|
70
|
-
# Set env to determine whether to use sys.argv.
|
|
71
|
-
# Why settings env? Prevent tyro using sys.argv if we are in an interactive shell like Jupyter,
|
|
72
|
-
# as sys.argv is non-related there.
|
|
73
|
-
try:
|
|
74
|
-
# Note wherease `"get_ipython" in globals()` returns True in Jupyter, it is still False
|
|
75
|
-
# in a script a Jupyter cell runs. Hence we must put here this lengthty statement.
|
|
76
|
-
global get_ipython
|
|
77
|
-
get_ipython()
|
|
78
|
-
except:
|
|
79
|
-
args = sys.argv[1:] # Fetch from the CLI
|
|
80
|
-
else:
|
|
81
|
-
args = []
|
|
82
|
-
return args
|
|
83
|
-
|
|
84
|
-
|
|
85
68
|
def _subcommands_default_appliable(kwargs, _crawling):
|
|
86
69
|
if len(_crawling.get()):
|
|
87
70
|
return kwargs.get("subcommands_default")
|
|
@@ -245,15 +228,7 @@ def parse_cli(
|
|
|
245
228
|
kwargs, None if helponly else m, args, type_form, env_classes, _custom_registry, annot, _req_fields
|
|
246
229
|
)
|
|
247
230
|
|
|
248
|
-
#
|
|
249
|
-
# 1. Getting the interface is a costly operation
|
|
250
|
-
# 2. There is this bug so that we need to use single interface:
|
|
251
|
-
# TODO
|
|
252
|
-
# As this works badly, lets make sure we use single interface now
|
|
253
|
-
# and will not need the second one.
|
|
254
|
-
# get_interface("gui")
|
|
255
|
-
# m = get_interface("gui")
|
|
256
|
-
# m.select([1,2,3])
|
|
231
|
+
# Make the interface ready for the user
|
|
257
232
|
m.env = env
|
|
258
233
|
except SystemExit as exception:
|
|
259
234
|
# --- (C) The dialog missing section ---
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
import dataclasses
|
|
3
|
+
import warnings
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Type
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from ..settings import MininterfaceSettings
|
|
9
|
+
from .auxiliary import dataclass_asdict_no_defaults, merge_dicts
|
|
10
|
+
from .form_dict import EnvClass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_settings_from_config(config_file: Path) -> tuple[dict, dict | None]:
|
|
14
|
+
"""Load yaml config without tyro. Returns (raw_cli_dict, mininterface_settings_dict).
|
|
15
|
+
raw_cli_dict has the 'mininterface' key already removed."""
|
|
16
|
+
import yaml
|
|
17
|
+
raw = yaml.safe_load(config_file.read_text()) or {}
|
|
18
|
+
return raw, raw.pop("mininterface", None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ensure_settings_inheritance(
|
|
22
|
+
base: Optional[MininterfaceSettings], conf: dict, _def_fact=MininterfaceSettings
|
|
23
|
+
) -> MininterfaceSettings:
|
|
24
|
+
"""Merge a config-file settings dict into MininterfaceSettings.
|
|
25
|
+
Handles direct fields and one level of nested dataclass fields.
|
|
26
|
+
Applies UI inheritance (ui→gui, ui→tui, etc.)."""
|
|
27
|
+
if base:
|
|
28
|
+
conf = merge_dicts(dataclass_asdict_no_defaults(base), conf)
|
|
29
|
+
for sources in [
|
|
30
|
+
("ui", "gui"),
|
|
31
|
+
("ui", "tui"),
|
|
32
|
+
("ui", "tui", "textual"),
|
|
33
|
+
("ui", "tui", "text"),
|
|
34
|
+
("ui", "tui", "textual", "web"),
|
|
35
|
+
]:
|
|
36
|
+
target = sources[-1]
|
|
37
|
+
merged: dict = {}
|
|
38
|
+
for s in sources:
|
|
39
|
+
merged.update(conf.get(s, {}))
|
|
40
|
+
merged.update(conf.get(target, {}))
|
|
41
|
+
if merged:
|
|
42
|
+
conf[target] = merged
|
|
43
|
+
result = base or _def_fact()
|
|
44
|
+
for key, value in conf.items():
|
|
45
|
+
if not hasattr(result, key):
|
|
46
|
+
continue
|
|
47
|
+
attr = getattr(result, key)
|
|
48
|
+
if dataclasses.is_dataclass(attr) and isinstance(value, dict):
|
|
49
|
+
for k2, v2 in value.items():
|
|
50
|
+
if hasattr(attr, k2):
|
|
51
|
+
setattr(attr, k2, v2)
|
|
52
|
+
else:
|
|
53
|
+
setattr(result, key, value)
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_config_file(
|
|
58
|
+
env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
59
|
+
raw_config: "dict | None" = None,
|
|
60
|
+
config_file: "Path | None" = None,
|
|
61
|
+
**kwargs,
|
|
62
|
+
) -> dict:
|
|
63
|
+
"""Fill kwargs["default"] from a pre-loaded config dict. Needs tyro.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
env_or_list: Class(es) with the configuration.
|
|
67
|
+
raw_config: Pre-loaded yaml dict from load_settings_from_config (mininterface key already removed).
|
|
68
|
+
Pass None when there is no config file.
|
|
69
|
+
config_file: Original path, used only for error messages.
|
|
70
|
+
Kwargs:
|
|
71
|
+
The same as for argparse.ArgumentParser.
|
|
72
|
+
"""
|
|
73
|
+
if raw_config is not None and "default" not in kwargs:
|
|
74
|
+
from .dataclass_creation import create_with_missing, to_kebab_case
|
|
75
|
+
try:
|
|
76
|
+
subc = {}
|
|
77
|
+
|
|
78
|
+
if isinstance(env_or_list, list):
|
|
79
|
+
kwargs["subcommands_default_union"] = {}
|
|
80
|
+
for cl in env_or_list:
|
|
81
|
+
cl_name = to_kebab_case(cl.__name__)
|
|
82
|
+
subc[cl_name] = {}
|
|
83
|
+
ooo = create_with_missing(cl, raw_config.get(cl_name, {}), subc=subc[cl_name])
|
|
84
|
+
kwargs["subcommands_default_union"][cl_name] = asdict(ooo)
|
|
85
|
+
# `kwargs["default"]` remains empty for now as there is no bare default that tyro would support as everything is hidden under the subcommands
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
kwargs["default"] = create_with_missing(env_or_list, raw_config, subc=subc)
|
|
89
|
+
|
|
90
|
+
if subc:
|
|
91
|
+
kwargs["subcommands_default"] = subc
|
|
92
|
+
except TypeError:
|
|
93
|
+
raise SyntaxError(f"Config file parsing failed for {config_file}")
|
|
94
|
+
|
|
95
|
+
return kwargs
|
|
96
|
+
|
|
97
|
+
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import warnings
|
|
3
|
-
from dataclasses import MISSING,
|
|
3
|
+
from dataclasses import MISSING, fields, is_dataclass
|
|
4
4
|
from types import UnionType
|
|
5
5
|
from typing import Annotated, Optional, Sequence, Type, Union, get_args, get_origin, TypeVar
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
try:
|
|
9
8
|
from tyro._singleton import MISSING_NONPROP
|
|
10
|
-
from tyro.extras import subcommand_type_from_defaults
|
|
11
9
|
|
|
12
10
|
from ..cli import SubcommandPlaceholder
|
|
13
11
|
except ImportError:
|
|
@@ -19,7 +17,8 @@ except ImportError:
|
|
|
19
17
|
from ..tag import Tag
|
|
20
18
|
from ..tag.tag_factory import tag_factory
|
|
21
19
|
from ..validators import not_empty
|
|
22
|
-
from .auxiliary import _get_origin
|
|
20
|
+
from .auxiliary import _get_origin
|
|
21
|
+
from .docstrings import get_class_description, get_description
|
|
23
22
|
from .form_dict import DataClass, EnvClass, MissingTagValue
|
|
24
23
|
|
|
25
24
|
# Pydantic is not a project dependency, that is just an optional integration
|
|
@@ -49,6 +48,15 @@ def coerce_type_to_annotation(value, annotation):
|
|
|
49
48
|
annotation = _unwrap_annotated(annotation) # NOTE might be superfluous, called before
|
|
50
49
|
origin = get_origin(annotation)
|
|
51
50
|
|
|
51
|
+
# Handle Union (e.g. int | None)
|
|
52
|
+
if origin in (Union, UnionType):
|
|
53
|
+
for arg in get_args(annotation):
|
|
54
|
+
try:
|
|
55
|
+
return coerce_type_to_annotation(value, arg)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return value
|
|
59
|
+
|
|
52
60
|
# Handle tuple[...] conversion
|
|
53
61
|
if origin is tuple and isinstance(value, list):
|
|
54
62
|
args = get_args(annotation)
|
|
@@ -353,7 +361,7 @@ def choose_subcommand(env_classes: list[Type[DataClass]], m: "Mininterface[EnvCl
|
|
|
353
361
|
# NOTE make select display buttons if there is a little amount of options.
|
|
354
362
|
env = m.select(
|
|
355
363
|
{
|
|
356
|
-
(to_kebab_case(cl.__name__).replace("-", " ").capitalize(),
|
|
364
|
+
(to_kebab_case(cl.__name__).replace("-", " ").capitalize(), get_class_description(cl)): cl
|
|
357
365
|
for cl in env_classes
|
|
358
366
|
if cl is not SubcommandPlaceholder
|
|
359
367
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Callable, Iterable, Optional, TypeVar
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
KT = str
|
|
5
|
+
common_iterables = list, tuple, set
|
|
6
|
+
""" collections, and not a str """
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]] = None) -> Iterable[T]:
|
|
10
|
+
"""Recursively traverse whole dict"""
|
|
11
|
+
for k, v in d.items():
|
|
12
|
+
if isinstance(v, dict):
|
|
13
|
+
if include_keys:
|
|
14
|
+
yield from include_keys(k)
|
|
15
|
+
yield from flatten(v)
|
|
16
|
+
else:
|
|
17
|
+
yield v
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Tyro-dependent docstring/description helpers, split out from auxiliary.py.
|
|
2
|
+
|
|
3
|
+
Keeping these separate lets child processes import auxiliary.flatten without
|
|
4
|
+
paying the ~20 ms cost of loading tyro. Tyro itself is imported lazily inside
|
|
5
|
+
the functions that need it, so importing this module is also cheap.
|
|
6
|
+
"""
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from typing import Annotated, get_args, get_origin, get_type_hints
|
|
9
|
+
from dataclasses import fields
|
|
10
|
+
|
|
11
|
+
_tyro_loaded = False
|
|
12
|
+
_tyro_docstrings_available = False
|
|
13
|
+
_tyro_get_field_docstring = None
|
|
14
|
+
_tyro_get_callable_description = None
|
|
15
|
+
tyro = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _ensure_tyro():
|
|
19
|
+
global _tyro_loaded, _tyro_docstrings_available, _tyro_get_field_docstring, _tyro_get_callable_description, tyro
|
|
20
|
+
if _tyro_loaded:
|
|
21
|
+
return
|
|
22
|
+
_tyro_loaded = True
|
|
23
|
+
try:
|
|
24
|
+
import tyro as _tyro
|
|
25
|
+
from tyro._docstrings import get_field_docstring as _gfd
|
|
26
|
+
from tyro._docstrings import get_callable_description as _gcd
|
|
27
|
+
tyro = _tyro
|
|
28
|
+
_tyro_get_field_docstring = _gfd
|
|
29
|
+
_tyro_get_callable_description = _gcd
|
|
30
|
+
_tyro_docstrings_available = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_class_description(obj) -> str:
|
|
36
|
+
_ensure_tyro()
|
|
37
|
+
if _tyro_get_callable_description:
|
|
38
|
+
return _tyro_get_callable_description(obj)
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@lru_cache
|
|
43
|
+
def _get_descriptions_from_docstring(obj) -> dict[str, str]:
|
|
44
|
+
"""Extract field descriptions for all fields of a class.
|
|
45
|
+
|
|
46
|
+
Uses tyro's internal helptext extraction (tyro._docstrings.get_field_docstring),
|
|
47
|
+
which supports the same sources and precedence as tyro's own CLI generation:
|
|
48
|
+
1. tyro.conf.arg(help=...)
|
|
49
|
+
2. PEP 727 Doc
|
|
50
|
+
3. Docstrings (attribute docstrings or class docstring params)
|
|
51
|
+
4. Comments (inline or preceding)
|
|
52
|
+
|
|
53
|
+
We used to rely on tyro.extras.get_parser(), but that was marked deprecated,
|
|
54
|
+
so we call tyro's internal API directly instead.
|
|
55
|
+
"""
|
|
56
|
+
_ensure_tyro()
|
|
57
|
+
if not _tyro_docstrings_available:
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
result = {}
|
|
61
|
+
|
|
62
|
+
# Highest priority: tyro.conf.arg(help=...) in Annotated metadata.
|
|
63
|
+
try:
|
|
64
|
+
hints = get_type_hints(obj, include_extras=True)
|
|
65
|
+
ArgConfig = tyro.conf._confstruct._ArgConfig
|
|
66
|
+
for field_name, hint in hints.items():
|
|
67
|
+
if get_origin(hint) is Annotated:
|
|
68
|
+
for meta in hint.__metadata__:
|
|
69
|
+
if isinstance(meta, ArgConfig) and meta.help:
|
|
70
|
+
result[field_name] = meta.help
|
|
71
|
+
except Exception:
|
|
72
|
+
hints = {}
|
|
73
|
+
|
|
74
|
+
# Mid priority: docstrings and comments via tyro's own extraction.
|
|
75
|
+
for field_name in hints:
|
|
76
|
+
doc = _tyro_get_field_docstring(obj, field_name, ())
|
|
77
|
+
if doc:
|
|
78
|
+
result.setdefault(field_name, doc)
|
|
79
|
+
|
|
80
|
+
# Lowest priority: field.metadata["help"] from dynamically generated
|
|
81
|
+
# dataclasses (e.g. built from ArgumentParser via make_dataclass).
|
|
82
|
+
try:
|
|
83
|
+
for f in fields(obj): # type: ignore
|
|
84
|
+
if help_text := f.metadata.get("help"):
|
|
85
|
+
result.setdefault(f.name, help_text)
|
|
86
|
+
except TypeError:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_description(obj, param: str) -> str:
|
|
93
|
+
desc = _get_descriptions_from_docstring(obj).get(param, "")
|
|
94
|
+
if desc and desc.replace("-", "_") != param:
|
|
95
|
+
return desc
|
|
96
|
+
|
|
97
|
+
# We are missing mininterface[basic] requirement. Tyro is missing.
|
|
98
|
+
# Without tyro, we are not able to evaluate the class: m.form(Env),
|
|
99
|
+
# we can still evaluate its instance: m.form(Env()).
|
|
100
|
+
# However, without descriptions.
|
|
101
|
+
return ""
|
|
@@ -9,8 +9,10 @@ from dataclasses import fields, is_dataclass
|
|
|
9
9
|
from types import FunctionType, MethodType, SimpleNamespace
|
|
10
10
|
from typing import TYPE_CHECKING, Any, Callable, Hashable, Optional, Type, TypeVar, Union, get_args, get_type_hints
|
|
11
11
|
|
|
12
|
+
from .form_types import DataClass, EnvClass
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
from .docstrings import get_description
|
|
14
16
|
from ..tag.tag import MissingTagValue, Tag, TagValue
|
|
15
17
|
from ..tag.tag_factory import tag_assure_type, tag_factory
|
|
16
18
|
|
|
@@ -19,53 +21,35 @@ if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self`
|
|
|
19
21
|
|
|
20
22
|
from .. import Mininterface
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
# attr and pydantic are optional integrations — loaded only on first use so that
|
|
25
|
+
# users relying purely on dataclasses never imports the lib.
|
|
26
|
+
_attr = None
|
|
27
|
+
|
|
28
|
+
def _get_attr():
|
|
29
|
+
global _attr
|
|
30
|
+
if _attr is None:
|
|
31
|
+
try:
|
|
32
|
+
import attr as _a
|
|
33
|
+
_attr = _a
|
|
34
|
+
except ImportError:
|
|
35
|
+
_attr = False
|
|
36
|
+
return _attr or None
|
|
37
|
+
|
|
38
|
+
_BaseModel = None
|
|
39
|
+
|
|
40
|
+
def _get_BaseModel():
|
|
41
|
+
global _BaseModel
|
|
42
|
+
if _BaseModel is None:
|
|
43
|
+
try:
|
|
44
|
+
from pydantic import BaseModel as _BM
|
|
45
|
+
_BaseModel = _BM
|
|
46
|
+
except ImportError:
|
|
47
|
+
_BaseModel = False
|
|
48
|
+
return _BaseModel or None
|
|
30
49
|
|
|
31
50
|
|
|
32
51
|
logger = logging.getLogger(__name__)
|
|
33
52
|
|
|
34
|
-
DataClass = TypeVar("DataClass")
|
|
35
|
-
""" Any dataclass. Or a pydantic model or attrs. """
|
|
36
|
-
EnvClass = TypeVar("EnvClass", bound=DataClass)
|
|
37
|
-
""" Any dataclass. Its instance will be available through [Mininterface.env][mininterface.Mininterface.env] after CLI parsing. Its fields or whole class might be annotated with [tyro conf flags](https://brentyi.github.io/tyro/api/tyro/conf/).
|
|
38
|
-
|
|
39
|
-
The following example turns down boolean flag conversion.
|
|
40
|
-
|
|
41
|
-
```python
|
|
42
|
-
from dataclasses import dataclass
|
|
43
|
-
from mininterface import run
|
|
44
|
-
from tyro.conf import FlagConversionOff
|
|
45
|
-
|
|
46
|
-
@dataclass
|
|
47
|
-
class Env:
|
|
48
|
-
my_bool: bool = False
|
|
49
|
-
|
|
50
|
-
m = run(FlagConversionOff[Env])
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
$ program.py --help
|
|
55
|
-
# --my-bool {True,False} (default: False)
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Whereas by default, both flags are generated:
|
|
59
|
-
|
|
60
|
-
```python
|
|
61
|
-
m = run(Env)
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
$ program.py --help
|
|
66
|
-
# --my-bool, --no-my-bool (default: False)
|
|
67
|
-
```
|
|
68
|
-
"""
|
|
69
53
|
FormDict = dict[Hashable, TypeVar("FormDictRecursiveValue", TagValue, Tag, "Self")]
|
|
70
54
|
""" Nested dict that can have descriptions (through Tag) instead of plain values."""
|
|
71
55
|
# Attention to programmers. Should we to change FormDict type, check these IDE suggestions are still the same.
|
|
@@ -192,14 +176,14 @@ def iterate_attributes(env: DataClass):
|
|
|
192
176
|
# Why using fields instead of vars(env)? There might be some helper parameters in the dataclasses that should not be form editable.
|
|
193
177
|
for f in fields(env):
|
|
194
178
|
yield f.name, getattr(env, f.name)
|
|
195
|
-
elif
|
|
179
|
+
elif (_bm := _get_BaseModel()) and isinstance(env, _bm):
|
|
196
180
|
for param, val in vars(env).items():
|
|
197
181
|
yield param, val
|
|
198
182
|
# NOTE private pydantic attributes might be printed to forms, because this makes test fail for nested models
|
|
199
183
|
# for param, val in env.model_dump().items():
|
|
200
184
|
# yield param, val
|
|
201
|
-
elif
|
|
202
|
-
for f in
|
|
185
|
+
elif (_at := _get_attr()) and _at.has(env):
|
|
186
|
+
for f in _at.fields(env.__class__):
|
|
203
187
|
yield f.name, getattr(env, f.name)
|
|
204
188
|
else: # might be a normal class; which is unsupported but mostly might work
|
|
205
189
|
for param, val in vars(env).items():
|
|
@@ -212,14 +196,14 @@ def iterate_attributes_keys(env: DataClass):
|
|
|
212
196
|
# Why using fields instead of vars(env)? There might be some helper parameters in the dataclasses that should not be form editable.
|
|
213
197
|
for f in fields(env):
|
|
214
198
|
yield f.name
|
|
215
|
-
elif
|
|
199
|
+
elif (_bm := _get_BaseModel()) and isinstance(env, _bm):
|
|
216
200
|
for param, val in vars(env).items():
|
|
217
201
|
yield param
|
|
218
202
|
# NOTE private pydantic attributes might be printed to forms, because this makes test fail for nested models
|
|
219
203
|
# for param, val in env.model_dump().items():
|
|
220
204
|
# yield param, val
|
|
221
|
-
elif
|
|
222
|
-
for f in
|
|
205
|
+
elif (_at := _get_attr()) and _at.has(env):
|
|
206
|
+
for f in _at.fields(env.__class__):
|
|
223
207
|
yield f.name
|
|
224
208
|
else: # might be a normal class; which is unsupported but mostly might work
|
|
225
209
|
for param, val in vars(env).items():
|