mininterface 1.2.2__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.2 → mininterface-1.3.0}/PKG-INFO +2 -2
- mininterface-1.3.0/mininterface/__init__.py +29 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/auxiliary.py +6 -114
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/cli_parser.py +1 -18
- mininterface-1.3.0/mininterface/_lib/config_file.py +97 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/dataclass_creation.py +2 -1
- mininterface-1.3.0/mininterface/_lib/dict_utils.py +17 -0
- mininterface-1.3.0/mininterface/_lib/docstrings.py +101 -0
- {mininterface-1.2.2 → 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.2 → 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.2 → mininterface-1.3.0}/mininterface/_mininterface/__init__.py +12 -9
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_mininterface/adaptor.py +13 -7
- mininterface-1.3.0/mininterface/_textual_interface/__init__.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/adaptor.py +38 -31
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/button_contents.py +6 -5
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/facet.py +7 -12
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/file_picker_input.py +2 -2
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/form_contents.py +9 -9
- mininterface-1.2.2/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.2 → mininterface-1.3.0}/mininterface/_tk_interface/adaptor.py +18 -9
- mininterface-1.2.2/mininterface/_tk_interface/__init__.py → mininterface-1.3.0/mininterface/_tk_interface/interface.py +17 -20
- 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.2 → mininterface-1.3.0}/mininterface/exceptions.py +14 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/facet/__init__.py +4 -4
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/interfaces.py +2 -2
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/select_tag.py +11 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/tag.py +12 -19
- {mininterface-1.2.2 → mininterface-1.3.0}/pyproject.toml +2 -2
- mininterface-1.2.2/mininterface/__init__.py +0 -7
- mininterface-1.2.2/mininterface/_lib/config_file.py +0 -99
- mininterface-1.2.2/mininterface/_lib/redirectable.py +0 -62
- mininterface-1.2.2/mininterface/_textual_interface/style.tcss +0 -50
- mininterface-1.2.2/mininterface/_web_interface/__init__.py +0 -92
- mininterface-1.2.2/mininterface/_web_interface/app.py +0 -43
- mininterface-1.2.2/mininterface/_web_interface/child_adaptor.py +0 -90
- mininterface-1.2.2/mininterface/_web_interface/parent_adaptor.py +0 -83
- {mininterface-1.2.2 → mininterface-1.3.0}/LICENSE +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/README.md +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/__main__.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/argparse_support.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/cli_flags.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/cli_utils.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/future_compatibility.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/shortcuts.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/showcase.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/start.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/tyro_patches.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_mininterface/mixin.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_text_interface/__init__.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_text_interface/adaptor.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_text_interface/timeout.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/textual_app.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/timeout.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/widgets.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/date_entry.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/external_fix.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/facet.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/secret_entry.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/select_input.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/timeout.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/utils.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/cli.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/experimental.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/settings.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/alias.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/callback_tag.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/datetime_tag.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/flag.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/internal.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/path_tag.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/secret_tag.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/tag_factory.py +0 -0
- {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/type_stubs.py +0 -0
- {mininterface-1.2.2 → 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"
|
|
@@ -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}")
|
|
@@ -5,11 +5,7 @@ from functools import lru_cache
|
|
|
5
5
|
from types import UnionType
|
|
6
6
|
from typing import (
|
|
7
7
|
Any,
|
|
8
|
-
Annotated,
|
|
9
|
-
Callable,
|
|
10
8
|
Iterable,
|
|
11
|
-
Optional,
|
|
12
|
-
TypeVar,
|
|
13
9
|
Union,
|
|
14
10
|
Literal,
|
|
15
11
|
get_args,
|
|
@@ -18,50 +14,10 @@ from typing import (
|
|
|
18
14
|
)
|
|
19
15
|
|
|
20
16
|
from annotated_types import Ge, Gt, Le, Len, Lt, MultipleOf
|
|
17
|
+
from .dict_utils import T, KT, common_iterables, flatten
|
|
21
18
|
|
|
22
19
|
logger = logging.getLogger(__name__)
|
|
23
20
|
|
|
24
|
-
try:
|
|
25
|
-
import tyro
|
|
26
|
-
from tyro._docstrings import get_field_docstring as _tyro_get_field_docstring
|
|
27
|
-
from tyro._docstrings import get_callable_description as _tyro_get_callable_description
|
|
28
|
-
|
|
29
|
-
_tyro_docstrings_available = True
|
|
30
|
-
except ImportError:
|
|
31
|
-
tyro = None
|
|
32
|
-
_tyro_docstrings_available = False
|
|
33
|
-
_tyro_get_callable_description = None
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
from humanize import naturalsize as naturalsize_
|
|
37
|
-
except ImportError:
|
|
38
|
-
naturalsize_ = None
|
|
39
|
-
|
|
40
|
-
T = TypeVar("T")
|
|
41
|
-
KT = str
|
|
42
|
-
common_iterables = list, tuple, set
|
|
43
|
-
""" collections, and not a str """
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]] = None) -> Iterable[T]:
|
|
47
|
-
"""Recursively traverse whole dict"""
|
|
48
|
-
for k, v in d.items():
|
|
49
|
-
if isinstance(v, dict):
|
|
50
|
-
if include_keys:
|
|
51
|
-
yield from include_keys(k)
|
|
52
|
-
yield from flatten(v)
|
|
53
|
-
else:
|
|
54
|
-
yield v
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# NOTE: Not used.
|
|
58
|
-
def flatten_keys(d: dict[KT, T | dict]) -> Iterable[tuple[KT, T]]:
|
|
59
|
-
"""Recursively traverse whole dict"""
|
|
60
|
-
for k, v in d.items():
|
|
61
|
-
if isinstance(v, dict):
|
|
62
|
-
yield from flatten_keys(v)
|
|
63
|
-
else:
|
|
64
|
-
yield k, v
|
|
65
21
|
|
|
66
22
|
|
|
67
23
|
def guess_type(val: T) -> type[T]:
|
|
@@ -86,72 +42,6 @@ def get_terminal_size():
|
|
|
86
42
|
return 0, 0
|
|
87
43
|
|
|
88
44
|
|
|
89
|
-
def get_class_description(obj) -> str:
|
|
90
|
-
if _tyro_get_callable_description:
|
|
91
|
-
return _tyro_get_callable_description(obj)
|
|
92
|
-
return ""
|
|
93
|
-
|
|
94
|
-
@lru_cache
|
|
95
|
-
def _get_descriptions_from_docstring(obj) -> dict[str, str]:
|
|
96
|
-
"""Extract field descriptions for all fields of a class.
|
|
97
|
-
|
|
98
|
-
Uses tyro's internal helptext extraction (tyro._docstrings.get_field_docstring),
|
|
99
|
-
which supports the same sources and precedence as tyro's own CLI generation:
|
|
100
|
-
1. tyro.conf.arg(help=...)
|
|
101
|
-
2. PEP 727 Doc
|
|
102
|
-
3. Docstrings (attribute docstrings or class docstring params)
|
|
103
|
-
4. Comments (inline or preceding)
|
|
104
|
-
|
|
105
|
-
We used to rely on tyro.extras.get_parser(), but that was marked deprecated,
|
|
106
|
-
so we call tyro's internal API directly instead.
|
|
107
|
-
"""
|
|
108
|
-
if not _tyro_docstrings_available:
|
|
109
|
-
return {}
|
|
110
|
-
|
|
111
|
-
result = {}
|
|
112
|
-
|
|
113
|
-
# Highest priority: tyro.conf.arg(help=...) in Annotated metadata.
|
|
114
|
-
try:
|
|
115
|
-
hints = get_type_hints(obj, include_extras=True)
|
|
116
|
-
ArgConfig = tyro.conf._confstruct._ArgConfig
|
|
117
|
-
for field_name, hint in hints.items():
|
|
118
|
-
if get_origin(hint) is Annotated:
|
|
119
|
-
for meta in hint.__metadata__:
|
|
120
|
-
if isinstance(meta, ArgConfig) and meta.help:
|
|
121
|
-
result[field_name] = meta.help
|
|
122
|
-
except Exception:
|
|
123
|
-
hints = {}
|
|
124
|
-
|
|
125
|
-
# Mid priority: docstrings and comments via tyro's own extraction.
|
|
126
|
-
for field_name in hints:
|
|
127
|
-
doc = _tyro_get_field_docstring(obj, field_name, ())
|
|
128
|
-
if doc:
|
|
129
|
-
result.setdefault(field_name, doc)
|
|
130
|
-
|
|
131
|
-
# Lowest priority: field.metadata["help"] from dynamically generated
|
|
132
|
-
# dataclasses (e.g. built from ArgumentParser via make_dataclass).
|
|
133
|
-
try:
|
|
134
|
-
for f in fields(obj): # type: ignore
|
|
135
|
-
if help_text := f.metadata.get("help"):
|
|
136
|
-
result.setdefault(f.name, help_text)
|
|
137
|
-
except TypeError:
|
|
138
|
-
pass
|
|
139
|
-
|
|
140
|
-
return result
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def get_description(obj, param: str) -> str:
|
|
144
|
-
desc = _get_descriptions_from_docstring(obj).get(param, "")
|
|
145
|
-
if desc and desc.replace("-", "_") != param:
|
|
146
|
-
return desc
|
|
147
|
-
|
|
148
|
-
# We are missing mininterface[basic] requirement. Tyro is missing.
|
|
149
|
-
# Without tyro, we are not able to evaluate the class: m.form(Env),
|
|
150
|
-
# we can still evaluate its instance: m.form(Env()).
|
|
151
|
-
# However, without descriptions.
|
|
152
|
-
return ""
|
|
153
|
-
|
|
154
|
-
|
|
155
45
|
def yield_annotations(dataclass):
|
|
156
46
|
yield from (cl.__annotations__ for cl in dataclass.__mro__ if is_dataclass(cl))
|
|
157
47
|
|
|
@@ -349,9 +239,11 @@ def dict_diff(a: dict, b: dict) -> dict:
|
|
|
349
239
|
|
|
350
240
|
def naturalsize(value: float | str, *args) -> str:
|
|
351
241
|
"""For a bare interface, humanize might not be installed."""
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
242
|
+
try:
|
|
243
|
+
from humanize import naturalsize as _naturalsize
|
|
244
|
+
return _naturalsize(value, *args)
|
|
245
|
+
except ImportError:
|
|
246
|
+
return str(value)
|
|
355
247
|
|
|
356
248
|
|
|
357
249
|
def validate_annotated_type(meta, value) -> bool:
|
|
@@ -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")
|
|
@@ -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
|
+
|
|
@@ -17,7 +17,8 @@ except ImportError:
|
|
|
17
17
|
from ..tag import Tag
|
|
18
18
|
from ..tag.tag_factory import tag_factory
|
|
19
19
|
from ..validators import not_empty
|
|
20
|
-
from .auxiliary import _get_origin
|
|
20
|
+
from .auxiliary import _get_origin
|
|
21
|
+
from .docstrings import get_class_description, get_description
|
|
21
22
|
from .form_dict import DataClass, EnvClass, MissingTagValue
|
|
22
23
|
|
|
23
24
|
# Pydantic is not a project dependency, that is just an optional integration
|
|
@@ -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():
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
DataClass = TypeVar("DataClass")
|
|
4
|
+
""" Any dataclass. Or a pydantic model or attrs. """
|
|
5
|
+
|
|
6
|
+
EnvClass = TypeVar("EnvClass", bound=DataClass)
|
|
7
|
+
""" Any dataclass. Its instance will be available through [Mininterface.env][mininterface.Mininterface.env] after CLI parsing. """
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IpcCommand(Enum):
|
|
5
|
+
"""Message kinds exchanged over the parent⇄child pipe of every subprocess
|
|
6
|
+
UI backend — both the Tk GUI and the Textual TUI use the same protocol."""
|
|
7
|
+
|
|
8
|
+
FORM = "form"
|
|
9
|
+
BUTTONS = "buttons"
|
|
10
|
+
SHUTDOWN = "shutdown"
|
|
11
|
+
RESULT = "result"
|
|
12
|
+
CANCEL = "cancel"
|
|
13
|
+
QUIT = "quit" # child → parent: window closed (X) → exit the program
|
|
14
|
+
ERROR = "error" # child → parent: dialog build crashed (exception + traceback text)
|
|
15
|
+
CALLBACK = "callback" # child → parent: callback fired
|
|
16
|
+
FORM_UPDATE = "form_update" # parent → child: updated tag values after callback
|
|
17
|
+
VALIDATE_RESULT = "validate_result" # parent → child: result of a live validation round-trip
|
|
18
|
+
OUTPUT = "output" # parent → child: live print() text to stream
|
|
19
|
+
CLEAR_OUTPUT = "clear_output" # parent → child: clear the streamed-output widget
|
|
20
|
+
SETTINGS = "settings" # parent → child: the UI settings (sent once after spawn)
|