mininterface 1.1.3__tar.gz → 1.2.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.1.3 → mininterface-1.2.0}/PKG-INFO +7 -3
- {mininterface-1.1.3 → mininterface-1.2.0}/README.md +3 -1
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/argparse_support.py +19 -6
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/auxiliary.py +11 -6
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/cli_flags.py +15 -4
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/cli_parser.py +149 -29
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/cli_utils.py +1 -1
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/config_file.py +23 -21
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/dataclass_creation.py +137 -57
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/form_dict.py +13 -7
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/future_compatibility.py +2 -2
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/run.py +32 -15
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/start.py +4 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/tyro_patches.py +16 -4
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_mininterface/__init__.py +3 -6
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_mininterface/adaptor.py +14 -8
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_mininterface/mixin.py +1 -1
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_text_interface/__init__.py +1 -1
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_text_interface/timeout.py +16 -21
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/widgets.py +1 -1
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/select_input.py +1 -1
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/timeout.py +2 -3
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_web_interface/child_adaptor.py +1 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/cli.py +2 -1
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/datetime_tag.py +1 -1
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/flag.py +4 -2
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/tag.py +23 -7
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/tag_factory.py +5 -2
- {mininterface-1.1.3 → mininterface-1.2.0}/pyproject.toml +3 -4
- {mininterface-1.1.3 → mininterface-1.2.0}/LICENSE +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/__init__.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/__main__.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/redirectable.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/shortcuts.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_lib/showcase.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_text_interface/adaptor.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/__init__.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/adaptor.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/button_contents.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/facet.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/file_picker_input.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/form_contents.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/style.tcss +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/textual_app.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_textual_interface/timeout.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/__init__.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/adaptor.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/date_entry.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/external_fix.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/facet.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/secret_entry.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_tk_interface/utils.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_web_interface/__init__.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_web_interface/app.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/_web_interface/parent_adaptor.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/exceptions.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/experimental.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/facet/__init__.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/interfaces.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/settings.py +1 -1
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/alias.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/callback_tag.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/internal.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/path_tag.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/secret_tag.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/select_tag.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/tag/type_stubs.py +0 -0
- {mininterface-1.1.3 → mininterface-1.2.0}/mininterface/validators.py +0 -0
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.2.0
|
|
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
|
|
7
7
|
Author: Edvard Rejthar
|
|
8
8
|
Author-email: edvard.rejthar@nic.cz
|
|
9
|
-
Requires-Python: >=3.10,<
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
10
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
17
|
Provides-Extra: all
|
|
17
18
|
Provides-Extra: basic
|
|
18
19
|
Provides-Extra: gui
|
|
@@ -130,6 +131,8 @@ with run(Env) as m:
|
|
|
130
131
|
|
|
131
132
|
Wrapper between various libraries that provide a user interface.
|
|
132
133
|
|
|
134
|
+
It allows you to focus on writing the program's core logic without worrying about input/output. It generates CLI arguments, parses config file, and shows a UI. Depending on the endpoint, the dialogs automatically use either a GUI, a mouse-clickable TUI, a lightweight terminal text interface, or be available via HTTP — all while maintaining exactly the same functionality.
|
|
135
|
+
|
|
133
136
|
Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. *Mininterface* is here to help.
|
|
134
137
|
|
|
135
138
|
The config variables needed by your program are kept in cozy dataclasses. Write less! The syntax of [tyro](https://github.com/brentyi/tyro) does not require any overhead (as its `argparse` alternatives do). You just annotate a class attribute, append a simple docstring and get a fully functional application:
|
|
@@ -337,3 +340,4 @@ If you're sure enough to start using *Mininterface*, convert the argparse into a
|
|
|
337
340
|
|
|
338
341
|
!!! warning
|
|
339
342
|
The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
|
|
343
|
+
|
|
@@ -91,6 +91,8 @@ with run(Env) as m:
|
|
|
91
91
|
|
|
92
92
|
Wrapper between various libraries that provide a user interface.
|
|
93
93
|
|
|
94
|
+
It allows you to focus on writing the program's core logic without worrying about input/output. It generates CLI arguments, parses config file, and shows a UI. Depending on the endpoint, the dialogs automatically use either a GUI, a mouse-clickable TUI, a lightweight terminal text interface, or be available via HTTP — all while maintaining exactly the same functionality.
|
|
95
|
+
|
|
94
96
|
Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. *Mininterface* is here to help.
|
|
95
97
|
|
|
96
98
|
The config variables needed by your program are kept in cozy dataclasses. Write less! The syntax of [tyro](https://github.com/brentyi/tyro) does not require any overhead (as its `argparse` alternatives do). You just annotate a class attribute, append a simple docstring and get a fully functional application:
|
|
@@ -297,4 +299,4 @@ print(m.env.time) # -> 14:21
|
|
|
297
299
|
If you're sure enough to start using *Mininterface*, convert the argparse into a dataclass. Then, the IDE will auto-complete the hints as you type.
|
|
298
300
|
|
|
299
301
|
!!! warning
|
|
300
|
-
The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
|
|
302
|
+
The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import sys
|
|
3
|
-
from argparse import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
from argparse import (
|
|
4
|
+
SUPPRESS,
|
|
5
|
+
Action,
|
|
6
|
+
ArgumentParser,
|
|
7
|
+
_AppendAction,
|
|
8
|
+
_AppendConstAction,
|
|
9
|
+
_CountAction,
|
|
10
|
+
_HelpAction,
|
|
11
|
+
_StoreConstAction,
|
|
12
|
+
_StoreFalseAction,
|
|
13
|
+
_StoreTrueAction,
|
|
14
|
+
_SubParsersAction,
|
|
15
|
+
_VersionAction,
|
|
16
|
+
)
|
|
7
17
|
from collections import defaultdict
|
|
8
18
|
from dataclasses import MISSING, Field, dataclass, field, make_dataclass
|
|
9
19
|
from functools import cached_property
|
|
@@ -65,7 +75,9 @@ class ArgparseField:
|
|
|
65
75
|
return self.action.dest in self.properties
|
|
66
76
|
|
|
67
77
|
|
|
68
|
-
def parser_to_dataclass(
|
|
78
|
+
def parser_to_dataclass(
|
|
79
|
+
parser: ArgumentParser, name: str = "Args"
|
|
80
|
+
) -> tuple[DataClass | list[DataClass], Optional[str]]:
|
|
69
81
|
"""
|
|
70
82
|
Note: Ex. parser.add_argument("--time", type=time) -> does work at all in argparse, here it works.
|
|
71
83
|
|
|
@@ -197,8 +209,9 @@ def _make_dataclass_from_actions(
|
|
|
197
209
|
if action.choices:
|
|
198
210
|
# With the drop of Python 3.10, use mere:
|
|
199
211
|
# arg_type = Literal[*action.choices]
|
|
200
|
-
if sys.version_info >= (3,11):
|
|
212
|
+
if sys.version_info >= (3, 11):
|
|
201
213
|
from .future_compatibility import literal
|
|
214
|
+
|
|
202
215
|
arg_type = literal(action.choices)
|
|
203
216
|
else:
|
|
204
217
|
# we do not prefer this option as tyro does not understand it
|
|
@@ -5,8 +5,7 @@ from argparse import ArgumentParser
|
|
|
5
5
|
from dataclasses import fields, is_dataclass
|
|
6
6
|
from functools import lru_cache
|
|
7
7
|
from types import UnionType
|
|
8
|
-
from typing import
|
|
9
|
-
get_args, get_origin, get_type_hints)
|
|
8
|
+
from typing import Any, Callable, Iterable, Optional, TypeVar, Union, Literal, get_args, get_origin, get_type_hints
|
|
10
9
|
|
|
11
10
|
from annotated_types import Ge, Gt, Le, Len, Lt, MultipleOf
|
|
12
11
|
|
|
@@ -75,7 +74,9 @@ def get_descriptions(parser: ArgumentParser) -> dict:
|
|
|
75
74
|
"""Load descriptions from the parser. Strip argparse info about the default value as it will be editable in the form."""
|
|
76
75
|
# clean-up tyro stuff that may have a meaning in the CLI, but not in the UI
|
|
77
76
|
return {
|
|
78
|
-
re.sub(r"\s\(positional\)$", "", action.dest).replace("-", "_"): re.sub(
|
|
77
|
+
re.sub(r"\s\(positional\)$", "", action.dest).replace("-", "_"): re.sub(
|
|
78
|
+
r"\((default|fixed to|required).*\)", "", action.help or ""
|
|
79
|
+
)
|
|
79
80
|
for action in parser._actions
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -201,7 +202,7 @@ def matches_annotation(value, annotation) -> bool:
|
|
|
201
202
|
if origin is list:
|
|
202
203
|
return all(matches_annotation(item, subtypes[0]) for item in value)
|
|
203
204
|
elif origin is tuple:
|
|
204
|
-
if len(subtypes) == 2 and subtypes[1] is Ellipsis:
|
|
205
|
+
if len(subtypes) == 2 and subtypes[1] is Ellipsis: # ex. tuple[int, ...]
|
|
205
206
|
return all(matches_annotation(v, subtypes[0]) for v in value)
|
|
206
207
|
if len(subtypes) != len(value):
|
|
207
208
|
return False
|
|
@@ -286,8 +287,9 @@ def merge_dicts(d1: dict, d2: dict):
|
|
|
286
287
|
d1[key] = value
|
|
287
288
|
return d1
|
|
288
289
|
|
|
290
|
+
|
|
289
291
|
def dict_diff(a: dict, b: dict) -> dict:
|
|
290
|
-
"""
|
|
292
|
+
"""Returns the B values where they differ."""
|
|
291
293
|
result = {}
|
|
292
294
|
for k in b:
|
|
293
295
|
if isinstance(a.get(k), dict) and isinstance(b.get(k), dict):
|
|
@@ -298,6 +300,7 @@ def dict_diff(a: dict, b: dict) -> dict:
|
|
|
298
300
|
result[k] = b[k]
|
|
299
301
|
return result
|
|
300
302
|
|
|
303
|
+
|
|
301
304
|
def naturalsize(value: float | str, *args) -> str:
|
|
302
305
|
"""For a bare interface, humanize might not be installed."""
|
|
303
306
|
if naturalsize_:
|
|
@@ -346,6 +349,7 @@ def allows_none(annotation) -> bool:
|
|
|
346
349
|
return any(arg is type(None) for arg in args)
|
|
347
350
|
return False
|
|
348
351
|
|
|
352
|
+
|
|
349
353
|
def strip_none(annotation):
|
|
350
354
|
"""Return the same annotation but without NoneType inside a Union/Optional."""
|
|
351
355
|
origin = get_origin(annotation)
|
|
@@ -358,7 +362,8 @@ def strip_none(annotation):
|
|
|
358
362
|
|
|
359
363
|
return annotation
|
|
360
364
|
|
|
361
|
-
|
|
365
|
+
|
|
366
|
+
@lru_cache(maxsize=1024 * 10)
|
|
362
367
|
def _get_origin(tp: Any):
|
|
363
368
|
"""
|
|
364
369
|
Cached version of typing.get_origin.
|
|
@@ -15,14 +15,17 @@ class CliFlags:
|
|
|
15
15
|
default_verbosity: int = logging.WARNING
|
|
16
16
|
_verbosity_sequence: Optional[Sequence[int]] = None
|
|
17
17
|
|
|
18
|
+
config: bool = False
|
|
19
|
+
|
|
18
20
|
def __init__(
|
|
19
21
|
self,
|
|
20
22
|
add_verbose: bool | int | Sequence[int] = False,
|
|
21
23
|
add_version: Optional[str] = None,
|
|
22
24
|
add_version_package: Optional[str] = None,
|
|
23
25
|
add_quiet: bool = False,
|
|
26
|
+
add_config: bool = False,
|
|
24
27
|
):
|
|
25
|
-
self._enabled = {"verbose": True, "version": True, "quiet": True}
|
|
28
|
+
self._enabled = {"verbose": True, "version": True, "quiet": True, "config": True}
|
|
26
29
|
# verbosity
|
|
27
30
|
match add_verbose:
|
|
28
31
|
case bool():
|
|
@@ -30,7 +33,7 @@ class CliFlags:
|
|
|
30
33
|
case int():
|
|
31
34
|
self._add_verbose = True
|
|
32
35
|
self.default_verbosity = add_verbose
|
|
33
|
-
self._verbosity_sequence = list(range(add_verbose-10, -1, -10))
|
|
36
|
+
self._verbosity_sequence = list(range(add_verbose - 10, -1, -10))
|
|
34
37
|
case list() | tuple():
|
|
35
38
|
self._add_verbose = True
|
|
36
39
|
self.default_verbosity = add_verbose[0]
|
|
@@ -50,13 +53,17 @@ class CliFlags:
|
|
|
50
53
|
except PackageNotFoundError:
|
|
51
54
|
self.version = f"package {add_version_package} not found"
|
|
52
55
|
|
|
56
|
+
# config
|
|
57
|
+
self.config = add_config
|
|
58
|
+
|
|
53
59
|
def should_add(self, env_classes: list[EnvClass]) -> bool:
|
|
54
60
|
# Flags are added only if neither the env_class nor any of the subcommands have the same-name flag already
|
|
55
61
|
self._enabled["verbose"] = self._add_verbose and self._attr_not_present("verbose", env_classes)
|
|
56
62
|
self._enabled["quiet"] = self._add_quiet and self._attr_not_present("quiet", env_classes)
|
|
57
63
|
self._enabled["version"] = self.version and self._attr_not_present("version", env_classes)
|
|
64
|
+
self._enabled["config"] = self.config and self._attr_not_present("config", env_classes)
|
|
58
65
|
|
|
59
|
-
return self.add_verbose or self.add_version or self.add_quiet
|
|
66
|
+
return self.add_verbose or self.add_version or self.add_quiet or self.add_config
|
|
60
67
|
|
|
61
68
|
def _attr_not_present(self, flag, env_classes):
|
|
62
69
|
return all(flag not in cl.__annotations__ for cl in env_classes)
|
|
@@ -73,6 +80,10 @@ class CliFlags:
|
|
|
73
80
|
def add_quiet(self):
|
|
74
81
|
return self._add_quiet and self._enabled["quiet"]
|
|
75
82
|
|
|
83
|
+
@property
|
|
84
|
+
def add_config(self):
|
|
85
|
+
return self.config and self._enabled["config"]
|
|
86
|
+
|
|
76
87
|
def get_log_level(self, count):
|
|
77
88
|
"""
|
|
78
89
|
Ex.
|
|
@@ -95,7 +106,7 @@ class CliFlags:
|
|
|
95
106
|
Returns:
|
|
96
107
|
int: log level
|
|
97
108
|
"""
|
|
98
|
-
if count == -1:
|
|
109
|
+
if count == -1: # quiet flag
|
|
99
110
|
return logging.ERROR
|
|
100
111
|
if not count:
|
|
101
112
|
return self.default_verbosity
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
#
|
|
4
4
|
from dataclasses import asdict
|
|
5
5
|
from functools import reduce
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from multiprocessing import Value
|
|
6
8
|
import sys
|
|
7
9
|
from collections import deque
|
|
8
|
-
from contextlib import ExitStack
|
|
10
|
+
from contextlib import ExitStack, redirect_stderr, redirect_stdout
|
|
9
11
|
from typing import Annotated, Optional, Sequence, Type, Union
|
|
10
12
|
from unittest.mock import patch
|
|
11
13
|
|
|
@@ -21,10 +23,11 @@ from .auxiliary import (
|
|
|
21
23
|
flatten,
|
|
22
24
|
)
|
|
23
25
|
from .dataclass_creation import (
|
|
24
|
-
ChosenSubcommand,
|
|
25
26
|
_unwrap_annotated,
|
|
26
27
|
choose_subcommand,
|
|
27
28
|
create_with_missing,
|
|
29
|
+
get_chosen,
|
|
30
|
+
pop_from_passage,
|
|
28
31
|
to_kebab_case,
|
|
29
32
|
)
|
|
30
33
|
from .form_dict import EnvClass, TagDict, dataclass_to_tagdict, MissingTagValue, dict_added_main
|
|
@@ -69,13 +72,18 @@ def assure_args(args: Optional[Sequence[str]] = None):
|
|
|
69
72
|
return args
|
|
70
73
|
|
|
71
74
|
|
|
75
|
+
def _subcommands_default_appliable(kwargs, _crawling):
|
|
76
|
+
if len(_crawling.get()):
|
|
77
|
+
return kwargs.get("subcommands_default")
|
|
78
|
+
|
|
79
|
+
|
|
72
80
|
def parse_cli(
|
|
73
81
|
env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
74
82
|
kwargs: dict,
|
|
75
83
|
m: "Mininterface",
|
|
76
84
|
cf: Optional[CliFlags] = None,
|
|
77
85
|
ask_for_missing: bool = True,
|
|
78
|
-
args: Optional[Sequence[str]] = None,
|
|
86
|
+
args: Optional[Sequence[str]] = None, # NOTE no more Optional, change the arg order
|
|
79
87
|
ask_on_empty_cli: Optional[bool] = None,
|
|
80
88
|
cli_settings: Optional[CliSettings] = None,
|
|
81
89
|
_crawled=None,
|
|
@@ -89,6 +97,7 @@ def parse_cli(
|
|
|
89
97
|
"""
|
|
90
98
|
# Xint: The depth we crawled into. The number of subcommands in args.
|
|
91
99
|
# NOTE ask_on_empty_cli might reveal all fields (in cli_parser), not just wrongs. Eg. when using a subparser `$ prog run`, reveal all subparsers.
|
|
100
|
+
_req_fields = _req_fields or {}
|
|
92
101
|
|
|
93
102
|
if isinstance(env_or_list, list):
|
|
94
103
|
# We have to convert the list of possible classes (subcommands) to union for tyro.
|
|
@@ -149,17 +158,82 @@ def parse_cli(
|
|
|
149
158
|
warn(f"Cannot apply {annotations} on Python <= 3.11.")
|
|
150
159
|
return type_form
|
|
151
160
|
|
|
161
|
+
#
|
|
162
|
+
# --- Begin to launch tyro.cli ---
|
|
163
|
+
# This will be divided into four sections.
|
|
164
|
+
# (A) First parse section
|
|
165
|
+
# (B) Re-parse with subcommand-config ensured section
|
|
166
|
+
# (C) The dialog missing section
|
|
167
|
+
# (D) The nothing was missing section
|
|
168
|
+
#
|
|
169
|
+
enforce_dialog = False
|
|
170
|
+
""" When subcommand-chooser was raised (hence the CLI input was not completely working and without mininterface it would raise an error),
|
|
171
|
+
we make sure we display whole CLI overview form at the end."""
|
|
172
|
+
|
|
152
173
|
try:
|
|
153
174
|
with ExitStack() as stack:
|
|
154
175
|
[stack.enter_context(p) for p in patches] # apply just the chosen mocks
|
|
176
|
+
|
|
177
|
+
# --- (A) First parse section ---
|
|
178
|
+
|
|
179
|
+
# Let me explain this awful structure.
|
|
180
|
+
# If we have subcommanded-config file, we first need the tyro to do the parsing as it leaks the crawled path (through the subcommands).
|
|
181
|
+
# Then, we can fill the kwargs['default'] from the subcommanded-config and do the second parsing with some field filled up.
|
|
182
|
+
buffer = StringIO()
|
|
183
|
+
helponly = False
|
|
155
184
|
try:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
185
|
+
# Why redirect_stdout? Help-text shows the defaults, which also uses the subcommanded-config.
|
|
186
|
+
with redirect_stdout(buffer):
|
|
187
|
+
try:
|
|
188
|
+
# Standard way.
|
|
189
|
+
env = cli(annot(type_form), args=args, registry=_custom_registry, **kwargs)
|
|
190
|
+
except BaseException:
|
|
191
|
+
# Why this exception handling? Try putting this out and test_strange_error_mitigation fails.
|
|
192
|
+
if len(env_classes) > 1 and kwargs.get("default"):
|
|
193
|
+
env = cli(
|
|
194
|
+
annot(kwargs["default"].__class__), args=args[1:], registry=_custom_registry, **kwargs
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
raise
|
|
198
|
+
except SystemExit as exception:
|
|
199
|
+
# This catch handling is just for the subcommanded-config.
|
|
200
|
+
# Not raising this exception means it worked well and we re-parse with the subcommand-config data just below.
|
|
201
|
+
if _crawled is None and exception.code == 0 and _subcommands_default_appliable(kwargs, _crawling):
|
|
202
|
+
# Help-text exception, continue here and try again with subcommands. As it raises SystemExit first,
|
|
203
|
+
# it will raise SystemExit in the second run too.
|
|
204
|
+
helponly = True
|
|
205
|
+
elif (
|
|
206
|
+
_crawled is None
|
|
207
|
+
and _subcommands_default_appliable(kwargs, _crawling)
|
|
208
|
+
and exception.code == 2
|
|
209
|
+
and failed_fields.get()
|
|
210
|
+
):
|
|
211
|
+
# Some fields are missing, directly try again. If it raises again
|
|
212
|
+
# (some fields are really missing which cannot be filled from the subcommanded-config),
|
|
213
|
+
# it will immediately raise again and trigger the (C) dialog missing section.
|
|
214
|
+
# If it worked (and no fields are missing), we continue here without triggering the (C) dialog missing section.
|
|
215
|
+
_crawled = True
|
|
216
|
+
env, enforce_dialog = _try_with_subcommands(
|
|
217
|
+
kwargs, m, args, type_form, env_classes, _custom_registry, annot, _req_fields
|
|
218
|
+
)
|
|
161
219
|
else:
|
|
220
|
+
# This is either a recurrent call from the (C) dialog missing section (and thus subcommand-config re-parsing was done),
|
|
221
|
+
# or there is no subcommand-config data and thus we continue as if this exception handling did not happen.
|
|
222
|
+
if content := buffer.getvalue():
|
|
223
|
+
print(content)
|
|
162
224
|
raise
|
|
225
|
+
|
|
226
|
+
# --- (B) Re-parse with subcommand-config ensured section ---
|
|
227
|
+
|
|
228
|
+
# Re-parse with subcommand-config.
|
|
229
|
+
# It either raises (if it raised before and subcommand-config did not bring the missing fields) or works well if it worked well before.
|
|
230
|
+
if _crawled is None and _subcommands_default_appliable(kwargs, _crawling):
|
|
231
|
+
# Why not catching enforce_dialog here? As we are here, calling tyro.cli worked for the first time.
|
|
232
|
+
# For sure then, there were no choose_subcommand dialog, subcommands for sure are all written in the CLI.
|
|
233
|
+
env, _ = _try_with_subcommands(
|
|
234
|
+
kwargs, None if helponly else m, args, type_form, env_classes, _custom_registry, annot, _req_fields
|
|
235
|
+
)
|
|
236
|
+
|
|
163
237
|
# Why setting m.env instead of putting into into a constructor of a new get_interface() call?
|
|
164
238
|
# 1. Getting the interface is a costly operation
|
|
165
239
|
# 2. There is this bug so that we need to use single interface:
|
|
@@ -170,17 +244,18 @@ def parse_cli(
|
|
|
170
244
|
# m = get_interface("gui")
|
|
171
245
|
# m.select([1,2,3])
|
|
172
246
|
m.env = env
|
|
173
|
-
except
|
|
174
|
-
|
|
247
|
+
except SystemExit as exception:
|
|
248
|
+
# --- (C) The dialog missing section ---
|
|
249
|
+
# Some fields are needed to be filled up.
|
|
250
|
+
if ask_for_missing and exception.code == 2 and failed_fields.get():
|
|
175
251
|
env = _dialog_missing(
|
|
176
252
|
env_classes, kwargs, m, cf, ask_for_missing, args, cli_settings, _crawled, _req_fields
|
|
177
253
|
)
|
|
178
254
|
|
|
179
255
|
if final_call:
|
|
180
256
|
# Ask for the wrong fields
|
|
181
|
-
# Why
|
|
257
|
+
# Why final_call? We display the wrong-fields-form only once in the `parse_cli` uppermost call.
|
|
182
258
|
_ensure_command_init(env, m)
|
|
183
|
-
|
|
184
259
|
try:
|
|
185
260
|
m.form(env)
|
|
186
261
|
except Cancelled as e:
|
|
@@ -201,6 +276,7 @@ def parse_cli(
|
|
|
201
276
|
# Parsing wrong fields failed. The program ends with a nice tyro message.
|
|
202
277
|
raise
|
|
203
278
|
else:
|
|
279
|
+
# --- (D) The nothing was missing section ---
|
|
204
280
|
dialog_raised = False
|
|
205
281
|
if final_call:
|
|
206
282
|
_ensure_command_init(env, m)
|
|
@@ -219,7 +295,7 @@ def parse_cli(
|
|
|
219
295
|
|
|
220
296
|
# Empty CLI → GUI edit
|
|
221
297
|
subcommand_count = len(_crawling.get())
|
|
222
|
-
if not dialog_raised and ask_on_empty_cli and len(
|
|
298
|
+
if not dialog_raised and (ask_on_empty_cli and len(args) <= subcommand_count) or enforce_dialog:
|
|
223
299
|
# Raise a dialog if the command line is empty.
|
|
224
300
|
# This still means empty because 'run' and 'message' are just subcommands: `program.py run message`
|
|
225
301
|
m.form()
|
|
@@ -228,6 +304,37 @@ def parse_cli(
|
|
|
228
304
|
return env, dialog_raised
|
|
229
305
|
|
|
230
306
|
|
|
307
|
+
def _try_with_subcommands(kwargs, m, args, type_form, env_classes, _custom_registry, annot, _req_fields):
|
|
308
|
+
"""This awful method is here to re-parse the tyro.cli with the subcommand-config"""
|
|
309
|
+
|
|
310
|
+
failed_fields.set([])
|
|
311
|
+
old_defs = kwargs.get("default", {})
|
|
312
|
+
if old_defs:
|
|
313
|
+
old_defs = asdict(old_defs)
|
|
314
|
+
passage = [cl_name for _, cl_name, _ in _crawling.get()]
|
|
315
|
+
|
|
316
|
+
if len(env_classes) > 1:
|
|
317
|
+
if len(passage):
|
|
318
|
+
env, cl_name = pop_from_passage(passage, env_classes)
|
|
319
|
+
if not old_defs:
|
|
320
|
+
old_defs = kwargs["subcommands_default_union"][cl_name]
|
|
321
|
+
subc = kwargs["subcommands_default"].get(cl_name)
|
|
322
|
+
else: # we should never come here
|
|
323
|
+
raise ValueError("Subcommands parsing failed")
|
|
324
|
+
else:
|
|
325
|
+
env = env_classes[0]
|
|
326
|
+
subc = kwargs["subcommands_default"]
|
|
327
|
+
kwargs["default"] = create_with_missing(env, old_defs, _req_fields, m, subc=subc, subc_passage=passage)
|
|
328
|
+
dialog_used = False
|
|
329
|
+
if hasattr(m, "__subcommand_dialog_used"):
|
|
330
|
+
delattr(m, "__subcommand_dialog_used")
|
|
331
|
+
dialog_used = True
|
|
332
|
+
|
|
333
|
+
env = cli(annot(type_form), args=args, registry=_custom_registry, **kwargs)
|
|
334
|
+
|
|
335
|
+
return env, dialog_used
|
|
336
|
+
|
|
337
|
+
|
|
231
338
|
def _apply_patches(cf: Optional[CliFlags], ask_for_missing, env_classes, kwargs):
|
|
232
339
|
patches = []
|
|
233
340
|
|
|
@@ -272,7 +379,7 @@ def _dialog_missing(
|
|
|
272
379
|
args: Optional[Sequence[str]],
|
|
273
380
|
cli_settings,
|
|
274
381
|
crawled,
|
|
275
|
-
req_fields:
|
|
382
|
+
req_fields: TagDict,
|
|
276
383
|
) -> EnvClass:
|
|
277
384
|
"""Some required arguments are missing. Determine which and ask for them.
|
|
278
385
|
|
|
@@ -288,10 +395,8 @@ def _dialog_missing(
|
|
|
288
395
|
* env – Tyro's merge of CLI and kwargs["default"].
|
|
289
396
|
|
|
290
397
|
"""
|
|
291
|
-
req_fields = req_fields or {}
|
|
292
|
-
|
|
293
398
|
# There are multiple dataclasses, query which is chosen
|
|
294
|
-
|
|
399
|
+
env_cl = _ensure_chosen_env(env_classes, args, m, kwargs)
|
|
295
400
|
|
|
296
401
|
if crawled is None:
|
|
297
402
|
# This is the first correction attempt.
|
|
@@ -302,16 +407,19 @@ def _dialog_missing(
|
|
|
302
407
|
# So in further run, there is no need to rebuild the data. We just process new failed_fields reported by tyro.
|
|
303
408
|
|
|
304
409
|
# Merge with the config file defaults.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
410
|
+
if len(env_classes) > 1:
|
|
411
|
+
disk = kwargs.get("subcommands_default_union", {})
|
|
412
|
+
else:
|
|
413
|
+
disk = asdict(dc) if (dc := kwargs.get("default")) else {}
|
|
414
|
+
crawled = True
|
|
415
|
+
kwargs["default"] = create_with_missing(
|
|
416
|
+
env_cl,
|
|
417
|
+
disk,
|
|
418
|
+
req_fields,
|
|
419
|
+
m,
|
|
420
|
+
subc=kwargs.get("subcommands_default"),
|
|
421
|
+
subc_passage=[cl_name for _, cl_name, _ in _crawling.get()],
|
|
422
|
+
)
|
|
315
423
|
|
|
316
424
|
missing_req = _fetch_currently_failed(req_fields)
|
|
317
425
|
""" Fields required and missing from CLI """
|
|
@@ -341,10 +449,15 @@ def _dialog_missing(
|
|
|
341
449
|
return env
|
|
342
450
|
|
|
343
451
|
|
|
344
|
-
def _ensure_chosen_env(env_classes, args, m):
|
|
452
|
+
def _ensure_chosen_env(env_classes, args, m, kwargs):
|
|
453
|
+
# NOTE by preference, handling subclasses union should be done
|
|
454
|
+
# by making an arbitrary dataclass, having single subcommands attribute.
|
|
455
|
+
# That way, all the mendling with the env_classes list would disappear from many places in the code as
|
|
456
|
+
# we already support subclasses in attribute – and this awful function would disappear.
|
|
345
457
|
env = None
|
|
346
458
|
if len(env_classes) == 1:
|
|
347
459
|
env = env_classes[0]
|
|
460
|
+
return env
|
|
348
461
|
elif len(args):
|
|
349
462
|
env = next(
|
|
350
463
|
(env for env in env_classes if to_kebab_case(env.__name__) == args[0]),
|
|
@@ -356,7 +469,14 @@ def _ensure_chosen_env(env_classes, args, m):
|
|
|
356
469
|
env = choose_subcommand(env_classes, m)
|
|
357
470
|
if not env:
|
|
358
471
|
raise NotImplementedError("This case of nested dataclasses is not implemented. Raise an issue please.")
|
|
359
|
-
|
|
472
|
+
|
|
473
|
+
cl_name = to_kebab_case(env.__name__)
|
|
474
|
+
if kwargs.get("subcommands_default"):
|
|
475
|
+
kwargs["subcommands_default"] = kwargs["subcommands_default"].get(cl_name)
|
|
476
|
+
if kwargs.get("subcommands_default_union"):
|
|
477
|
+
kwargs["subcommands_default_union"] = kwargs["subcommands_default_union"].get(cl_name)
|
|
478
|
+
|
|
479
|
+
return env
|
|
360
480
|
|
|
361
481
|
|
|
362
482
|
def _fetch_currently_failed(requireds) -> TagDict:
|
|
@@ -168,4 +168,4 @@ SubcommandPlaceholder.__name__ = "subcommand" # show just the shortcut in the C
|
|
|
168
168
|
# val: Message | Console
|
|
169
169
|
# m = run(Env) # here
|
|
170
170
|
# m = run([Message, Console]) # and here too
|
|
171
|
-
# Then, add is as a tip to Supported-types.md.
|
|
171
|
+
# Then, add is as a tip to Supported-types.md.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
1
2
|
import warnings
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Optional, Type
|
|
4
5
|
|
|
6
|
+
|
|
5
7
|
from ..settings import MininterfaceSettings
|
|
6
8
|
from .auxiliary import dataclass_asdict_no_defaults, merge_dicts
|
|
7
|
-
from .dataclass_creation import create_with_missing
|
|
9
|
+
from .dataclass_creation import create_with_missing, to_kebab_case
|
|
8
10
|
from .form_dict import EnvClass
|
|
9
11
|
|
|
10
12
|
try:
|
|
@@ -15,6 +17,7 @@ except ImportError:
|
|
|
15
17
|
|
|
16
18
|
raise DependencyRequired("basic")
|
|
17
19
|
|
|
20
|
+
|
|
18
21
|
def parse_config_file(
|
|
19
22
|
env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
20
23
|
config_file: Path | None = None,
|
|
@@ -23,7 +26,7 @@ def parse_config_file(
|
|
|
23
26
|
"""Fetches the config file into the program defaults kwargs["default"] and UI settings.
|
|
24
27
|
|
|
25
28
|
Args:
|
|
26
|
-
|
|
29
|
+
env_or_list: Class(es) with the configuration.
|
|
27
30
|
config_file: File to load YAML to be merged with the configuration.
|
|
28
31
|
You do not have to re-define all the settings in the config file, you can choose a few.
|
|
29
32
|
Kwargs:
|
|
@@ -32,30 +35,29 @@ def parse_config_file(
|
|
|
32
35
|
Returns:
|
|
33
36
|
Tuple of kwargs and dict (section 'mininterface' in the config file).
|
|
34
37
|
"""
|
|
35
|
-
if isinstance(env_or_list, list):
|
|
36
|
-
subcommands, env = env_or_list, None
|
|
37
|
-
else:
|
|
38
|
-
subcommands, env = None, env_or_list
|
|
39
|
-
|
|
40
|
-
# Load config file
|
|
41
|
-
if config_file and subcommands:
|
|
42
|
-
# Reading config files when using subcommands is not implemented.
|
|
43
|
-
# NOTE But might be now.
|
|
44
|
-
kwargs.pop("default", None)
|
|
45
|
-
warnings.warn(
|
|
46
|
-
f"Config file {config_file} is ignored because subcommands are used."
|
|
47
|
-
" It is not easy to set how this should work."
|
|
48
|
-
" Describe the developer your usecase so that they might implement this."
|
|
49
|
-
)
|
|
50
|
-
|
|
51
38
|
confopt = None
|
|
52
|
-
if "default" not in kwargs and
|
|
39
|
+
if "default" not in kwargs and config_file:
|
|
53
40
|
# Undocumented feature. User put a namespace into kwargs["default"]
|
|
54
41
|
# that already serves for defaults. We do not fetch defaults yet from a config file.
|
|
55
42
|
disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
|
|
56
43
|
try:
|
|
57
44
|
confopt = disk.pop("mininterface", None)
|
|
58
|
-
|
|
45
|
+
subc = {}
|
|
46
|
+
|
|
47
|
+
if isinstance(env_or_list, list):
|
|
48
|
+
kwargs["subcommands_default_union"] = {}
|
|
49
|
+
for cl in env_or_list:
|
|
50
|
+
cl_name = to_kebab_case(cl.__name__)
|
|
51
|
+
subc[cl_name] = {}
|
|
52
|
+
ooo = create_with_missing(cl, disk.get(cl_name, {}), subc=subc[cl_name])
|
|
53
|
+
kwargs["subcommands_default_union"][cl_name] = asdict(ooo)
|
|
54
|
+
# `kwargs["default"]` remains empty for now as there is no bare default that tyro would support as everything is hidden under the subcommands
|
|
55
|
+
|
|
56
|
+
else:
|
|
57
|
+
kwargs["default"] = create_with_missing(env_or_list, disk, subc=subc)
|
|
58
|
+
|
|
59
|
+
if subc:
|
|
60
|
+
kwargs["subcommands_default"] = subc
|
|
59
61
|
except TypeError:
|
|
60
62
|
raise SyntaxError(f"Config file parsing failed for {config_file}")
|
|
61
63
|
|
|
@@ -94,4 +96,4 @@ def ensure_settings_inheritance(
|
|
|
94
96
|
for key, value in vars(create_with_missing(_def_fact, confopt)).items():
|
|
95
97
|
if value is not MISSING_NONPROP:
|
|
96
98
|
setattr(runopt, key, value)
|
|
97
|
-
return runopt
|
|
99
|
+
return runopt
|