mininterface 1.0.3__tar.gz → 1.0.4__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.3 → mininterface-1.0.4}/PKG-INFO +5 -5
- {mininterface-1.0.3 → mininterface-1.0.4}/README.md +4 -4
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/__init__.py +10 -19
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/__main__.py +20 -8
- mininterface-1.0.4/mininterface/_lib/argparse_support.py +284 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_lib/auxiliary.py +27 -12
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_lib/cli_parser.py +20 -103
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_lib/form_dict.py +9 -9
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_lib/redirectable.py +3 -2
- mininterface-1.0.4/mininterface/_lib/shortcuts.py +59 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_lib/showcase.py +1 -1
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_lib/start.py +3 -2
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_mininterface/__init__.py +83 -74
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_mininterface/adaptor.py +8 -6
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_mininterface/mixin.py +12 -12
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_text_interface/__init__.py +21 -16
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_text_interface/adaptor.py +19 -10
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/__init__.py +3 -1
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/adaptor.py +26 -14
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/button_contents.py +3 -3
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/facet.py +2 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/file_picker_input.py +2 -5
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/form_contents.py +11 -7
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/widgets.py +3 -2
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_tk_interface/__init__.py +8 -2
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_tk_interface/adaptor.py +23 -25
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_tk_interface/date_entry.py +41 -36
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_tk_interface/external_fix.py +2 -6
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_tk_interface/facet.py +1 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_tk_interface/redirect_text_tkinter.py +4 -4
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_tk_interface/secret_entry.py +12 -13
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_tk_interface/select_input.py +29 -29
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_tk_interface/utils.py +25 -20
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_web_interface/__init__.py +16 -10
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_web_interface/app.py +1 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_web_interface/child_adaptor.py +1 -2
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_web_interface/parent_adaptor.py +2 -7
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/cli.py +9 -7
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/exceptions.py +10 -8
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/experimental.py +6 -4
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/facet/__init__.py +8 -7
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/interfaces.py +10 -4
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/settings.py +12 -18
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/alias.py +2 -2
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/callback_tag.py +2 -1
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/datetime_tag.py +53 -8
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/flag.py +8 -4
- mininterface-1.0.4/mininterface/tag/internal.py +10 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/path_tag.py +12 -4
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/select_tag.py +23 -11
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/tag.py +89 -74
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/tag_factory.py +14 -8
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/type_stubs.py +4 -2
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/validators.py +27 -12
- {mininterface-1.0.3 → mininterface-1.0.4}/pyproject.toml +4 -1
- mininterface-1.0.3/mininterface/tag/internal.py +0 -10
- {mininterface-1.0.3 → mininterface-1.0.4}/LICENSE +0 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/style.tcss +0 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/_textual_interface/textual_app.py +0 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.0.3 → mininterface-1.0.4}/mininterface/tag/secret_tag.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: A minimal access to GUI, TUI, CLI and config
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
Author: Edvard Rejthar
|
|
@@ -187,7 +187,7 @@ These projects have the code base reduced thanks to the mininterface:
|
|
|
187
187
|
Take a look at the following example.
|
|
188
188
|
|
|
189
189
|
1. We define any Env class.
|
|
190
|
-
2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be
|
|
190
|
+
2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be prompted for
|
|
191
191
|
3. Then, we use various dialog methods, like [`confirm`][mininterface.Mininterface.confirm], [`select`][mininterface.Mininterface.select] or [`form`][mininterface.Mininterface.form].
|
|
192
192
|
|
|
193
193
|
Below, you find the screenshots how the program looks in various environments ([graphic](Interfaces.md#guiinterface-or-tkinterface-or-gui) interface, [web](Interfaces.md#webinterface-or-web) interface...).
|
|
@@ -280,11 +280,11 @@ from pathlib import Path
|
|
|
280
280
|
from mininterface import run
|
|
281
281
|
|
|
282
282
|
parser = ArgumentParser()
|
|
283
|
-
parser.add_argument("input_file", type=Path, help="Path to the input file.")
|
|
284
|
-
parser.add_argument("--time", type=time, help="Given time")
|
|
285
283
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
286
284
|
sub1 = subparsers.add_parser("build", help="Build something.")
|
|
287
285
|
sub1.add_argument("--optimize", action="store_true", help="Enable optimizations.")
|
|
286
|
+
parser.add_argument("input_file", type=Path, help="Path to the input file.")
|
|
287
|
+
parser.add_argument("--time", type=time, help="Given time")
|
|
288
288
|
|
|
289
289
|
# Old version
|
|
290
290
|
# env = parser.parse_args()
|
|
@@ -335,4 +335,4 @@ print(m.env.time) # -> 14:21
|
|
|
335
335
|
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.
|
|
336
336
|
|
|
337
337
|
!!! warning
|
|
338
|
-
|
|
338
|
+
The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
|
|
@@ -149,7 +149,7 @@ These projects have the code base reduced thanks to the mininterface:
|
|
|
149
149
|
Take a look at the following example.
|
|
150
150
|
|
|
151
151
|
1. We define any Env class.
|
|
152
|
-
2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be
|
|
152
|
+
2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be prompted for
|
|
153
153
|
3. Then, we use various dialog methods, like [`confirm`][mininterface.Mininterface.confirm], [`select`][mininterface.Mininterface.select] or [`form`][mininterface.Mininterface.form].
|
|
154
154
|
|
|
155
155
|
Below, you find the screenshots how the program looks in various environments ([graphic](Interfaces.md#guiinterface-or-tkinterface-or-gui) interface, [web](Interfaces.md#webinterface-or-web) interface...).
|
|
@@ -242,11 +242,11 @@ from pathlib import Path
|
|
|
242
242
|
from mininterface import run
|
|
243
243
|
|
|
244
244
|
parser = ArgumentParser()
|
|
245
|
-
parser.add_argument("input_file", type=Path, help="Path to the input file.")
|
|
246
|
-
parser.add_argument("--time", type=time, help="Given time")
|
|
247
245
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
248
246
|
sub1 = subparsers.add_parser("build", help="Build something.")
|
|
249
247
|
sub1.add_argument("--optimize", action="store_true", help="Enable optimizations.")
|
|
248
|
+
parser.add_argument("input_file", type=Path, help="Path to the input file.")
|
|
249
|
+
parser.add_argument("--time", type=time, help="Given time")
|
|
250
250
|
|
|
251
251
|
# Old version
|
|
252
252
|
# env = parser.parse_args()
|
|
@@ -297,4 +297,4 @@ print(m.env.time) # -> 14:21
|
|
|
297
297
|
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
298
|
|
|
299
299
|
!!! warning
|
|
300
|
-
|
|
300
|
+
The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
|
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Literal, Optional, Sequence, Type
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
from .exceptions import Cancelled, DependencyRequired, InterfaceNotAvailable
|
|
9
10
|
from ._lib.form_dict import DataClass, EnvClass
|
|
10
11
|
from .interfaces import get_interface
|
|
@@ -16,11 +17,11 @@ from .tag.alias import Options, Validation
|
|
|
16
17
|
try:
|
|
17
18
|
from ._lib.start import ChooseSubcommandOverview, Start
|
|
18
19
|
from .cli import Command, SubcommandPlaceholder
|
|
20
|
+
from ._lib.argparse_support import parser_to_dataclass
|
|
19
21
|
from ._lib.cli_parser import (
|
|
20
22
|
assure_args,
|
|
21
23
|
parse_cli,
|
|
22
24
|
parse_config_file,
|
|
23
|
-
parser_to_dataclass,
|
|
24
25
|
)
|
|
25
26
|
except DependencyRequired as e:
|
|
26
27
|
assure_args, parse_cli, parse_config_file, parser_to_dataclass = (e,) * 4
|
|
@@ -40,14 +41,7 @@ def run(
|
|
|
40
41
|
add_verbose: bool = True,
|
|
41
42
|
ask_for_missing: bool = True,
|
|
42
43
|
# 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,
|
|
44
|
+
interface: Type[Mininterface] | Literal["gui"] | Literal["tui"] | Literal["text"] | Literal["web"] | None = None,
|
|
51
45
|
args: Optional[Sequence[str]] = None,
|
|
52
46
|
settings: Optional[MininterfaceSettings] = None,
|
|
53
47
|
**kwargs
|
|
@@ -218,24 +212,21 @@ def run(
|
|
|
218
212
|
if superform_args is not None:
|
|
219
213
|
# Run Superform as multiple subcommands exist and we have to decide which one to run.
|
|
220
214
|
m = get_interface(interface, title, settings, None)
|
|
221
|
-
|
|
222
|
-
env_or_list, m, args=superform_args, ask_for_missing=ask_for_missing
|
|
223
|
-
|
|
224
|
-
|
|
215
|
+
try:
|
|
216
|
+
ChooseSubcommandOverview(env_or_list, m, args=superform_args, ask_for_missing=ask_for_missing)
|
|
217
|
+
return m # m with added `m.env`
|
|
218
|
+
except Exception as e: # some nested subcommands would fail in overview
|
|
219
|
+
env_or_list = m.select({cl.__name__: cl for cl in env_or_list if cl is not SubcommandPlaceholder})
|
|
225
220
|
|
|
226
221
|
# B) A single Env object, or a list of such objects (with one is being selected via args)
|
|
227
222
|
# C) No Env object
|
|
228
223
|
|
|
229
224
|
# Parse CLI arguments, possibly merged from a config file.
|
|
230
|
-
kwargs, settings = parse_config_file(
|
|
231
|
-
env_or_list or _Empty, config_file, settings, **kwargs
|
|
232
|
-
)
|
|
225
|
+
kwargs, settings = parse_config_file(env_or_list or _Empty, config_file, settings, **kwargs)
|
|
233
226
|
if env_or_list:
|
|
234
227
|
# B) single Env object
|
|
235
228
|
# Load configuration from CLI and a config file
|
|
236
|
-
env, wrong_fields = parse_cli(
|
|
237
|
-
env_or_list, kwargs, add_verbose, ask_for_missing, args
|
|
238
|
-
)
|
|
229
|
+
env, wrong_fields = parse_cli(env_or_list, kwargs, add_verbose, ask_for_missing, args)
|
|
239
230
|
m = get_interface(interface, title, settings, env)
|
|
240
231
|
|
|
241
232
|
# Empty CLI → GUI edit
|
|
@@ -9,6 +9,7 @@ try:
|
|
|
9
9
|
from tyro.conf import Positional
|
|
10
10
|
except ImportError:
|
|
11
11
|
from .exceptions import DependencyRequired
|
|
12
|
+
|
|
12
13
|
raise DependencyRequired("basic").exit()
|
|
13
14
|
|
|
14
15
|
from . import run
|
|
@@ -36,7 +37,7 @@ Showcase_Type = Literal[1, 2]
|
|
|
36
37
|
|
|
37
38
|
@dataclass
|
|
38
39
|
class Alert(Command):
|
|
39
|
-
"""
|
|
40
|
+
"""Dialog: Display the OK dialog with text."""
|
|
40
41
|
|
|
41
42
|
text: Positional[str]
|
|
42
43
|
|
|
@@ -46,7 +47,7 @@ class Alert(Command):
|
|
|
46
47
|
|
|
47
48
|
@dataclass
|
|
48
49
|
class Ask(Command):
|
|
49
|
-
"""
|
|
50
|
+
"""Dialog: Prompt the user to input a value.
|
|
50
51
|
By default, we input a str, by the second parameter, you can infer a type,
|
|
51
52
|
ex. `mininterface --ask 'My heading' int`
|
|
52
53
|
"""
|
|
@@ -77,12 +78,15 @@ class Ask(Command):
|
|
|
77
78
|
v = Path
|
|
78
79
|
case "date":
|
|
79
80
|
from datetime import date
|
|
81
|
+
|
|
80
82
|
v = date
|
|
81
83
|
case "datetime":
|
|
82
84
|
from datetime import datetime
|
|
85
|
+
|
|
83
86
|
v = datetime
|
|
84
87
|
case "time":
|
|
85
88
|
from datetime import time
|
|
89
|
+
|
|
86
90
|
v = time
|
|
87
91
|
case "file":
|
|
88
92
|
v = PathTag(is_file=True)
|
|
@@ -95,7 +99,7 @@ class Ask(Command):
|
|
|
95
99
|
|
|
96
100
|
@dataclass
|
|
97
101
|
class Confirm(Command):
|
|
98
|
-
"""
|
|
102
|
+
"""Dialog: Display confirm box. Returns 0 / 1."""
|
|
99
103
|
|
|
100
104
|
text: Positional[str]
|
|
101
105
|
focus: Positional[Literal["yes", "no"]] = "yes"
|
|
@@ -108,7 +112,8 @@ class Confirm(Command):
|
|
|
108
112
|
|
|
109
113
|
@dataclass
|
|
110
114
|
class Select(Command):
|
|
111
|
-
"""
|
|
115
|
+
"""Dialog: Prompt the user to select."""
|
|
116
|
+
|
|
112
117
|
options: Positional[list[str]]
|
|
113
118
|
title: str = ""
|
|
114
119
|
|
|
@@ -118,7 +123,7 @@ class Select(Command):
|
|
|
118
123
|
|
|
119
124
|
@dataclass
|
|
120
125
|
class Integrate(Command):
|
|
121
|
-
"""
|
|
126
|
+
"""Integrate to the system. Generates a bash completion for the given program."""
|
|
122
127
|
|
|
123
128
|
cmd: Positional[File]
|
|
124
129
|
"""Path to the program using mininterface.
|
|
@@ -126,17 +131,18 @@ class Integrate(Command):
|
|
|
126
131
|
"""
|
|
127
132
|
|
|
128
133
|
def run(self):
|
|
129
|
-
environ["MININTERFACE_INTEGRATE_TO_SYSTEM"] =
|
|
134
|
+
environ["MININTERFACE_INTEGRATE_TO_SYSTEM"] = "1"
|
|
130
135
|
srun(self.cmd.absolute(), env=environ)
|
|
131
136
|
quit()
|
|
132
137
|
|
|
133
138
|
|
|
134
139
|
@dataclass
|
|
135
140
|
class Showcase:
|
|
136
|
-
"""
|
|
141
|
+
"""Prints various form just to show what's possible.
|
|
137
142
|
Choose the interface by MININTERFACE_INTERFACE=...
|
|
138
143
|
Ex. MININTERFACE_INTERFACE=tui mininterface showcase 2
|
|
139
144
|
"""
|
|
145
|
+
|
|
140
146
|
showcase: Positional[Showcase_Type] = 1
|
|
141
147
|
|
|
142
148
|
|
|
@@ -151,11 +157,17 @@ class Web(Command):
|
|
|
151
157
|
|
|
152
158
|
def run(self):
|
|
153
159
|
from ._web_interface import WebInterface
|
|
160
|
+
|
|
154
161
|
WebInterface(cmd=self.cmd, port=self.port)
|
|
155
162
|
|
|
156
163
|
|
|
157
164
|
def main():
|
|
158
|
-
with run(
|
|
165
|
+
with run(
|
|
166
|
+
[Alert, Ask, Confirm, Select, Integrate, Showcase, Web],
|
|
167
|
+
prog="Mininterface",
|
|
168
|
+
description=__doc__,
|
|
169
|
+
ask_for_missing=False,
|
|
170
|
+
) as m:
|
|
159
171
|
pass
|
|
160
172
|
|
|
161
173
|
if isinstance(m.env, Showcase):
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from argparse import (
|
|
2
|
+
SUPPRESS,
|
|
3
|
+
_AppendAction,
|
|
4
|
+
_AppendConstAction,
|
|
5
|
+
_CountAction,
|
|
6
|
+
_HelpAction,
|
|
7
|
+
_StoreConstAction,
|
|
8
|
+
_StoreFalseAction,
|
|
9
|
+
_StoreTrueAction,
|
|
10
|
+
_SubParsersAction,
|
|
11
|
+
_VersionAction,
|
|
12
|
+
Action,
|
|
13
|
+
ArgumentParser,
|
|
14
|
+
)
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from dataclasses import MISSING, Field, dataclass, field, make_dataclass
|
|
17
|
+
from functools import cached_property
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Annotated, Callable, Literal, Optional
|
|
21
|
+
from warnings import warn
|
|
22
|
+
|
|
23
|
+
from tyro.conf import OmitSubcommandPrefixes
|
|
24
|
+
|
|
25
|
+
from .. import Options
|
|
26
|
+
|
|
27
|
+
from .form_dict import DataClass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from tyro.constructors import PrimitiveConstructorSpec
|
|
32
|
+
from tyro.conf import Positional, arg
|
|
33
|
+
except ImportError:
|
|
34
|
+
from ..exceptions import DependencyRequired
|
|
35
|
+
|
|
36
|
+
raise DependencyRequired("basic")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Property:
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self._usages = []
|
|
42
|
+
|
|
43
|
+
def add(self, callback: Callable):
|
|
44
|
+
self._usages.append(callback)
|
|
45
|
+
|
|
46
|
+
def generate_property(self):
|
|
47
|
+
def _(this):
|
|
48
|
+
for clb in self._usages:
|
|
49
|
+
v = clb(this)
|
|
50
|
+
if v is not None:
|
|
51
|
+
return v
|
|
52
|
+
|
|
53
|
+
return property(_)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ArgparseField:
|
|
58
|
+
|
|
59
|
+
action: Action
|
|
60
|
+
properties: dict[str, Property]
|
|
61
|
+
|
|
62
|
+
@cached_property
|
|
63
|
+
def name(self):
|
|
64
|
+
if n := self.action.option_strings:
|
|
65
|
+
# --get-one → get_one
|
|
66
|
+
return re.sub(r"^--?", "", self.action.option_strings[0]).replace("-", "_")
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Cannot load argparse, due to field {self.action}")
|
|
69
|
+
|
|
70
|
+
def add(self, callback: Callable):
|
|
71
|
+
if self.action.dest == self.name:
|
|
72
|
+
raise NotImplementedError(
|
|
73
|
+
f"Cannot load argparse, due to field {self.action}. It must be visible from CLI and cannot"
|
|
74
|
+
"be read directly from the program. Solution: Do not use argparse or add a .dest parameter."
|
|
75
|
+
)
|
|
76
|
+
self.properties[self.action.dest].add(callback)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def has_property(self):
|
|
80
|
+
return self.action.dest in self.properties
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> DataClass | list[DataClass]:
|
|
84
|
+
"""
|
|
85
|
+
Note: Ex. parser.add_argument("--time", type=time) -> does work at all in argparse, here it works.
|
|
86
|
+
"""
|
|
87
|
+
subparsers: list[_SubParsersAction] = []
|
|
88
|
+
|
|
89
|
+
normal_actions: list[Action] = []
|
|
90
|
+
has_positionals = False
|
|
91
|
+
for action in parser._actions:
|
|
92
|
+
match action:
|
|
93
|
+
case _HelpAction():
|
|
94
|
+
continue
|
|
95
|
+
case _SubParsersAction():
|
|
96
|
+
if has_positionals:
|
|
97
|
+
warn(
|
|
98
|
+
"This CLI parser have a subcommand placed after positional arguments. The order of arguments changes, see --help."
|
|
99
|
+
)
|
|
100
|
+
subparsers.append(action)
|
|
101
|
+
case _:
|
|
102
|
+
if not action.option_strings:
|
|
103
|
+
has_positionals = True
|
|
104
|
+
normal_actions.append(action)
|
|
105
|
+
|
|
106
|
+
if subparsers:
|
|
107
|
+
return [
|
|
108
|
+
_make_dataclass_from_actions(
|
|
109
|
+
normal_actions + subactions._actions,
|
|
110
|
+
subname,
|
|
111
|
+
help_,
|
|
112
|
+
subactions.description,
|
|
113
|
+
)
|
|
114
|
+
for subparser in subparsers
|
|
115
|
+
for subname, subactions, help_ in _loop_SubParsersAction(subparser)
|
|
116
|
+
]
|
|
117
|
+
else:
|
|
118
|
+
return _make_dataclass_from_actions(normal_actions, name, None, parser.description)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _loop_SubParsersAction(subparser: _SubParsersAction):
|
|
122
|
+
return [
|
|
123
|
+
(subname, subactions, ch_act.help)
|
|
124
|
+
for (subname, subactions), ch_act in zip(subparser.choices.items(), subparser._choices_actions)
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _make_dataclass_from_actions(
|
|
129
|
+
actions: list[Action], name, helptext: str | None, description: str | None
|
|
130
|
+
) -> DataClass:
|
|
131
|
+
const_actions = defaultdict(list[ArgparseField])
|
|
132
|
+
normal_fields: list[tuple[str, type, Field]] = []
|
|
133
|
+
pos_fields: list[tuple[str, type, Field]] = []
|
|
134
|
+
properties = defaultdict(Property)
|
|
135
|
+
""" Sometimes, the action.dest differs from the field name.
|
|
136
|
+
Field name is exposed to the CLI, action.dest is used in the program.
|
|
137
|
+
"""
|
|
138
|
+
subparser_fields: list[tuple[str, type]] = []
|
|
139
|
+
|
|
140
|
+
for action in actions:
|
|
141
|
+
af = ArgparseField(action, properties)
|
|
142
|
+
opt = {}
|
|
143
|
+
|
|
144
|
+
match action:
|
|
145
|
+
case _HelpAction():
|
|
146
|
+
continue
|
|
147
|
+
case _VersionAction():
|
|
148
|
+
# NOTE Should be probably implemented in tyro. Here that way:
|
|
149
|
+
# run(add_version="1.2.3")
|
|
150
|
+
# run(add_version_package="intelmq") -> get pip version
|
|
151
|
+
arg_type = Annotated[
|
|
152
|
+
None,
|
|
153
|
+
PrimitiveConstructorSpec(
|
|
154
|
+
nargs="*",
|
|
155
|
+
metavar="",
|
|
156
|
+
instance_from_str=lambda _, v=action.version: print(v) or sys.exit(0),
|
|
157
|
+
is_instance=lambda _: True,
|
|
158
|
+
# NOTE tyro might not diplay anything here,
|
|
159
|
+
# but it displays `(default: )`
|
|
160
|
+
str_from_instance=(lambda _, v=action.version: [str(v)]),
|
|
161
|
+
),
|
|
162
|
+
]
|
|
163
|
+
case _SubParsersAction():
|
|
164
|
+
# Note that there is only one _SubParsersAction in argparse
|
|
165
|
+
# but to be sure, we allow multiple of them
|
|
166
|
+
# This probably makes a different CLI output than the original argparse but should work.
|
|
167
|
+
for subname, subparser, help_ in _loop_SubParsersAction(action):
|
|
168
|
+
sub_dc = _make_dataclass_from_actions(
|
|
169
|
+
subparser._actions,
|
|
170
|
+
subname.capitalize(),
|
|
171
|
+
help_,
|
|
172
|
+
subparser.description,
|
|
173
|
+
)
|
|
174
|
+
subparser_fields.append((subname, sub_dc)) # required, no default
|
|
175
|
+
|
|
176
|
+
from functools import reduce
|
|
177
|
+
|
|
178
|
+
union_type = reduce(lambda a, b: a | b, [aa[1] for aa in subparser_fields])
|
|
179
|
+
|
|
180
|
+
result = OmitSubcommandPrefixes[Positional[union_type]]
|
|
181
|
+
pos_fields.append(("_subparsers", result))
|
|
182
|
+
subparser_fields.clear()
|
|
183
|
+
continue
|
|
184
|
+
case _AppendAction():
|
|
185
|
+
arg_type = list[action.type or str]
|
|
186
|
+
opt["default_factory"] = list
|
|
187
|
+
case _AppendConstAction():
|
|
188
|
+
# `--one --two` -> env.section = [one, two]
|
|
189
|
+
arg_type = bool
|
|
190
|
+
opt["default"] = False
|
|
191
|
+
const_actions[af.action.dest].append(af)
|
|
192
|
+
af.add(
|
|
193
|
+
lambda self, af=af: (
|
|
194
|
+
[_af.action.const for _af in const_actions[af.action.dest] if getattr(self, _af.name)]
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
case _StoreTrueAction():
|
|
198
|
+
arg_type = bool
|
|
199
|
+
case _StoreFalseAction():
|
|
200
|
+
arg_type = bool
|
|
201
|
+
opt["default"] = False
|
|
202
|
+
af.add(lambda self, field_name=af.name: not getattr(self, field_name))
|
|
203
|
+
case _StoreConstAction():
|
|
204
|
+
arg_type = bool
|
|
205
|
+
opt["default"] = False
|
|
206
|
+
af.add(
|
|
207
|
+
lambda self, field_name=af.name, const=action.const: (const if getattr(self, field_name) else None)
|
|
208
|
+
)
|
|
209
|
+
case _CountAction():
|
|
210
|
+
arg_type = int
|
|
211
|
+
case _:
|
|
212
|
+
if action.type:
|
|
213
|
+
arg_type = action.type
|
|
214
|
+
elif action.default:
|
|
215
|
+
arg_type = type(action.default)
|
|
216
|
+
else:
|
|
217
|
+
arg_type = str
|
|
218
|
+
|
|
219
|
+
metavar = None
|
|
220
|
+
if "default" not in opt and "default_factory" not in opt:
|
|
221
|
+
if action.choices:
|
|
222
|
+
# With the drop of Python 3.10, use:
|
|
223
|
+
# arg_type = Literal[*action.choices]
|
|
224
|
+
arg_type = Annotated[arg_type, Options(*action.choices)]
|
|
225
|
+
|
|
226
|
+
if not action.option_strings and action.default is None and action.nargs != "?":
|
|
227
|
+
opt["default"] = MISSING
|
|
228
|
+
else:
|
|
229
|
+
if action.default is None:
|
|
230
|
+
# parser.add_argument("--path", type=Path) -> becomes None, not Path('.').
|
|
231
|
+
# By default, argparse put None if not used in the CLI.
|
|
232
|
+
# Which makes tyro output the warning: annotated with type `<class 'str'>`, but the default value `None`
|
|
233
|
+
# We either make None an option by `arg_type |= None`
|
|
234
|
+
# or else we default the value.
|
|
235
|
+
if arg_type is not None:
|
|
236
|
+
arg_type |= None
|
|
237
|
+
opt["default"] = action.default if action.default != SUPPRESS else None
|
|
238
|
+
|
|
239
|
+
# build a dataclass field, either optional, or positional
|
|
240
|
+
opt["metadata"] = {"help": action.help}
|
|
241
|
+
if action.option_strings:
|
|
242
|
+
# normal_fields.append((action.dest, arg_type, field(**opt, **met)))
|
|
243
|
+
# Annotated[arg_type, arg(metavar=metavar)]
|
|
244
|
+
normal_fields.append((af.name, arg_type, field(**opt)))
|
|
245
|
+
|
|
246
|
+
# Generate back-compatible property if dest != field_name
|
|
247
|
+
if af.name != action.dest and not af.has_property:
|
|
248
|
+
af.add(lambda self, field_name=af.name: getattr(self, field_name))
|
|
249
|
+
else:
|
|
250
|
+
pos_fields.append((action.dest, Positional[arg_type], field(**opt)))
|
|
251
|
+
|
|
252
|
+
# Subparser can have the same field name as the parser. We use the latter.
|
|
253
|
+
# Ex:
|
|
254
|
+
# parser.add_argument('--level', type=int, default=1)
|
|
255
|
+
# subparsers = parser.add_subparsers(dest='command')
|
|
256
|
+
# run_parser = subparsers.add_parser('run')
|
|
257
|
+
# run_parser.add_argument('--level', type=int, default=5)
|
|
258
|
+
uniq_fields = []
|
|
259
|
+
seen = set()
|
|
260
|
+
# for f in reversed(subparser_fields + pos_fields + normal_fields):
|
|
261
|
+
for f in reversed(pos_fields + normal_fields):
|
|
262
|
+
if f[0] not in seen:
|
|
263
|
+
seen.add(f[0])
|
|
264
|
+
uniq_fields.append(f)
|
|
265
|
+
|
|
266
|
+
# if subparser_fields:
|
|
267
|
+
# from functools import reduce
|
|
268
|
+
# union_type = reduce(lambda a, b: a | b, [aa[1] for aa in subparser_fields])
|
|
269
|
+
# result = OmitSubcommandPrefixes[Positional[union_type]]
|
|
270
|
+
# uniq_fields.append(("_subparsers", result ))
|
|
271
|
+
|
|
272
|
+
dc = make_dataclass(
|
|
273
|
+
name,
|
|
274
|
+
reversed(uniq_fields),
|
|
275
|
+
namespace={k: prop.generate_property() for k, prop in properties.items()},
|
|
276
|
+
)
|
|
277
|
+
if helptext or description:
|
|
278
|
+
trimmed = (helptext or "").strip()
|
|
279
|
+
needs_colon = trimmed and description and trimmed[-1] not in (".", ":", "!", "?", "…")
|
|
280
|
+
|
|
281
|
+
separator = ": " if needs_colon else ("\n" if trimmed else "")
|
|
282
|
+
dc.__doc__ = trimmed + separator + (description or "")
|
|
283
|
+
|
|
284
|
+
return dc
|
|
@@ -25,7 +25,7 @@ common_iterables = list, tuple, set
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]] = None) -> Iterable[T]:
|
|
28
|
-
"""
|
|
28
|
+
"""Recursively traverse whole dict"""
|
|
29
29
|
for k, v in d.items():
|
|
30
30
|
if isinstance(v, dict):
|
|
31
31
|
if include_keys:
|
|
@@ -37,7 +37,7 @@ def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]
|
|
|
37
37
|
|
|
38
38
|
# NOTE: Not used.
|
|
39
39
|
def flatten_keys(d: dict[KT, T | dict]) -> Iterable[tuple[KT, T]]:
|
|
40
|
-
"""
|
|
40
|
+
"""Recursively traverse whole dict"""
|
|
41
41
|
for k, v in d.items():
|
|
42
42
|
if isinstance(v, dict):
|
|
43
43
|
yield from flatten_keys(v)
|
|
@@ -61,17 +61,19 @@ def get_terminal_size():
|
|
|
61
61
|
# stty: 'standard input': Inappropriate ioctl for device
|
|
62
62
|
# I do not know how to suppress this warning.
|
|
63
63
|
# NOTE why not using os.get_terminal_size()
|
|
64
|
-
height, width = (int(s) for s in os.popen(
|
|
64
|
+
height, width = (int(s) for s in os.popen("stty size", "r").read().split())
|
|
65
65
|
return height, width
|
|
66
66
|
except (OSError, ValueError):
|
|
67
67
|
return 0, 0
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def get_descriptions(parser: ArgumentParser) -> dict:
|
|
71
|
-
"""
|
|
71
|
+
"""Load descriptions from the parser. Strip argparse info about the default value as it will be editable in the form."""
|
|
72
72
|
# clean-up tyro stuff that may have a meaning in the CLI, but not in the UI
|
|
73
|
-
return {
|
|
74
|
-
|
|
73
|
+
return {
|
|
74
|
+
action.dest.replace("-", "_"): re.sub(r"\((default|fixed to|required).*\)", "", action.help or "")
|
|
75
|
+
for action in parser._actions
|
|
76
|
+
}
|
|
75
77
|
|
|
76
78
|
|
|
77
79
|
def get_description(obj, param: str) -> str:
|
|
@@ -90,7 +92,7 @@ def yield_annotations(dataclass):
|
|
|
90
92
|
|
|
91
93
|
|
|
92
94
|
def matches_annotation(value, annotation) -> bool:
|
|
93
|
-
"""
|
|
95
|
+
"""Check whether the value type corresponds to the annotation.
|
|
94
96
|
Because built-in isinstance is not enough, it cannot determine parametrized generics.
|
|
95
97
|
"""
|
|
96
98
|
# union, including Optional and UnionType
|
|
@@ -155,7 +157,7 @@ def subclass_matches_annotation(cls, annotation) -> bool:
|
|
|
155
157
|
|
|
156
158
|
|
|
157
159
|
def serialize_structure(obj):
|
|
158
|
-
"""
|
|
160
|
+
"""Ex: [Path("/tmp"), Path("/usr"), 1] -> ["/tmp", "/usr", 1]."""
|
|
159
161
|
if isinstance(obj, (str, int, float)):
|
|
160
162
|
return obj
|
|
161
163
|
elif isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)):
|
|
@@ -165,7 +167,7 @@ def serialize_structure(obj):
|
|
|
165
167
|
|
|
166
168
|
|
|
167
169
|
def dataclass_asdict_no_defaults(obj) -> dict:
|
|
168
|
-
"""
|
|
170
|
+
"""Ignore the dataclass default values."""
|
|
169
171
|
if not hasattr(obj, "__dataclass_fields__"):
|
|
170
172
|
return obj
|
|
171
173
|
|
|
@@ -182,7 +184,7 @@ def dataclass_asdict_no_defaults(obj) -> dict:
|
|
|
182
184
|
|
|
183
185
|
|
|
184
186
|
def merge_dicts(d1: dict, d2: dict):
|
|
185
|
-
"""
|
|
187
|
+
"""Recursively merge second dict to the first."""
|
|
186
188
|
for key, value in d2.items():
|
|
187
189
|
if isinstance(value, dict) and isinstance(d1.get(key), dict):
|
|
188
190
|
merge_dicts(d1[key], value)
|
|
@@ -192,14 +194,14 @@ def merge_dicts(d1: dict, d2: dict):
|
|
|
192
194
|
|
|
193
195
|
|
|
194
196
|
def naturalsize(value: float | str, *args) -> str:
|
|
195
|
-
"""
|
|
197
|
+
"""For a bare interface, humanize might not be installed."""
|
|
196
198
|
if naturalsize_:
|
|
197
199
|
return naturalsize_(value, *args)
|
|
198
200
|
return str(value)
|
|
199
201
|
|
|
200
202
|
|
|
201
203
|
def validate_annotated_type(meta, value) -> bool:
|
|
202
|
-
"""
|
|
204
|
+
"""Raises: ValueError, NotImplementedError"""
|
|
203
205
|
if isinstance(meta, Gt):
|
|
204
206
|
if not value > meta.gt:
|
|
205
207
|
raise ValueError(f"Value {value} must be > {meta.gt}")
|
|
@@ -224,3 +226,16 @@ def validate_annotated_type(meta, value) -> bool:
|
|
|
224
226
|
else:
|
|
225
227
|
raise NotImplementedError(f"Unknown predicated {meta}")
|
|
226
228
|
return True
|
|
229
|
+
|
|
230
|
+
def allows_none(annotation) -> bool:
|
|
231
|
+
"""True, if annotation allows None: `int | None`, `Optional[int]`, `Union[int,None]`."""
|
|
232
|
+
if annotation is None:
|
|
233
|
+
return True
|
|
234
|
+
origin = get_origin(annotation)
|
|
235
|
+
args = get_args(annotation)
|
|
236
|
+
|
|
237
|
+
# if NoneType in get_args(self.annotation):
|
|
238
|
+
|
|
239
|
+
if origin is Union or origin is UnionType:
|
|
240
|
+
return any(arg is type(None) for arg in args)
|
|
241
|
+
return False
|