mininterface 1.0.3__tar.gz → 1.1.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.0.3 → mininterface-1.1.0}/PKG-INFO +7 -7
- {mininterface-1.0.3 → mininterface-1.1.0}/README.md +6 -6
- mininterface-1.1.0/mininterface/__init__.py +7 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/__main__.py +21 -9
- mininterface-1.1.0/mininterface/_lib/argparse_support.py +266 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/auxiliary.py +165 -18
- mininterface-1.1.0/mininterface/_lib/cli_flags.py +107 -0
- mininterface-1.1.0/mininterface/_lib/cli_parser.py +408 -0
- mininterface-1.0.3/mininterface/cli.py → mininterface-1.1.0/mininterface/_lib/cli_utils.py +10 -53
- mininterface-1.1.0/mininterface/_lib/config_file.py +101 -0
- mininterface-1.1.0/mininterface/_lib/dataclass_creation.py +282 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/form_dict.py +43 -16
- mininterface-1.1.0/mininterface/_lib/future_compatibility.py +6 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/redirectable.py +3 -2
- mininterface-1.0.3/mininterface/__init__.py → mininterface-1.1.0/mininterface/_lib/run.py +161 -86
- mininterface-1.1.0/mininterface/_lib/shortcuts.py +59 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/showcase.py +4 -2
- mininterface-1.1.0/mininterface/_lib/start.py +135 -0
- mininterface-1.1.0/mininterface/_lib/tyro_patches.py +400 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_mininterface/__init__.py +107 -91
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_mininterface/adaptor.py +12 -14
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_mininterface/mixin.py +12 -12
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_text_interface/__init__.py +21 -16
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_text_interface/adaptor.py +19 -10
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/__init__.py +3 -1
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/adaptor.py +26 -14
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/button_contents.py +3 -3
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/facet.py +2 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/file_picker_input.py +2 -5
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/form_contents.py +11 -7
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/widgets.py +3 -2
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/__init__.py +8 -2
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/adaptor.py +33 -28
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/date_entry.py +41 -36
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/external_fix.py +2 -6
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/facet.py +1 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/redirect_text_tkinter.py +4 -4
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/secret_entry.py +12 -13
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/select_input.py +47 -32
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/utils.py +30 -21
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_web_interface/__init__.py +16 -10
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_web_interface/app.py +1 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_web_interface/child_adaptor.py +1 -2
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_web_interface/parent_adaptor.py +2 -7
- mininterface-1.1.0/mininterface/cli.py +46 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/exceptions.py +10 -8
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/experimental.py +6 -4
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/facet/__init__.py +8 -7
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/interfaces.py +13 -10
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/settings.py +27 -33
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/alias.py +2 -2
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/callback_tag.py +2 -1
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/datetime_tag.py +53 -8
- mininterface-1.1.0/mininterface/tag/flag.py +284 -0
- mininterface-1.1.0/mininterface/tag/internal.py +10 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/path_tag.py +12 -4
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/select_tag.py +98 -26
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/tag.py +142 -90
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/tag_factory.py +54 -10
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/type_stubs.py +4 -2
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/validators.py +39 -20
- {mininterface-1.0.3 → mininterface-1.1.0}/pyproject.toml +4 -1
- mininterface-1.0.3/mininterface/_lib/cli_parser.py +0 -657
- mininterface-1.0.3/mininterface/_lib/start.py +0 -132
- mininterface-1.0.3/mininterface/tag/flag.py +0 -140
- mininterface-1.0.3/mininterface/tag/internal.py +0 -10
- {mininterface-1.0.3 → mininterface-1.1.0}/LICENSE +0 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/style.tcss +0 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/textual_app.py +0 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.0.3 → mininterface-1.1.0}/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.1.0
|
|
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
|
|
@@ -37,7 +37,7 @@ Project-URL: Homepage, https://github.com/CZ-NIC/mininterface
|
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
38
38
|
|
|
39
39
|
# Mininterface – access to GUI, TUI, web, CLI and config files
|
|
40
|
-
[](https://github.com/CZ-NIC/mininterface/actions)
|
|
40
|
+
[](https://github.com/CZ-NIC/mininterface/actions)
|
|
41
41
|
[](https://pepy.tech/project/mininterface)
|
|
42
42
|
|
|
43
43
|
Write the program core, do not bother with the input/output.
|
|
@@ -144,7 +144,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
144
144
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
145
145
|
|
|
146
146
|
```bash
|
|
147
|
-
pip install mininterface[all] # GPLv3 and compatible
|
|
147
|
+
pip install "mininterface[all]<2" # GPLv3 and compatible
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
## Bundles
|
|
@@ -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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Mininterface – access to GUI, TUI, web, CLI and config files
|
|
2
|
-
[](https://github.com/CZ-NIC/mininterface/actions)
|
|
2
|
+
[](https://github.com/CZ-NIC/mininterface/actions)
|
|
3
3
|
[](https://pepy.tech/project/mininterface)
|
|
4
4
|
|
|
5
5
|
Write the program core, do not bother with the input/output.
|
|
@@ -106,7 +106,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
106
106
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
pip install mininterface[all] # GPLv3 and compatible
|
|
109
|
+
pip install "mininterface[all]<2" # GPLv3 and compatible
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
## Bundles
|
|
@@ -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.
|
|
@@ -9,9 +9,10 @@ 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
|
-
from . import run
|
|
15
|
+
from ._lib.run import run
|
|
15
16
|
from .cli import Command
|
|
16
17
|
from .tag.flag import File
|
|
17
18
|
from .tag.path_tag import PathTag
|
|
@@ -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,266 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
from argparse import (SUPPRESS, Action, ArgumentParser, _AppendAction,
|
|
4
|
+
_AppendConstAction, _CountAction, _HelpAction,
|
|
5
|
+
_StoreConstAction, _StoreFalseAction, _StoreTrueAction,
|
|
6
|
+
_SubParsersAction, _VersionAction)
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from dataclasses import MISSING, Field, dataclass, field, make_dataclass
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
from typing import Annotated, Callable, Optional
|
|
11
|
+
from warnings import warn
|
|
12
|
+
|
|
13
|
+
from ..tag.alias import Options
|
|
14
|
+
from .form_dict import DataClass
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from tyro.conf import DisallowNone, OmitSubcommandPrefixes, Positional
|
|
18
|
+
except ImportError:
|
|
19
|
+
from ..exceptions import DependencyRequired
|
|
20
|
+
|
|
21
|
+
raise DependencyRequired("basic")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Property:
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self._usages = []
|
|
27
|
+
|
|
28
|
+
def add(self, callback: Callable):
|
|
29
|
+
self._usages.append(callback)
|
|
30
|
+
|
|
31
|
+
def generate_property(self):
|
|
32
|
+
def _(this):
|
|
33
|
+
for clb in self._usages:
|
|
34
|
+
v = clb(this)
|
|
35
|
+
if v is not None:
|
|
36
|
+
return v
|
|
37
|
+
|
|
38
|
+
return property(_)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ArgparseField:
|
|
43
|
+
|
|
44
|
+
action: Action
|
|
45
|
+
properties: dict[str, Property]
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def name(self):
|
|
49
|
+
if n := self.action.option_strings:
|
|
50
|
+
# --get-one → get_one
|
|
51
|
+
return re.sub(r"^--?", "", self.action.option_strings[0]).replace("-", "_")
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f"Cannot load argparse, due to field {self.action}")
|
|
54
|
+
|
|
55
|
+
def add(self, callback: Callable):
|
|
56
|
+
if self.action.dest == self.name:
|
|
57
|
+
raise NotImplementedError(
|
|
58
|
+
f"Cannot load argparse, due to field {self.action}. It must be visible from CLI and cannot"
|
|
59
|
+
"be read directly from the program. Solution: Do not use argparse or add a .dest parameter."
|
|
60
|
+
)
|
|
61
|
+
self.properties[self.action.dest].add(callback)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def has_property(self):
|
|
65
|
+
return self.action.dest in self.properties
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> tuple[DataClass | list[DataClass], Optional[str]]:
|
|
69
|
+
"""
|
|
70
|
+
Note: Ex. parser.add_argument("--time", type=time) -> does work at all in argparse, here it works.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
DataClass | list[DataClass]
|
|
74
|
+
Optional[str]: add_version flag
|
|
75
|
+
"""
|
|
76
|
+
subparsers: list[_SubParsersAction] = []
|
|
77
|
+
add_version = None
|
|
78
|
+
|
|
79
|
+
normal_actions: list[Action] = []
|
|
80
|
+
has_positionals = False
|
|
81
|
+
for action in parser._actions:
|
|
82
|
+
match action:
|
|
83
|
+
case _HelpAction():
|
|
84
|
+
continue
|
|
85
|
+
case _SubParsersAction():
|
|
86
|
+
if has_positionals:
|
|
87
|
+
warn(
|
|
88
|
+
"This CLI parser have a subcommand placed after positional arguments. The order of arguments changes, see --help."
|
|
89
|
+
)
|
|
90
|
+
subparsers.append(action)
|
|
91
|
+
case _VersionAction():
|
|
92
|
+
# We do not want the version to be part of the dataclass (and appear in `m.form()`).
|
|
93
|
+
add_version = action.version
|
|
94
|
+
case _:
|
|
95
|
+
if not action.option_strings:
|
|
96
|
+
has_positionals = True
|
|
97
|
+
normal_actions.append(action)
|
|
98
|
+
|
|
99
|
+
if subparsers:
|
|
100
|
+
return [
|
|
101
|
+
_make_dataclass_from_actions(
|
|
102
|
+
normal_actions + subactions._actions,
|
|
103
|
+
subname,
|
|
104
|
+
help_,
|
|
105
|
+
subactions.description,
|
|
106
|
+
)
|
|
107
|
+
for subparser in subparsers
|
|
108
|
+
for subname, subactions, help_ in _loop_SubParsersAction(subparser)
|
|
109
|
+
], add_version
|
|
110
|
+
else:
|
|
111
|
+
return _make_dataclass_from_actions(normal_actions, name, None, parser.description), add_version
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _loop_SubParsersAction(subparser: _SubParsersAction):
|
|
115
|
+
return [
|
|
116
|
+
(subname, subactions, ch_act.help)
|
|
117
|
+
for (subname, subactions), ch_act in zip(subparser.choices.items(), subparser._choices_actions)
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _make_dataclass_from_actions(
|
|
122
|
+
actions: list[Action], name, helptext: str | None, description: str | None
|
|
123
|
+
) -> DataClass:
|
|
124
|
+
const_actions = defaultdict(list[ArgparseField])
|
|
125
|
+
normal_fields: list[tuple[str, type, Field]] = []
|
|
126
|
+
pos_fields: list[tuple[str, type, Field]] = []
|
|
127
|
+
properties = defaultdict(Property)
|
|
128
|
+
""" Sometimes, the action.dest differs from the field name.
|
|
129
|
+
Field name is exposed to the CLI, action.dest is used in the program.
|
|
130
|
+
"""
|
|
131
|
+
subparser_fields: list[tuple[str, type]] = []
|
|
132
|
+
|
|
133
|
+
for action in actions:
|
|
134
|
+
af = ArgparseField(action, properties)
|
|
135
|
+
opt = {}
|
|
136
|
+
|
|
137
|
+
match action:
|
|
138
|
+
case _HelpAction():
|
|
139
|
+
continue
|
|
140
|
+
case _SubParsersAction():
|
|
141
|
+
# Note that there is only one _SubParsersAction in argparse
|
|
142
|
+
# but to be sure, we allow multiple of them
|
|
143
|
+
# This probably makes a different CLI output than the original argparse but should work.
|
|
144
|
+
for subname, subparser, help_ in _loop_SubParsersAction(action):
|
|
145
|
+
sub_dc = _make_dataclass_from_actions(
|
|
146
|
+
subparser._actions,
|
|
147
|
+
subname.capitalize(),
|
|
148
|
+
help_,
|
|
149
|
+
subparser.description,
|
|
150
|
+
)
|
|
151
|
+
subparser_fields.append((subname, sub_dc)) # required, no default
|
|
152
|
+
|
|
153
|
+
from functools import reduce
|
|
154
|
+
|
|
155
|
+
union_type = reduce(lambda a, b: a | b, [aa[1] for aa in subparser_fields])
|
|
156
|
+
|
|
157
|
+
result = OmitSubcommandPrefixes[Positional[union_type]]
|
|
158
|
+
pos_fields.append(("_subparsers", result))
|
|
159
|
+
subparser_fields.clear()
|
|
160
|
+
continue
|
|
161
|
+
case _AppendAction():
|
|
162
|
+
arg_type = list[action.type or str]
|
|
163
|
+
opt["default_factory"] = list
|
|
164
|
+
case _AppendConstAction():
|
|
165
|
+
# `--one --two` -> env.section = [one, two]
|
|
166
|
+
arg_type = bool
|
|
167
|
+
opt["default"] = False
|
|
168
|
+
const_actions[af.action.dest].append(af)
|
|
169
|
+
af.add(
|
|
170
|
+
lambda self, af=af: (
|
|
171
|
+
[_af.action.const for _af in const_actions[af.action.dest] if getattr(self, _af.name)]
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
case _StoreTrueAction():
|
|
175
|
+
arg_type = bool
|
|
176
|
+
case _StoreFalseAction():
|
|
177
|
+
arg_type = bool
|
|
178
|
+
opt["default"] = False
|
|
179
|
+
af.add(lambda self, field_name=af.name: not getattr(self, field_name))
|
|
180
|
+
case _StoreConstAction():
|
|
181
|
+
arg_type = bool
|
|
182
|
+
opt["default"] = False
|
|
183
|
+
af.add(
|
|
184
|
+
lambda self, field_name=af.name, const=action.const: (const if getattr(self, field_name) else None)
|
|
185
|
+
)
|
|
186
|
+
case _CountAction():
|
|
187
|
+
arg_type = int
|
|
188
|
+
case _:
|
|
189
|
+
if action.type:
|
|
190
|
+
arg_type = action.type
|
|
191
|
+
elif action.default:
|
|
192
|
+
arg_type = type(action.default)
|
|
193
|
+
else:
|
|
194
|
+
arg_type = str
|
|
195
|
+
|
|
196
|
+
if "default" not in opt and "default_factory" not in opt:
|
|
197
|
+
if action.choices:
|
|
198
|
+
# With the drop of Python 3.10, use mere:
|
|
199
|
+
# arg_type = Literal[*action.choices]
|
|
200
|
+
if sys.version_info >= (3,11):
|
|
201
|
+
from .future_compatibility import literal
|
|
202
|
+
arg_type = literal(action.choices)
|
|
203
|
+
else:
|
|
204
|
+
# we do not prefer this option as tyro does not understand it
|
|
205
|
+
# and won't display options in the help
|
|
206
|
+
arg_type = Annotated[arg_type, Options(*action.choices)]
|
|
207
|
+
|
|
208
|
+
if not action.option_strings and action.default is None and action.nargs != "?":
|
|
209
|
+
opt["default"] = MISSING
|
|
210
|
+
else:
|
|
211
|
+
if action.default is None:
|
|
212
|
+
# parser.add_argument("--path", type=Path) -> becomes None, not Path('.').
|
|
213
|
+
# By default, argparse put None if not used in the CLI.
|
|
214
|
+
# Which makes tyro output the warning: annotated with type `<class 'str'>`, but the default value `None`
|
|
215
|
+
# We either make None an option by `arg_type |= None`
|
|
216
|
+
# or else we default the value.
|
|
217
|
+
if arg_type is not None:
|
|
218
|
+
arg_type |= None
|
|
219
|
+
opt["default"] = action.default if action.default != SUPPRESS else None
|
|
220
|
+
|
|
221
|
+
# build a dataclass field, either optional, or positional
|
|
222
|
+
opt["metadata"] = {"help": action.help}
|
|
223
|
+
if action.option_strings:
|
|
224
|
+
# normal_fields.append((action.dest, arg_type, field(**opt, **met)))
|
|
225
|
+
# Annotated[arg_type, arg(metavar=metavar)]
|
|
226
|
+
normal_fields.append((af.name, arg_type, field(**opt)))
|
|
227
|
+
|
|
228
|
+
# Generate back-compatible property if dest != field_name
|
|
229
|
+
if af.name != action.dest and not af.has_property:
|
|
230
|
+
af.add(lambda self, field_name=af.name: getattr(self, field_name))
|
|
231
|
+
else:
|
|
232
|
+
pos_fields.append((action.dest, Positional[arg_type], field(**opt)))
|
|
233
|
+
|
|
234
|
+
# Subparser can have the same field name as the parser. We use the latter.
|
|
235
|
+
# Ex:
|
|
236
|
+
# parser.add_argument('--level', type=int, default=1)
|
|
237
|
+
# subparsers = parser.add_subparsers(dest='command')
|
|
238
|
+
# run_parser = subparsers.add_parser('run')
|
|
239
|
+
# run_parser.add_argument('--level', type=int, default=5)
|
|
240
|
+
uniq_fields = []
|
|
241
|
+
seen = set()
|
|
242
|
+
# for f in reversed(subparser_fields + pos_fields + normal_fields):
|
|
243
|
+
for f in reversed(pos_fields + normal_fields):
|
|
244
|
+
if f[0] not in seen:
|
|
245
|
+
seen.add(f[0])
|
|
246
|
+
uniq_fields.append(f)
|
|
247
|
+
|
|
248
|
+
# if subparser_fields:
|
|
249
|
+
# from functools import reduce
|
|
250
|
+
# union_type = reduce(lambda a, b: a | b, [aa[1] for aa in subparser_fields])
|
|
251
|
+
# result = OmitSubcommandPrefixes[Positional[union_type]]
|
|
252
|
+
# uniq_fields.append(("_subparsers", result ))
|
|
253
|
+
|
|
254
|
+
dc = make_dataclass(
|
|
255
|
+
name,
|
|
256
|
+
reversed(uniq_fields),
|
|
257
|
+
namespace={k: prop.generate_property() for k, prop in properties.items()},
|
|
258
|
+
)
|
|
259
|
+
if helptext or description:
|
|
260
|
+
trimmed = (helptext or "").strip()
|
|
261
|
+
needs_colon = trimmed and description and trimmed[-1] not in (".", ":", "!", "?", "…")
|
|
262
|
+
|
|
263
|
+
separator = ": " if needs_colon else ("\n" if trimmed else "")
|
|
264
|
+
dc.__doc__ = trimmed + separator + (description or "")
|
|
265
|
+
|
|
266
|
+
return DisallowNone[dc]
|