mininterface 1.0.2__tar.gz → 1.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mininterface-1.0.2 → mininterface-1.0.3}/PKG-INFO +1 -1
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/__init__.py +41 -22
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_lib/cli_parser.py +194 -75
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_lib/showcase.py +5 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_mininterface/adaptor.py +33 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_text_interface/adaptor.py +15 -2
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/textual_app.py +1 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/widgets.py +13 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_tk_interface/adaptor.py +8 -1
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_tk_interface/select_input.py +9 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_tk_interface/utils.py +102 -11
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/settings.py +63 -5
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/flag.py +1 -1
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/path_tag.py +11 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/tag.py +12 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/pyproject.toml +2 -1
- {mininterface-1.0.2 → mininterface-1.0.3}/LICENSE +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/README.md +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/__main__.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_lib/auxiliary.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_lib/form_dict.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_lib/redirectable.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_lib/start.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_mininterface/__init__.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_mininterface/mixin.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_text_interface/__init__.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/__init__.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/adaptor.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/button_contents.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/facet.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/file_picker_input.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/form_contents.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_textual_interface/style.tcss +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_tk_interface/__init__.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_tk_interface/date_entry.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_tk_interface/external_fix.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_tk_interface/facet.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_tk_interface/secret_entry.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_web_interface/__init__.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_web_interface/app.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_web_interface/child_adaptor.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/_web_interface/parent_adaptor.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/cli.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/exceptions.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/experimental.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/facet/__init__.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/interfaces.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/alias.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/callback_tag.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/datetime_tag.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/internal.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/secret_tag.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/select_tag.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/tag_factory.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/tag/type_stubs.py +0 -0
- {mininterface-1.0.2 → mininterface-1.0.3}/mininterface/validators.py +0 -0
|
@@ -16,7 +16,12 @@ from .tag.alias import Options, Validation
|
|
|
16
16
|
try:
|
|
17
17
|
from ._lib.start import ChooseSubcommandOverview, Start
|
|
18
18
|
from .cli import Command, SubcommandPlaceholder
|
|
19
|
-
from ._lib.cli_parser import
|
|
19
|
+
from ._lib.cli_parser import (
|
|
20
|
+
assure_args,
|
|
21
|
+
parse_cli,
|
|
22
|
+
parse_config_file,
|
|
23
|
+
parser_to_dataclass,
|
|
24
|
+
)
|
|
20
25
|
except DependencyRequired as e:
|
|
21
26
|
assure_args, parse_cli, parse_config_file, parser_to_dataclass = (e,) * 4
|
|
22
27
|
ChooseSubcommandOverview, Start, SubcommandPlaceholder = (e,) * 3
|
|
@@ -27,18 +32,27 @@ class _Empty:
|
|
|
27
32
|
pass
|
|
28
33
|
|
|
29
34
|
|
|
30
|
-
def run(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
def run(
|
|
36
|
+
env_or_list: Type[EnvClass] | list[Type[EnvClass]] | ArgumentParser | None = None,
|
|
37
|
+
ask_on_empty_cli: bool = False,
|
|
38
|
+
title: str = "",
|
|
39
|
+
config_file: Path | str | bool = True,
|
|
40
|
+
add_verbose: bool = True,
|
|
41
|
+
ask_for_missing: bool = True,
|
|
42
|
+
# We do not use InterfaceType as a type here because we want the documentation to show full alias:
|
|
43
|
+
interface: (
|
|
44
|
+
Type[Mininterface]
|
|
45
|
+
| Literal["gui"]
|
|
46
|
+
| Literal["tui"]
|
|
47
|
+
| Literal["text"]
|
|
48
|
+
| Literal["web"]
|
|
49
|
+
| None
|
|
50
|
+
) = None,
|
|
51
|
+
args: Optional[Sequence[str]] = None,
|
|
52
|
+
settings: Optional[MininterfaceSettings] = None,
|
|
53
|
+
**kwargs
|
|
54
|
+
) -> Mininterface[EnvClass]:
|
|
55
|
+
"""The main access, start here.
|
|
42
56
|
Wrap your configuration dataclass into `run` to access the interface. An interface is chosen automatically,
|
|
43
57
|
with the preference of the graphical one, regressed to a text interface for machines without display.
|
|
44
58
|
Besides, if given a configuration dataclass, the function enriches it with the CLI commands and possibly
|
|
@@ -59,9 +73,9 @@ def run(env_or_list: Type[EnvClass] | list[Type[EnvClass]] | ArgumentParser | No
|
|
|
59
73
|
```python
|
|
60
74
|
@dataclass
|
|
61
75
|
class Env:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
number: int = 3
|
|
77
|
+
text: str = ""
|
|
78
|
+
m = run(Env, ask_on_empty=True)
|
|
65
79
|
```
|
|
66
80
|
|
|
67
81
|
```bash
|
|
@@ -77,6 +91,7 @@ def run(env_or_list: Type[EnvClass] | list[Type[EnvClass]] | ArgumentParser | No
|
|
|
77
91
|
whose name stem is the same as the program's.
|
|
78
92
|
Ex: `program.py` will search for `program.yaml`.
|
|
79
93
|
If False, no config file is used.
|
|
94
|
+
See the [Config file](Config-file.md) section.
|
|
80
95
|
add_verbose: Adds the verbose flag that automatically sets the level to `logging.INFO` (*-v*) or `logging.DEBUG` (*-vv*).
|
|
81
96
|
|
|
82
97
|
```python
|
|
@@ -203,18 +218,24 @@ def run(env_or_list: Type[EnvClass] | list[Type[EnvClass]] | ArgumentParser | No
|
|
|
203
218
|
if superform_args is not None:
|
|
204
219
|
# Run Superform as multiple subcommands exist and we have to decide which one to run.
|
|
205
220
|
m = get_interface(interface, title, settings, None)
|
|
206
|
-
ChooseSubcommandOverview(
|
|
221
|
+
ChooseSubcommandOverview(
|
|
222
|
+
env_or_list, m, args=superform_args, ask_for_missing=ask_for_missing
|
|
223
|
+
)
|
|
207
224
|
return m # m with added `m.env`
|
|
208
225
|
|
|
209
226
|
# B) A single Env object, or a list of such objects (with one is being selected via args)
|
|
210
227
|
# C) No Env object
|
|
211
228
|
|
|
212
229
|
# Parse CLI arguments, possibly merged from a config file.
|
|
213
|
-
kwargs, settings = parse_config_file(
|
|
230
|
+
kwargs, settings = parse_config_file(
|
|
231
|
+
env_or_list or _Empty, config_file, settings, **kwargs
|
|
232
|
+
)
|
|
214
233
|
if env_or_list:
|
|
215
234
|
# B) single Env object
|
|
216
235
|
# Load configuration from CLI and a config file
|
|
217
|
-
env, wrong_fields = parse_cli(
|
|
236
|
+
env, wrong_fields = parse_cli(
|
|
237
|
+
env_or_list, kwargs, add_verbose, ask_for_missing, args
|
|
238
|
+
)
|
|
218
239
|
m = get_interface(interface, title, settings, env)
|
|
219
240
|
|
|
220
241
|
# Empty CLI → GUI edit
|
|
@@ -243,6 +264,4 @@ def run(env_or_list: Type[EnvClass] | list[Type[EnvClass]] | ArgumentParser | No
|
|
|
243
264
|
return m
|
|
244
265
|
|
|
245
266
|
|
|
246
|
-
__all__ = ["run", "Mininterface", "Tag",
|
|
247
|
-
"Cancelled",
|
|
248
|
-
"Validation", "Options"]
|
|
267
|
+
__all__ = ["run", "Mininterface", "Tag", "Cancelled", "Validation", "Options"]
|
|
@@ -8,16 +8,32 @@ import sys
|
|
|
8
8
|
import warnings
|
|
9
9
|
from argparse import Action, ArgumentParser
|
|
10
10
|
from contextlib import ExitStack
|
|
11
|
-
from dataclasses import (
|
|
12
|
-
|
|
11
|
+
from dataclasses import (
|
|
12
|
+
MISSING,
|
|
13
|
+
Field,
|
|
14
|
+
asdict,
|
|
15
|
+
dataclass,
|
|
16
|
+
field,
|
|
17
|
+
fields,
|
|
18
|
+
is_dataclass,
|
|
19
|
+
make_dataclass,
|
|
20
|
+
)
|
|
13
21
|
from pathlib import Path
|
|
14
22
|
from types import SimpleNamespace
|
|
15
|
-
from typing import (
|
|
16
|
-
|
|
23
|
+
from typing import (
|
|
24
|
+
Annotated,
|
|
25
|
+
Any,
|
|
26
|
+
Callable,
|
|
27
|
+
Optional,
|
|
28
|
+
Sequence,
|
|
29
|
+
Type,
|
|
30
|
+
Union,
|
|
31
|
+
get_args,
|
|
32
|
+
get_origin,
|
|
33
|
+
)
|
|
17
34
|
from unittest.mock import patch
|
|
18
35
|
|
|
19
|
-
from .auxiliary import
|
|
20
|
-
yield_annotations)
|
|
36
|
+
from .auxiliary import dataclass_asdict_no_defaults, merge_dicts, yield_annotations
|
|
21
37
|
from .form_dict import DataClass, EnvClass, MissingTagValue
|
|
22
38
|
from ..settings import MininterfaceSettings
|
|
23
39
|
from ..tag import Tag
|
|
@@ -33,17 +49,19 @@ try:
|
|
|
33
49
|
from tyro.extras import get_parser
|
|
34
50
|
except ImportError:
|
|
35
51
|
from ..exceptions import DependencyRequired
|
|
52
|
+
|
|
36
53
|
raise DependencyRequired("basic")
|
|
37
54
|
|
|
38
55
|
|
|
39
56
|
# Pydantic is not a project dependency, that is just an optional integration
|
|
40
57
|
try: # Pydantic is not a dependency but integration
|
|
41
58
|
from pydantic import BaseModel
|
|
59
|
+
|
|
42
60
|
pydantic = True
|
|
43
61
|
except ImportError:
|
|
44
62
|
pydantic = False
|
|
45
63
|
BaseModel = False
|
|
46
|
-
try:
|
|
64
|
+
try: # Attrs is not a dependency but integration
|
|
47
65
|
import attr
|
|
48
66
|
except ImportError:
|
|
49
67
|
attr = None
|
|
@@ -58,11 +76,11 @@ reraise: Optional[Callable] = None
|
|
|
58
76
|
|
|
59
77
|
|
|
60
78
|
class Patches:
|
|
61
|
-
"""
|
|
79
|
+
"""Various mocking patches."""
|
|
62
80
|
|
|
63
81
|
@staticmethod
|
|
64
82
|
def custom_error(self: TyroArgumentParser, message: str):
|
|
65
|
-
"""
|
|
83
|
+
"""Fetch missing required options in GUI.
|
|
66
84
|
On missing argument, tyro fail. We cannot determine which one was missing, except by intercepting
|
|
67
85
|
the error message function. Then, we reconstruct the missing options.
|
|
68
86
|
Thanks to this we will be able to invoke a UI dialog with the missing options only.
|
|
@@ -71,28 +89,38 @@ class Patches:
|
|
|
71
89
|
if not message.startswith("the following arguments are required:"):
|
|
72
90
|
return super(TyroArgumentParser, self).error(message)
|
|
73
91
|
eavesdrop = message
|
|
74
|
-
|
|
92
|
+
|
|
93
|
+
def reraise():
|
|
94
|
+
return super(TyroArgumentParser, self).error(message)
|
|
95
|
+
|
|
75
96
|
raise SystemExit(2) # will be catched
|
|
76
97
|
|
|
77
98
|
@staticmethod
|
|
78
99
|
def custom_init(self: TyroArgumentParser, *args, **kwargs):
|
|
79
100
|
super(TyroArgumentParser, self).__init__(*args, **kwargs)
|
|
80
|
-
default_prefix =
|
|
81
|
-
self.add_argument(
|
|
82
|
-
|
|
101
|
+
default_prefix = "-" if "-" in self.prefix_chars else self.prefix_chars[0]
|
|
102
|
+
self.add_argument(
|
|
103
|
+
default_prefix + "v",
|
|
104
|
+
default_prefix * 2 + "verbose",
|
|
105
|
+
action="count",
|
|
106
|
+
default=0,
|
|
107
|
+
help="Verbosity level. Can be used twice to increase.",
|
|
108
|
+
)
|
|
83
109
|
|
|
84
110
|
@staticmethod
|
|
85
111
|
def custom_parse_known_args(self: TyroArgumentParser, args=None, namespace=None):
|
|
86
|
-
namespace, args = super(TyroArgumentParser, self).parse_known_args(
|
|
112
|
+
namespace, args = super(TyroArgumentParser, self).parse_known_args(
|
|
113
|
+
args, namespace
|
|
114
|
+
)
|
|
87
115
|
# NOTE We may check that the Env does not have its own `verbose``
|
|
88
116
|
if hasattr(namespace, "verbose"):
|
|
89
117
|
if namespace.verbose > 0:
|
|
90
|
-
log_level = {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
118
|
+
log_level = {1: logging.INFO, 2: logging.DEBUG, 3: logging.NOTSET}.get(
|
|
119
|
+
namespace.verbose, logging.NOTSET
|
|
120
|
+
)
|
|
121
|
+
logging.basicConfig(
|
|
122
|
+
level=log_level, format="%(levelname)s - %(message)s"
|
|
123
|
+
)
|
|
96
124
|
delattr(namespace, "verbose")
|
|
97
125
|
return namespace, args
|
|
98
126
|
|
|
@@ -114,19 +142,23 @@ def assure_args(args: Optional[Sequence[str]] = None):
|
|
|
114
142
|
return args
|
|
115
143
|
|
|
116
144
|
|
|
117
|
-
def parse_cli(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
145
|
+
def parse_cli(
|
|
146
|
+
env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
147
|
+
kwargs: dict,
|
|
148
|
+
add_verbose: bool = True,
|
|
149
|
+
ask_for_missing: bool = True,
|
|
150
|
+
args: Optional[Sequence[str]] = None,
|
|
151
|
+
) -> tuple[EnvClass, WrongFields]:
|
|
152
|
+
"""Run the tyro parser to fetch program configuration from CLI"""
|
|
123
153
|
if isinstance(env_or_list, list):
|
|
124
154
|
# We have to convert the list of possible classes (subcommands) to union for tyro.
|
|
125
155
|
# We have to accept the list and not an union directly because we are not able
|
|
126
156
|
# to type hint a union type, only a union instance.
|
|
127
157
|
# def sugg(a: UnionType[EnvClass]) -> EnvClass: ...
|
|
128
158
|
# sugg(Subcommand1 | Subcommand2). -> IDE will not suggest anything
|
|
129
|
-
type_form = Union[
|
|
159
|
+
type_form = Union[
|
|
160
|
+
tuple(env_or_list)
|
|
161
|
+
] # Union[*env_or_list] not supported in Python3.10
|
|
130
162
|
env_classes = env_or_list
|
|
131
163
|
else:
|
|
132
164
|
type_form = env_or_list
|
|
@@ -142,14 +174,20 @@ def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
142
174
|
# Mock parser, inject special options into
|
|
143
175
|
patches = []
|
|
144
176
|
if ask_for_missing: # Get the missing flags from the parser
|
|
145
|
-
patches.append(patch.object(TyroArgumentParser,
|
|
177
|
+
patches.append(patch.object(TyroArgumentParser, "error", Patches.custom_error))
|
|
146
178
|
if add_verbose: # Mock parser to add verbosity
|
|
147
179
|
# The verbose flag is added only if neither the env_class nor any of the subcommands have the verbose flag already
|
|
148
180
|
if all("verbose" not in cl.__annotations__ for cl in env_classes):
|
|
149
|
-
patches.extend(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
patches.extend(
|
|
182
|
+
(
|
|
183
|
+
patch.object(TyroArgumentParser, "__init__", Patches.custom_init),
|
|
184
|
+
patch.object(
|
|
185
|
+
TyroArgumentParser,
|
|
186
|
+
"parse_known_args",
|
|
187
|
+
Patches.custom_parse_known_args,
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
)
|
|
153
191
|
|
|
154
192
|
# Run the parser, with the mocks
|
|
155
193
|
try:
|
|
@@ -178,12 +216,21 @@ def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
178
216
|
parser: ArgumentParser = get_parser(type_form, **kwargs)
|
|
179
217
|
subargs = args
|
|
180
218
|
elif len(args):
|
|
181
|
-
env = next(
|
|
219
|
+
env = next(
|
|
220
|
+
(
|
|
221
|
+
env
|
|
222
|
+
for env in env_classes
|
|
223
|
+
if to_kebab_case(env.__name__) == args[0]
|
|
224
|
+
),
|
|
225
|
+
None,
|
|
226
|
+
)
|
|
182
227
|
if env:
|
|
183
228
|
parser: ArgumentParser = get_parser(env)
|
|
184
229
|
subargs = args[1:]
|
|
185
230
|
if not env:
|
|
186
|
-
raise NotImplemented(
|
|
231
|
+
raise NotImplemented(
|
|
232
|
+
"This case of nested dataclasses is not implemented. Raise an issue please."
|
|
233
|
+
)
|
|
187
234
|
|
|
188
235
|
# Determine missing argument of the given dataclass
|
|
189
236
|
positionals = (p for p in parser._actions if p.default != argparse.SUPPRESS)
|
|
@@ -194,12 +241,16 @@ def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
194
241
|
# Positional
|
|
195
242
|
# Ex: `The following arguments are required: PATH, INT, STR`
|
|
196
243
|
argument = next(positionals)
|
|
197
|
-
register_wrong_field(
|
|
244
|
+
register_wrong_field(
|
|
245
|
+
env, kwargs, wf, argument, exception, eavesdrop
|
|
246
|
+
)
|
|
198
247
|
else:
|
|
199
248
|
# required arguments
|
|
200
249
|
# Ex: `the following arguments are required: --foo, --bar`
|
|
201
|
-
if argument := identify_required(parser,
|
|
202
|
-
register_wrong_field(
|
|
250
|
+
if argument := identify_required(parser, arg):
|
|
251
|
+
register_wrong_field(
|
|
252
|
+
env, kwargs, wf, argument, exception, eavesdrop
|
|
253
|
+
)
|
|
203
254
|
|
|
204
255
|
# Second attempt to parse CLI.
|
|
205
256
|
# We have just put a default values for missing fields so that tyro will not fail.
|
|
@@ -213,7 +264,7 @@ def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
213
264
|
# (This is not true anymore; to support pydantic we put a default value of the type,
|
|
214
265
|
# so there is probably no more warning to be caught.)
|
|
215
266
|
with warnings.catch_warnings():
|
|
216
|
-
warnings.simplefilter(
|
|
267
|
+
warnings.simplefilter("ignore")
|
|
217
268
|
try:
|
|
218
269
|
env = cli(env, args=subargs, **kwargs)
|
|
219
270
|
except AssertionError:
|
|
@@ -243,7 +294,9 @@ def identify_required(parser: ArgumentParser, arg: str) -> None | Action:
|
|
|
243
294
|
# we should never come here, as treating missing subcommand should be treated by run/start.choose_subcommand
|
|
244
295
|
return
|
|
245
296
|
try:
|
|
246
|
-
argument: Action = next(
|
|
297
|
+
argument: Action = next(
|
|
298
|
+
iter(p for p in parser._actions if arg in p.option_strings)
|
|
299
|
+
)
|
|
247
300
|
except:
|
|
248
301
|
# missing subcommand flag not implemented (correction: might be implemented and we never come here anymore)
|
|
249
302
|
return
|
|
@@ -268,8 +321,15 @@ def argument_to_field_name(env_class: EnvClass, argument: Action):
|
|
|
268
321
|
return field_name
|
|
269
322
|
|
|
270
323
|
|
|
271
|
-
def register_wrong_field(
|
|
272
|
-
|
|
324
|
+
def register_wrong_field(
|
|
325
|
+
env_class: EnvClass,
|
|
326
|
+
kwargs: dict,
|
|
327
|
+
wf: dict,
|
|
328
|
+
argument: Action,
|
|
329
|
+
exception: BaseException,
|
|
330
|
+
eavesdrop,
|
|
331
|
+
):
|
|
332
|
+
"""The field is missing.
|
|
273
333
|
We prepare it to the list of wrong fields to be filled up
|
|
274
334
|
and make a temporary default value so that tyro will not fail.
|
|
275
335
|
"""
|
|
@@ -277,12 +337,13 @@ def register_wrong_field(env_class: EnvClass, kwargs: dict, wf: dict, argument:
|
|
|
277
337
|
# NOTE: We put MissingTagValue to the UI to clearly state that the value is missing.
|
|
278
338
|
# However, the UI then is not able to use ex. the number filtering capabilities.
|
|
279
339
|
# Putting there None is not a good idea as dataclass_to_tagdict fails if None is not allowed by the annotation.
|
|
280
|
-
tag = wf[field_name] = tag_factory(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
340
|
+
tag = wf[field_name] = tag_factory(
|
|
341
|
+
MissingTagValue(exception, eavesdrop),
|
|
342
|
+
(argument.help or "").replace("(required)", ""),
|
|
343
|
+
validation=not_empty,
|
|
344
|
+
_src_class=env_class,
|
|
345
|
+
_src_key=field_name,
|
|
346
|
+
)
|
|
286
347
|
# Why `_make_default_value`? We need to put a default value so that the parsing will not fail.
|
|
287
348
|
# A None would be enough because Mininterface will ask for the missing values
|
|
288
349
|
# promply, however, Pydantic model would fail.
|
|
@@ -300,11 +361,13 @@ def set_default(kwargs, field_name, val):
|
|
|
300
361
|
setattr(kwargs["default"], field_name, val)
|
|
301
362
|
|
|
302
363
|
|
|
303
|
-
def parse_config_file(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
364
|
+
def parse_config_file(
|
|
365
|
+
env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
366
|
+
config_file: Path | None = None,
|
|
367
|
+
settings: Optional[MininterfaceSettings] = None,
|
|
368
|
+
**kwargs,
|
|
369
|
+
) -> tuple[dict, MininterfaceSettings | None]:
|
|
370
|
+
"""Fetches the config file into the program defaults kwargs["default"] and UI settings.
|
|
308
371
|
|
|
309
372
|
Args:
|
|
310
373
|
env_class: Class with the configuration.
|
|
@@ -326,24 +389,31 @@ def parse_config_file(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
326
389
|
if config_file and subcommands:
|
|
327
390
|
# Reading config files when using subcommands is not implemented.
|
|
328
391
|
kwargs.pop("default", None)
|
|
329
|
-
warnings.warn(
|
|
330
|
-
|
|
331
|
-
|
|
392
|
+
warnings.warn(
|
|
393
|
+
f"Config file {config_file} is ignored because subcommands are used."
|
|
394
|
+
" It is not easy to set how this should work."
|
|
395
|
+
" Describe the developer your usecase so that they might implement this."
|
|
396
|
+
)
|
|
332
397
|
|
|
333
398
|
if "default" not in kwargs and not subcommands and config_file:
|
|
334
399
|
# Undocumented feature. User put a namespace into kwargs["default"]
|
|
335
400
|
# that already serves for defaults. We do not fetch defaults yet from a config file.
|
|
336
401
|
disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
402
|
+
try:
|
|
403
|
+
if confopt := disk.pop("mininterface", None):
|
|
404
|
+
# Section 'mininterface' in the config file.
|
|
405
|
+
settings = _merge_settings(settings, confopt)
|
|
340
406
|
|
|
341
|
-
|
|
407
|
+
kwargs["default"] = _create_with_missing(env, disk)
|
|
408
|
+
except TypeError:
|
|
409
|
+
raise SyntaxError(f"Config file parsing failed for {config_file}")
|
|
342
410
|
|
|
343
411
|
return kwargs, settings
|
|
344
412
|
|
|
345
413
|
|
|
346
|
-
def _merge_settings(
|
|
414
|
+
def _merge_settings(
|
|
415
|
+
runopt: MininterfaceSettings | None, confopt: dict, _def_fact=MininterfaceSettings
|
|
416
|
+
) -> MininterfaceSettings:
|
|
347
417
|
# Settings inheritance:
|
|
348
418
|
# Config file > program-given through run(settings=) > the default settings (original dataclasses)
|
|
349
419
|
|
|
@@ -357,14 +427,18 @@ def _merge_settings(runopt: MininterfaceSettings | None, confopt: dict, _def_fac
|
|
|
357
427
|
|
|
358
428
|
# Merge option sections.
|
|
359
429
|
# Ex: TextSettings will derive from both Tui and Ui. You may specify a Tui default value, common for all Tui interfaces.
|
|
360
|
-
for sources in [
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
430
|
+
for sources in [
|
|
431
|
+
("ui", "gui"),
|
|
432
|
+
("ui", "tui"),
|
|
433
|
+
("ui", "tui", "textual"),
|
|
434
|
+
("ui", "tui", "text"),
|
|
435
|
+
("ui", "tui", "textual", "web"),
|
|
436
|
+
]:
|
|
366
437
|
target = sources[-1]
|
|
367
|
-
confopt[target] = {
|
|
438
|
+
confopt[target] = {
|
|
439
|
+
**{k: v for s in sources for k, v in confopt.get(s, {}).items()},
|
|
440
|
+
**confopt.get(target, {}),
|
|
441
|
+
}
|
|
368
442
|
|
|
369
443
|
for key, value in vars(_create_with_missing(_def_fact, confopt)).items():
|
|
370
444
|
if value is not MISSING_NONPROP:
|
|
@@ -372,6 +446,47 @@ def _merge_settings(runopt: MininterfaceSettings | None, confopt: dict, _def_fac
|
|
|
372
446
|
return runopt
|
|
373
447
|
|
|
374
448
|
|
|
449
|
+
def coerce_type_to_annotation(value, annotation):
|
|
450
|
+
"""
|
|
451
|
+
Coerce value (e.g. list) to expected type (e.g. tuple[int, int]).
|
|
452
|
+
Only handles basic cases: tuple[...] from list, and recurses if needed.
|
|
453
|
+
"""
|
|
454
|
+
if annotation is None:
|
|
455
|
+
return value
|
|
456
|
+
|
|
457
|
+
annotation = _unwrap_annotated(annotation)
|
|
458
|
+
origin = get_origin(annotation)
|
|
459
|
+
|
|
460
|
+
# Handle tuple[...] conversion
|
|
461
|
+
if origin is tuple and isinstance(value, list):
|
|
462
|
+
args = get_args(annotation)
|
|
463
|
+
if args and len(args) == len(value):
|
|
464
|
+
return tuple(
|
|
465
|
+
coerce_type_to_annotation(v, arg) for v, arg in zip(value, args)
|
|
466
|
+
)
|
|
467
|
+
return tuple(value)
|
|
468
|
+
|
|
469
|
+
# Handle list[...] conversion
|
|
470
|
+
if origin is list and isinstance(value, list):
|
|
471
|
+
args = get_args(annotation)
|
|
472
|
+
if args:
|
|
473
|
+
return [coerce_type_to_annotation(v, args[0]) for v in value]
|
|
474
|
+
return value
|
|
475
|
+
|
|
476
|
+
# Handle dict[...] conversion
|
|
477
|
+
if origin is dict and isinstance(value, dict):
|
|
478
|
+
key_type, val_type = get_args(annotation)
|
|
479
|
+
return {
|
|
480
|
+
coerce_type_to_annotation(k, key_type): coerce_type_to_annotation(
|
|
481
|
+
v, val_type
|
|
482
|
+
)
|
|
483
|
+
for k, v in value.items()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
# For nested dataclass or BaseModel etc.
|
|
487
|
+
return value
|
|
488
|
+
|
|
489
|
+
|
|
375
490
|
def _unwrap_annotated(tp):
|
|
376
491
|
"""
|
|
377
492
|
Annotated[Inner, ...] -> `Inner`,
|
|
@@ -435,7 +550,7 @@ def _process_pydantic(env, disk):
|
|
|
435
550
|
if isinstance(f.default, BaseModel):
|
|
436
551
|
v = _create_with_missing(f.default.__class__, disk[name])
|
|
437
552
|
else:
|
|
438
|
-
v = disk[name]
|
|
553
|
+
v = coerce_type_to_annotation(disk[name], f.annotation)
|
|
439
554
|
elif f.default is not None:
|
|
440
555
|
v = f.default
|
|
441
556
|
yield name, v
|
|
@@ -447,7 +562,7 @@ def _process_attr(env, disk):
|
|
|
447
562
|
if attr.has(f.default):
|
|
448
563
|
v = _create_with_missing(f.default.__class__, disk[f.name])
|
|
449
564
|
else:
|
|
450
|
-
v = disk[f.name]
|
|
565
|
+
v = coerce_type_to_annotation(disk[f.name], f.type)
|
|
451
566
|
elif f.default is not attr.NOTHING:
|
|
452
567
|
v = f.default
|
|
453
568
|
else:
|
|
@@ -463,7 +578,7 @@ def _process_dataclass(env, disk):
|
|
|
463
578
|
if is_dataclass(_unwrap_annotated(f.type)):
|
|
464
579
|
v = _create_with_missing(f.type, disk[f.name])
|
|
465
580
|
else:
|
|
466
|
-
v = disk[f.name]
|
|
581
|
+
v = coerce_type_to_annotation(disk[f.name], f.type)
|
|
467
582
|
elif f.default_factory is not MISSING:
|
|
468
583
|
v = f.default_factory()
|
|
469
584
|
elif f.default is not MISSING:
|
|
@@ -474,7 +589,7 @@ def _process_dataclass(env, disk):
|
|
|
474
589
|
|
|
475
590
|
|
|
476
591
|
def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> DataClass:
|
|
477
|
-
"""
|
|
592
|
+
"""Note that in contrast to the argparse, we create default values.
|
|
478
593
|
When an optional flag is not used, argparse put None, we have a default value.
|
|
479
594
|
|
|
480
595
|
This does make sense for most values and should not pose problems for truthy-values.
|
|
@@ -502,7 +617,9 @@ def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> DataClass
|
|
|
502
617
|
arg_type = list[action.type or str]
|
|
503
618
|
opt["default_factory"] = list
|
|
504
619
|
else:
|
|
505
|
-
if isinstance(
|
|
620
|
+
if isinstance(
|
|
621
|
+
action, (argparse._StoreTrueAction, argparse._StoreFalseAction)
|
|
622
|
+
):
|
|
506
623
|
arg_type = bool
|
|
507
624
|
elif isinstance(action, argparse._StoreConstAction):
|
|
508
625
|
arg_type = type(action.const)
|
|
@@ -520,7 +637,9 @@ def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> DataClass
|
|
|
520
637
|
# nevertheless.
|
|
521
638
|
# Ex. parser.add_argument("--time", type=time) -> does work poorly in argparse.
|
|
522
639
|
action.default = Tag(annotation=arg_type)._make_default_value()
|
|
523
|
-
opt["default"] =
|
|
640
|
+
opt["default"] = (
|
|
641
|
+
action.default if action.default != argparse.SUPPRESS else None
|
|
642
|
+
)
|
|
524
643
|
|
|
525
644
|
# build a dataclass field, either optional, or positional
|
|
526
645
|
met = {"metadata": {"help": action.help}}
|
|
@@ -533,6 +652,6 @@ def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> DataClass
|
|
|
533
652
|
|
|
534
653
|
|
|
535
654
|
def to_kebab_case(name: str) -> str:
|
|
536
|
-
"""
|
|
655
|
+
"""MyClass -> my-class"""
|
|
537
656
|
# I did not find where tyro does it. If I find it, I might use its function instead.
|
|
538
|
-
return re.sub(r
|
|
657
|
+
return re.sub(r"(?<!^)(?=[A-Z])", "-", name).lower()
|
|
@@ -5,6 +5,8 @@ from typing import Annotated, Literal
|
|
|
5
5
|
|
|
6
6
|
from tyro.conf import Positional
|
|
7
7
|
|
|
8
|
+
from ..tag.select_tag import SelectTag
|
|
9
|
+
|
|
8
10
|
from ..exceptions import ValidationFail
|
|
9
11
|
from ..cli import Command, SubcommandPlaceholder
|
|
10
12
|
from ..tag.secret_tag import SecretTag
|
|
@@ -85,6 +87,9 @@ class Env:
|
|
|
85
87
|
my_choice: Annotated[str, Options("one", "two", "three")] = "two"
|
|
86
88
|
""" Choose between values """
|
|
87
89
|
|
|
90
|
+
my_multiple: Annotated[str, SelectTag(options=("one", "two", "three"), multiple=True)] = "two"
|
|
91
|
+
""" Choose values """
|
|
92
|
+
|
|
88
93
|
|
|
89
94
|
def showcase(case: int):
|
|
90
95
|
kw = {"args": []}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
+
from itertools import chain
|
|
3
|
+
from string import ascii_lowercase
|
|
2
4
|
from typing import TYPE_CHECKING, Callable, Optional
|
|
3
5
|
|
|
4
6
|
from .._lib.auxiliary import flatten
|
|
@@ -38,6 +40,37 @@ class BackendAdaptor(ABC):
|
|
|
38
40
|
Setups the facet._fetch_from_adaptor.
|
|
39
41
|
"""
|
|
40
42
|
self.facet._fetch_from_adaptor(form)
|
|
43
|
+
if self.settings.mnemonic is not False:
|
|
44
|
+
self._determine_mnemonic(form, self.settings.mnemonic is True)
|
|
45
|
+
|
|
46
|
+
def _determine_mnemonic(self, form: TagDict, also_nones=False):
|
|
47
|
+
""" also_nones – Also determine those tags when Tag.mnemonic=None. """
|
|
48
|
+
# Determine mnemonic
|
|
49
|
+
used_mnemonic = set()
|
|
50
|
+
to_be_determined: list[Tag] = []
|
|
51
|
+
tags = list(flatten(form))
|
|
52
|
+
if len(tags) <= 1: # do not use mnemonic for single field which is focused by default
|
|
53
|
+
return
|
|
54
|
+
for tag in tags:
|
|
55
|
+
if tag.mnemonic is False:
|
|
56
|
+
continue
|
|
57
|
+
if isinstance(tag.mnemonic, str):
|
|
58
|
+
used_mnemonic.add(tag.mnemonic)
|
|
59
|
+
tag._mnemonic = tag.mnemonic
|
|
60
|
+
elif also_nones or tag.mnemonic:
|
|
61
|
+
# .settings.mnemonic=None + tag.mnemonic=True OR
|
|
62
|
+
# .settings.mnemonic=True + tag.mnemonic=None
|
|
63
|
+
to_be_determined.append(tag)
|
|
64
|
+
|
|
65
|
+
# Find free mnemonic for Tag
|
|
66
|
+
for tag in to_be_determined:
|
|
67
|
+
# try every char in label
|
|
68
|
+
# then, if no free letter, give a random letter
|
|
69
|
+
for c in chain((c.lower() for c in tag.label if c.isalpha()), ascii_lowercase):
|
|
70
|
+
if c not in used_mnemonic:
|
|
71
|
+
used_mnemonic.add(c)
|
|
72
|
+
tag._mnemonic = c
|
|
73
|
+
break
|
|
41
74
|
|
|
42
75
|
def submit_done(self) -> bool:
|
|
43
76
|
if action := self.post_submit_action:
|