mininterface 0.4.2__tar.gz → 0.4.4rc1__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-0.4.2 → mininterface-0.4.4rc1}/PKG-INFO +58 -42
- {mininterface-0.4.2 → mininterface-0.4.4rc1}/README.md +56 -40
- mininterface-0.4.4rc1/mininterface/FormDict.py +120 -0
- mininterface-0.4.4rc1/mininterface/FormField.py +133 -0
- {mininterface-0.4.2 → mininterface-0.4.4rc1}/mininterface/GuiInterface.py +18 -19
- {mininterface-0.4.2 → mininterface-0.4.4rc1}/mininterface/Mininterface.py +13 -11
- mininterface-0.4.2/mininterface/TuiInterface.py → mininterface-0.4.4rc1/mininterface/TextInterface.py +7 -5
- {mininterface-0.4.2 → mininterface-0.4.4rc1}/mininterface/TextualInterface.py +35 -45
- {mininterface-0.4.2 → mininterface-0.4.4rc1}/mininterface/__init__.py +28 -11
- {mininterface-0.4.2 → mininterface-0.4.4rc1}/mininterface/__main__.py +0 -5
- mininterface-0.4.4rc1/mininterface/auxiliary.py +76 -0
- {mininterface-0.4.2 → mininterface-0.4.4rc1}/pyproject.toml +2 -2
- mininterface-0.4.2/mininterface/auxiliary.py +0 -327
- {mininterface-0.4.2 → mininterface-0.4.4rc1}/mininterface/common.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4rc1
|
|
4
4
|
Summary: A minimal access to GUI, TUI, CLI and config
|
|
5
5
|
Home-page: https://github.com/CZ-NIC/mininterface
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -17,7 +17,7 @@ Requires-Dist: pyyaml
|
|
|
17
17
|
Requires-Dist: requests
|
|
18
18
|
Requires-Dist: textual
|
|
19
19
|
Requires-Dist: tkinter-tooltip
|
|
20
|
-
Requires-Dist: tkinter_form
|
|
20
|
+
Requires-Dist: tkinter_form (==0.1.5.2)
|
|
21
21
|
Requires-Dist: tyro
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
@@ -27,9 +27,10 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
|
|
28
28
|
Write the program core, do not bother with the input/output.
|
|
29
29
|
|
|
30
|
-

|
|
31
|
+

|
|
31
32
|
|
|
32
|
-
Check out the code
|
|
33
|
+
Check out the code, which is surprisingly short, that displays such a window or its textual fallback.
|
|
33
34
|
|
|
34
35
|
```python
|
|
35
36
|
from dataclasses import dataclass
|
|
@@ -38,34 +39,50 @@ from mininterface import run
|
|
|
38
39
|
@dataclass
|
|
39
40
|
class Config:
|
|
40
41
|
"""Set of options."""
|
|
41
|
-
test: bool = False
|
|
42
|
-
|
|
43
|
-
important_number: int = 4
|
|
44
|
-
"""This number is very important"""
|
|
42
|
+
test: bool = False # My testing flag
|
|
43
|
+
important_number: int = 4 # This number is very important
|
|
45
44
|
|
|
46
45
|
if __name__ == "__main__":
|
|
47
|
-
args
|
|
48
|
-
print(args.important_number)
|
|
46
|
+
args = run(Config, prog="My application").get_args()
|
|
47
|
+
print(args.important_number) # suggested by the IDE with the hint text "This number is very important"
|
|
49
48
|
```
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
## You got CLI
|
|
51
|
+
It was all the code you need. No lengthy blocks of code imposed by an external dependency. Besides the GUI/TUI, you receive powerful YAML-configurable CLI parsing.
|
|
52
52
|
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
```bash
|
|
54
|
+
$ ./hello.py
|
|
55
|
+
usage: My application [-h] [--test | --no-test] [--important-number INT]
|
|
56
|
+
|
|
57
|
+
Set of options.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
╭─ options ──────────────────────────────────────────────────────────╮
|
|
60
|
+
│ -h, --help show this help message and exit │
|
|
61
|
+
│ --test, --no-test My testing flag (default: False) │
|
|
62
|
+
│ --important-number INT This number is very important (default: 4) │
|
|
63
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
64
|
+
```
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
## You got config file management
|
|
67
|
+
Loading config file is a piece of cake. Alongside `program.py`, put `program.yaml` and put there some of the arguments. They are seamlessly taken as defaults.
|
|
62
68
|
|
|
63
69
|
```yaml
|
|
64
70
|
important_number: 555
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
## You got dialogues
|
|
74
|
+
Check out several useful methods to handle user dialogues. Here we bound the interface to a `with` statement that redirects stdout directly to the window.
|
|
68
75
|
|
|
76
|
+
```python
|
|
77
|
+
with run(Config) as m:
|
|
78
|
+
print(f"Your important number is {m}")
|
|
79
|
+
boolean = m.is_yes("Is that alright?")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+

|
|
83
|
+

|
|
84
|
+
|
|
85
|
+
# Contents
|
|
69
86
|
- [Mininterface – GUI, TUI, CLI and config](#mininterface-gui-tui-cli-and-config)
|
|
70
87
|
- [Background](#background)
|
|
71
88
|
- [Installation](#installation)
|
|
@@ -73,21 +90,18 @@ TODO img
|
|
|
73
90
|
* [`mininterface`](#mininterface)
|
|
74
91
|
+ [`run(config=None, interface=GuiInterface, **kwargs)`](#runconfignone-interfaceguiinterface-kwargs)
|
|
75
92
|
* [Interfaces](#interfaces)
|
|
76
|
-
+ [`Mininterface(title: str = '')`](#
|
|
77
|
-
+ [`alert(
|
|
78
|
-
+ [`ask(
|
|
79
|
-
+ [`ask_args(
|
|
80
|
-
+ [`
|
|
81
|
-
+ [`
|
|
82
|
-
+ [`get_args(
|
|
83
|
-
+ [`is_no(
|
|
84
|
-
+ [`is_yes(
|
|
85
|
-
+ [`parse_args(
|
|
93
|
+
+ [`Mininterface(title: str = '')`](#mininterfacetitle-str--)
|
|
94
|
+
+ [`alert(text: str)`](#alerttext-str)
|
|
95
|
+
+ [`ask(text: str) -> str`](#asktext-str---str)
|
|
96
|
+
+ [`ask_args() -> ConfigInstance`](#ask_args--configinstance)
|
|
97
|
+
+ [`ask_number(text: str) -> int`](#ask_numbertext-str---int)
|
|
98
|
+
+ [`form(args: FormDict, title="") -> int`](#formargs-formdict-title---dict)
|
|
99
|
+
+ [`get_args(ask_on_empty_cli=True) -> ~ConfigInstance`](#get_argsask_on_empty_clitrue---configinstance)
|
|
100
|
+
+ [`is_no(text: str) -> bool`](#is_notext-str---bool)
|
|
101
|
+
+ [`is_yes(text: str) -> bool`](#is_yestext-str---bool)
|
|
102
|
+
+ [`parse_args(config: Type[ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ConfigInstance`](#parse_argsconfig-type-configinstance-config_file-pathlibpath--none--none-kwargs---configinstance)
|
|
86
103
|
* [Standalone](#standalone)
|
|
87
104
|
|
|
88
|
-
<small><i><a href='http://ecotrust-canada.github.io/markdown-toc/'>Table of contents generated with markdown-toc</a></i></small>
|
|
89
|
-
|
|
90
|
-
|
|
91
105
|
# Background
|
|
92
106
|
|
|
93
107
|
Wrapper between the [tyro](https://github.com/brentyi/tyro) `argparse` replacement and [tkinter_form](https://github.com/JohanEstebanCuervo/tkinter_form/) that converts dicts into a GUI.
|
|
@@ -147,7 +161,7 @@ $./program.py --further.host example.net
|
|
|
147
161
|
Wrap your configuration dataclass into `run` to access the interface. Normally, an interface is chosen automatically. We prefer the graphical one, regressed to a text interface on a machine without display.
|
|
148
162
|
Besides, if given a configuration dataclass, the function enriches it with the CLI commands and possibly with the default from a config file if such exists. It searches the config file in the current working directory, with the program name ending on *.yaml*, ex: `program.py` will fetch `./program.yaml`.
|
|
149
163
|
|
|
150
|
-
* `config:
|
|
164
|
+
* `config:Type[ConfigInstance]`: Dataclass with the configuration.
|
|
151
165
|
* `interface`: Which interface to prefer. By default, we use the GUI, the fallback is the REPL.
|
|
152
166
|
* `**kwargs`: The same as for [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html).
|
|
153
167
|
* Returns: `interface` Interface used.
|
|
@@ -163,6 +177,8 @@ Several interfaces exist:
|
|
|
163
177
|
* `Mininterface` – The base interface. Does not require any user input and hence is suitable for headless testing.
|
|
164
178
|
* `GuiInterface` – A tkinter window.
|
|
165
179
|
* `TuiInterface` – An interactive terminal.
|
|
180
|
+
* `TextualInterface` – If [textual](https://github.com/Textualize/textual) installed, rich interface is used.
|
|
181
|
+
* `TextInterface` – Plain text only interface with no dependency as a fallback.
|
|
166
182
|
* `ReplInterface` – A debug terminal. Invokes a breakpoint after every dialog.
|
|
167
183
|
|
|
168
184
|
You can invoke one directly instead of using [mininterface.run](#run-config-none-interface-guiinterface-kwargs). Then, you can connect a configuration object to the CLI and config file with `parse_args` if needed.
|
|
@@ -174,27 +190,27 @@ with TuiInterface("My program") as m:
|
|
|
174
190
|
|
|
175
191
|
### `Mininterface(title: str = '')`
|
|
176
192
|
Initialize.
|
|
177
|
-
### `alert(
|
|
193
|
+
### `alert(text: str)`
|
|
178
194
|
Prompt the user to confirm the text.
|
|
179
|
-
### `ask(
|
|
195
|
+
### `ask(text: str) -> str`
|
|
180
196
|
Prompt the user to input a text.
|
|
181
|
-
### `ask_args(
|
|
197
|
+
### `ask_args() -> ConfigInstance`
|
|
182
198
|
Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_args.)
|
|
183
|
-
### `
|
|
199
|
+
### `form(args: FormDict, title="") -> dict`
|
|
184
200
|
Prompt the user to fill up whole form.
|
|
185
201
|
* `args`: Dict of `{labels: default value}`. The form widget infers from the default value type.
|
|
186
202
|
The dict can be nested, it can contain a subgroup.
|
|
187
203
|
The default value might be `mininterface.FormField` that allows you to add descriptions.
|
|
188
204
|
A checkbox example: `{"my label": FormField(True, "my description")}`
|
|
189
205
|
* `title`: Optional form title.
|
|
190
|
-
### `ask_number(
|
|
206
|
+
### `ask_number(text: str) -> int`
|
|
191
207
|
Prompt the user to input a number. Empty input = 0.
|
|
192
|
-
### `get_args(
|
|
208
|
+
### `get_args(ask_on_empty_cli=True) -> ConfigInstance`
|
|
193
209
|
Returns whole configuration (previously fetched from CLI and config file by parse_args).
|
|
194
210
|
If program was launched with no arguments (empty CLI), invokes self.ask_args() to edit the fields.
|
|
195
|
-
### `is_no(
|
|
211
|
+
### `is_no(text: str) -> bool`
|
|
196
212
|
Display confirm box, focusing no.
|
|
197
|
-
### `is_yes(
|
|
213
|
+
### `is_yes(text: str) -> bool`
|
|
198
214
|
Display confirm box, focusing yes.
|
|
199
215
|
|
|
200
216
|
```python
|
|
@@ -202,7 +218,7 @@ m = run(prog="My program")
|
|
|
202
218
|
print(m.ask_yes("Is it true?")) # True/False
|
|
203
219
|
```
|
|
204
220
|
|
|
205
|
-
### `parse_args(
|
|
221
|
+
### `parse_args(config: Type[ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~ConfigInstance`
|
|
206
222
|
Parse CLI arguments, possibly merged from a config file.
|
|
207
223
|
* `config`: Dataclass with the configuration.
|
|
208
224
|
* `config_file`: File to load YAML to be merged with the configuration. You do not have to re-define all the settings, you can choose a few.
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
Write the program core, do not bother with the input/output.
|
|
6
6
|
|
|
7
|
-

|
|
8
|
+

|
|
8
9
|
|
|
9
|
-
Check out the code
|
|
10
|
+
Check out the code, which is surprisingly short, that displays such a window or its textual fallback.
|
|
10
11
|
|
|
11
12
|
```python
|
|
12
13
|
from dataclasses import dataclass
|
|
@@ -15,34 +16,50 @@ from mininterface import run
|
|
|
15
16
|
@dataclass
|
|
16
17
|
class Config:
|
|
17
18
|
"""Set of options."""
|
|
18
|
-
test: bool = False
|
|
19
|
-
|
|
20
|
-
important_number: int = 4
|
|
21
|
-
"""This number is very important"""
|
|
19
|
+
test: bool = False # My testing flag
|
|
20
|
+
important_number: int = 4 # This number is very important
|
|
22
21
|
|
|
23
22
|
if __name__ == "__main__":
|
|
24
|
-
args
|
|
25
|
-
print(args.important_number)
|
|
23
|
+
args = run(Config, prog="My application").get_args()
|
|
24
|
+
print(args.important_number) # suggested by the IDE with the hint text "This number is very important"
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
## You got CLI
|
|
28
|
+
It was all the code you need. No lengthy blocks of code imposed by an external dependency. Besides the GUI/TUI, you receive powerful YAML-configurable CLI parsing.
|
|
29
29
|
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
```bash
|
|
31
|
+
$ ./hello.py
|
|
32
|
+
usage: My application [-h] [--test | --no-test] [--important-number INT]
|
|
33
|
+
|
|
34
|
+
Set of options.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
╭─ options ──────────────────────────────────────────────────────────╮
|
|
37
|
+
│ -h, --help show this help message and exit │
|
|
38
|
+
│ --test, --no-test My testing flag (default: False) │
|
|
39
|
+
│ --important-number INT This number is very important (default: 4) │
|
|
40
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
41
|
+
```
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
## You got config file management
|
|
44
|
+
Loading config file is a piece of cake. Alongside `program.py`, put `program.yaml` and put there some of the arguments. They are seamlessly taken as defaults.
|
|
39
45
|
|
|
40
46
|
```yaml
|
|
41
47
|
important_number: 555
|
|
42
48
|
```
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
## You got dialogues
|
|
51
|
+
Check out several useful methods to handle user dialogues. Here we bound the interface to a `with` statement that redirects stdout directly to the window.
|
|
45
52
|
|
|
53
|
+
```python
|
|
54
|
+
with run(Config) as m:
|
|
55
|
+
print(f"Your important number is {m}")
|
|
56
|
+
boolean = m.is_yes("Is that alright?")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
# Contents
|
|
46
63
|
- [Mininterface – GUI, TUI, CLI and config](#mininterface-gui-tui-cli-and-config)
|
|
47
64
|
- [Background](#background)
|
|
48
65
|
- [Installation](#installation)
|
|
@@ -50,21 +67,18 @@ TODO img
|
|
|
50
67
|
* [`mininterface`](#mininterface)
|
|
51
68
|
+ [`run(config=None, interface=GuiInterface, **kwargs)`](#runconfignone-interfaceguiinterface-kwargs)
|
|
52
69
|
* [Interfaces](#interfaces)
|
|
53
|
-
+ [`Mininterface(title: str = '')`](#
|
|
54
|
-
+ [`alert(
|
|
55
|
-
+ [`ask(
|
|
56
|
-
+ [`ask_args(
|
|
57
|
-
+ [`
|
|
58
|
-
+ [`
|
|
59
|
-
+ [`get_args(
|
|
60
|
-
+ [`is_no(
|
|
61
|
-
+ [`is_yes(
|
|
62
|
-
+ [`parse_args(
|
|
70
|
+
+ [`Mininterface(title: str = '')`](#mininterfacetitle-str--)
|
|
71
|
+
+ [`alert(text: str)`](#alerttext-str)
|
|
72
|
+
+ [`ask(text: str) -> str`](#asktext-str---str)
|
|
73
|
+
+ [`ask_args() -> ConfigInstance`](#ask_args--configinstance)
|
|
74
|
+
+ [`ask_number(text: str) -> int`](#ask_numbertext-str---int)
|
|
75
|
+
+ [`form(args: FormDict, title="") -> int`](#formargs-formdict-title---dict)
|
|
76
|
+
+ [`get_args(ask_on_empty_cli=True) -> ~ConfigInstance`](#get_argsask_on_empty_clitrue---configinstance)
|
|
77
|
+
+ [`is_no(text: str) -> bool`](#is_notext-str---bool)
|
|
78
|
+
+ [`is_yes(text: str) -> bool`](#is_yestext-str---bool)
|
|
79
|
+
+ [`parse_args(config: Type[ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ConfigInstance`](#parse_argsconfig-type-configinstance-config_file-pathlibpath--none--none-kwargs---configinstance)
|
|
63
80
|
* [Standalone](#standalone)
|
|
64
81
|
|
|
65
|
-
<small><i><a href='http://ecotrust-canada.github.io/markdown-toc/'>Table of contents generated with markdown-toc</a></i></small>
|
|
66
|
-
|
|
67
|
-
|
|
68
82
|
# Background
|
|
69
83
|
|
|
70
84
|
Wrapper between the [tyro](https://github.com/brentyi/tyro) `argparse` replacement and [tkinter_form](https://github.com/JohanEstebanCuervo/tkinter_form/) that converts dicts into a GUI.
|
|
@@ -124,7 +138,7 @@ $./program.py --further.host example.net
|
|
|
124
138
|
Wrap your configuration dataclass into `run` to access the interface. Normally, an interface is chosen automatically. We prefer the graphical one, regressed to a text interface on a machine without display.
|
|
125
139
|
Besides, if given a configuration dataclass, the function enriches it with the CLI commands and possibly with the default from a config file if such exists. It searches the config file in the current working directory, with the program name ending on *.yaml*, ex: `program.py` will fetch `./program.yaml`.
|
|
126
140
|
|
|
127
|
-
* `config:
|
|
141
|
+
* `config:Type[ConfigInstance]`: Dataclass with the configuration.
|
|
128
142
|
* `interface`: Which interface to prefer. By default, we use the GUI, the fallback is the REPL.
|
|
129
143
|
* `**kwargs`: The same as for [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html).
|
|
130
144
|
* Returns: `interface` Interface used.
|
|
@@ -140,6 +154,8 @@ Several interfaces exist:
|
|
|
140
154
|
* `Mininterface` – The base interface. Does not require any user input and hence is suitable for headless testing.
|
|
141
155
|
* `GuiInterface` – A tkinter window.
|
|
142
156
|
* `TuiInterface` – An interactive terminal.
|
|
157
|
+
* `TextualInterface` – If [textual](https://github.com/Textualize/textual) installed, rich interface is used.
|
|
158
|
+
* `TextInterface` – Plain text only interface with no dependency as a fallback.
|
|
143
159
|
* `ReplInterface` – A debug terminal. Invokes a breakpoint after every dialog.
|
|
144
160
|
|
|
145
161
|
You can invoke one directly instead of using [mininterface.run](#run-config-none-interface-guiinterface-kwargs). Then, you can connect a configuration object to the CLI and config file with `parse_args` if needed.
|
|
@@ -151,27 +167,27 @@ with TuiInterface("My program") as m:
|
|
|
151
167
|
|
|
152
168
|
### `Mininterface(title: str = '')`
|
|
153
169
|
Initialize.
|
|
154
|
-
### `alert(
|
|
170
|
+
### `alert(text: str)`
|
|
155
171
|
Prompt the user to confirm the text.
|
|
156
|
-
### `ask(
|
|
172
|
+
### `ask(text: str) -> str`
|
|
157
173
|
Prompt the user to input a text.
|
|
158
|
-
### `ask_args(
|
|
174
|
+
### `ask_args() -> ConfigInstance`
|
|
159
175
|
Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_args.)
|
|
160
|
-
### `
|
|
176
|
+
### `form(args: FormDict, title="") -> dict`
|
|
161
177
|
Prompt the user to fill up whole form.
|
|
162
178
|
* `args`: Dict of `{labels: default value}`. The form widget infers from the default value type.
|
|
163
179
|
The dict can be nested, it can contain a subgroup.
|
|
164
180
|
The default value might be `mininterface.FormField` that allows you to add descriptions.
|
|
165
181
|
A checkbox example: `{"my label": FormField(True, "my description")}`
|
|
166
182
|
* `title`: Optional form title.
|
|
167
|
-
### `ask_number(
|
|
183
|
+
### `ask_number(text: str) -> int`
|
|
168
184
|
Prompt the user to input a number. Empty input = 0.
|
|
169
|
-
### `get_args(
|
|
185
|
+
### `get_args(ask_on_empty_cli=True) -> ConfigInstance`
|
|
170
186
|
Returns whole configuration (previously fetched from CLI and config file by parse_args).
|
|
171
187
|
If program was launched with no arguments (empty CLI), invokes self.ask_args() to edit the fields.
|
|
172
|
-
### `is_no(
|
|
188
|
+
### `is_no(text: str) -> bool`
|
|
173
189
|
Display confirm box, focusing no.
|
|
174
|
-
### `is_yes(
|
|
190
|
+
### `is_yes(text: str) -> bool`
|
|
175
191
|
Display confirm box, focusing yes.
|
|
176
192
|
|
|
177
193
|
```python
|
|
@@ -179,7 +195,7 @@ m = run(prog="My program")
|
|
|
179
195
|
print(m.ask_yes("Is it true?")) # True/False
|
|
180
196
|
```
|
|
181
197
|
|
|
182
|
-
### `parse_args(
|
|
198
|
+
### `parse_args(config: Type[ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~ConfigInstance`
|
|
183
199
|
Parse CLI arguments, possibly merged from a config file.
|
|
184
200
|
* `config`: Dataclass with the configuration.
|
|
185
201
|
* `config_file`: File to load YAML to be merged with the configuration. You do not have to re-define all the settings, you can choose a few.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
""" FormDict tools.
|
|
2
|
+
FormDict is not a real class, just a normal dict. But we need to put somewhere functions related to it.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from argparse import Action, ArgumentParser
|
|
6
|
+
from typing import Callable, Optional, Type, TypeVar, Union, get_type_hints
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from tyro import cli
|
|
10
|
+
from tyro._argparse_formatter import TyroArgumentParser
|
|
11
|
+
|
|
12
|
+
from .FormField import FormField
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
ConfigInstance = TypeVar("ConfigInstance")
|
|
17
|
+
ConfigClass = Type[ConfigInstance]
|
|
18
|
+
FormDict = dict[str, Union[FormField, 'FormDict']]
|
|
19
|
+
""" Nested form that can have descriptions (through FormField) instead of plain values. """
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def formdict_repr(d: FormDict) -> dict:
|
|
23
|
+
""" For the testing purposes, returns a new dict when all FormFields are replaced with their values. """
|
|
24
|
+
out = {}
|
|
25
|
+
for k, v in d.items():
|
|
26
|
+
if isinstance(v, FormField):
|
|
27
|
+
v = v.val
|
|
28
|
+
out[k] = formdict_repr(v) if isinstance(v, dict) else v
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def dict_to_formdict(data: dict) -> FormDict:
|
|
33
|
+
fd = {}
|
|
34
|
+
for key, val in data.items():
|
|
35
|
+
if isinstance(val, dict): # nested config hierarchy
|
|
36
|
+
fd[key] = dict_to_formdict(val)
|
|
37
|
+
else: # scalar value
|
|
38
|
+
# NOTE name=param is not set (yet?) in `config_to_formdict`, neither `src`
|
|
39
|
+
fd[key] = FormField(val, "", name=key, src=(data, key))
|
|
40
|
+
return fd
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def config_to_formdict(args: ConfigInstance, descr: dict, _path="") -> FormDict:
|
|
44
|
+
""" Convert the dataclass produced by tyro into dict of dicts. """
|
|
45
|
+
main = ""
|
|
46
|
+
params = {main: {}} if not _path else {}
|
|
47
|
+
for param, val in vars(args).items():
|
|
48
|
+
annotation = None
|
|
49
|
+
if val is None:
|
|
50
|
+
wanted_type = get_type_hints(args.__class__).get(param)
|
|
51
|
+
if wanted_type in (Optional[int], Optional[str]):
|
|
52
|
+
# Since tkinter_form does not handle None yet, we have help it.
|
|
53
|
+
# We need it to be able to write a number and if empty, return None.
|
|
54
|
+
# This would fail: `severity: int | None = None`
|
|
55
|
+
# Here, we convert None to str(""), in normalize_types we convert it back.
|
|
56
|
+
annotation = wanted_type
|
|
57
|
+
val = ""
|
|
58
|
+
else:
|
|
59
|
+
# An unknown type annotation encountered-
|
|
60
|
+
# Since tkinter_form does not handle None yet, this will display as checkbox.
|
|
61
|
+
# Which is not probably wanted.
|
|
62
|
+
val = False
|
|
63
|
+
logger.warn(f"Annotation {wanted_type} of `{param}` not supported by Mininterface."
|
|
64
|
+
"None converted to False.")
|
|
65
|
+
if hasattr(val, "__dict__"): # nested config hierarchy
|
|
66
|
+
params[param] = config_to_formdict(val, descr, _path=f"{_path}{param}.")
|
|
67
|
+
elif not _path: # scalar value in root
|
|
68
|
+
params[main][param] = FormField(val, descr.get(param), annotation, param, src2=(args, param))
|
|
69
|
+
else: # scalar value in nested
|
|
70
|
+
params[param] = FormField(val, descr.get(f"{_path}{param}"), annotation, param, src2=(args, param))
|
|
71
|
+
return params
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_args_allow_missing(config: Type[ConfigInstance], kwargs: dict, parser: ArgumentParser) -> ConfigInstance:
|
|
75
|
+
""" Fetch missing required options in GUI. """
|
|
76
|
+
# On missing argument, tyro fail. We cannot determine which one was missing, except by intercepting
|
|
77
|
+
# the error message function. Then, we reconstruct the missing options.
|
|
78
|
+
# NOTE But we should rather invoke a GUI with the missing options only.
|
|
79
|
+
original_error = TyroArgumentParser.error
|
|
80
|
+
eavesdrop = ""
|
|
81
|
+
|
|
82
|
+
def custom_error(self, message: str):
|
|
83
|
+
nonlocal eavesdrop
|
|
84
|
+
if not message.startswith("the following arguments are required:"):
|
|
85
|
+
return original_error(self, message)
|
|
86
|
+
eavesdrop = message
|
|
87
|
+
raise SystemExit(2) # will be catched
|
|
88
|
+
|
|
89
|
+
# Set args to determine whether to use sys.argv.
|
|
90
|
+
# Why settings args? Prevent tyro using sys.argv if we are in an interactive shell like Jupyter,
|
|
91
|
+
# as sys.argv is non-related there.
|
|
92
|
+
try:
|
|
93
|
+
# Note wherease `"get_ipython" in globals()` returns True in Jupyter, it is still False
|
|
94
|
+
# in a script a Jupyter cell runs. Hence we must put here this lengthty statement.
|
|
95
|
+
global get_ipython
|
|
96
|
+
get_ipython()
|
|
97
|
+
except:
|
|
98
|
+
args = None
|
|
99
|
+
else:
|
|
100
|
+
args = []
|
|
101
|
+
try:
|
|
102
|
+
with patch.object(TyroArgumentParser, 'error', custom_error):
|
|
103
|
+
return cli(config, args=args, **kwargs)
|
|
104
|
+
except BaseException as e:
|
|
105
|
+
if hasattr(e, "code") and e.code == 2 and eavesdrop: # Some arguments are missing. Determine which.
|
|
106
|
+
for arg in eavesdrop.partition(":")[2].strip().split(", "):
|
|
107
|
+
argument: Action = next(iter(p for p in parser._actions if arg in p.option_strings))
|
|
108
|
+
argument.default = "DEFAULT" # NOTE I do not know whether used
|
|
109
|
+
if "." in argument.dest: # missing nested required argument handler not implemented, we make tyro fail in CLI
|
|
110
|
+
pass
|
|
111
|
+
else:
|
|
112
|
+
match argument.metavar:
|
|
113
|
+
case "INT":
|
|
114
|
+
setattr(kwargs["default"], argument.dest, 0)
|
|
115
|
+
case "STR":
|
|
116
|
+
setattr(kwargs["default"], argument.dest, "")
|
|
117
|
+
case _:
|
|
118
|
+
pass # missing handler not implemented, we make tyro fail in CLI
|
|
119
|
+
return cli(config, **kwargs) # second attempt
|
|
120
|
+
raise
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Iterable, Optional, TypeVar, get_args
|
|
3
|
+
|
|
4
|
+
from .auxiliary import flatten
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .FormDict import FormDict
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from tkinter_form import Value
|
|
11
|
+
except ImportError:
|
|
12
|
+
# TODO put into GuiInterface create_ui(ff: FormField)
|
|
13
|
+
@dataclass
|
|
14
|
+
class Value:
|
|
15
|
+
""" This class helps to enrich the field with a description. """
|
|
16
|
+
val: Any
|
|
17
|
+
description: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
FFValue = TypeVar("FFValue")
|
|
21
|
+
TD = TypeVar("TD")
|
|
22
|
+
""" dict """
|
|
23
|
+
TK = TypeVar("TK")
|
|
24
|
+
""" dict key """
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class FormField(Value):
|
|
29
|
+
""" Bridge between the input values and a UI widget.
|
|
30
|
+
Helps to creates a widget from the input value (includes description etc.),
|
|
31
|
+
then transforms the value back (str to int conversion etc).
|
|
32
|
+
|
|
33
|
+
(Ex: Merge the dict of dicts from the GUI back into the object holding the configuration.)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
annotation: Any | None = None
|
|
37
|
+
""" Used for validation. To convert an empty '' to None. """
|
|
38
|
+
name: str | None = None # NOTE: Only TextualInterface uses this by now.
|
|
39
|
+
|
|
40
|
+
src: tuple[TD, TK] | None = None
|
|
41
|
+
""" The original dict to be updated when UI ends. """
|
|
42
|
+
src2: tuple[TD, TK] | None = None
|
|
43
|
+
""" The original object to be updated when UI ends.
|
|
44
|
+
NOTE should be merged to `src`
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __post_init__(self):
|
|
48
|
+
self._original_desc = self.description
|
|
49
|
+
|
|
50
|
+
def set_error_text(self, s):
|
|
51
|
+
self.description = f"{s} {self._original_desc}"
|
|
52
|
+
|
|
53
|
+
def update(self, ui_value):
|
|
54
|
+
""" UI value → FormField value → original value. (With type conversion and checks.)
|
|
55
|
+
|
|
56
|
+
The value has been updated in a UI.
|
|
57
|
+
Update accordingly the value in the original linked dict
|
|
58
|
+
the mininterface was invoked with.
|
|
59
|
+
|
|
60
|
+
Validates the type and do the transformation.
|
|
61
|
+
(Ex: Some values might be nulled from "".)
|
|
62
|
+
"""
|
|
63
|
+
fixed_value = ui_value
|
|
64
|
+
if self.annotation:
|
|
65
|
+
if ui_value == "" and type(None) in get_args(self.annotation):
|
|
66
|
+
# The user is not able to set the value to None, they left it empty.
|
|
67
|
+
# Cast back to None as None is one of the allowed types.
|
|
68
|
+
# Ex: `severity: int | None = None`
|
|
69
|
+
fixed_value = None
|
|
70
|
+
elif self.annotation == Optional[int]:
|
|
71
|
+
try:
|
|
72
|
+
fixed_value = int(ui_value)
|
|
73
|
+
except ValueError:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
if not isinstance(fixed_value, self.annotation):
|
|
77
|
+
self.set_error_text(f"Type must be `{self.annotation}`!")
|
|
78
|
+
return False # revision needed
|
|
79
|
+
|
|
80
|
+
# keep values if revision needed
|
|
81
|
+
# We merge new data to the origin. If form is re-submitted, the values will stay there.
|
|
82
|
+
self.val = ui_value
|
|
83
|
+
|
|
84
|
+
# Store to the source user data
|
|
85
|
+
if self.src:
|
|
86
|
+
d, k = self.src
|
|
87
|
+
d[k] = fixed_value
|
|
88
|
+
elif self.src2:
|
|
89
|
+
d, k = self.src2
|
|
90
|
+
setattr(d, k, fixed_value)
|
|
91
|
+
else:
|
|
92
|
+
# This might be user-created object. The user reads directly from this. There is no need to update anything.
|
|
93
|
+
pass
|
|
94
|
+
return True
|
|
95
|
+
# Fixing types:
|
|
96
|
+
# This code would support tuple[int, int]:
|
|
97
|
+
#
|
|
98
|
+
# self.types = get_args(self.annotation) \
|
|
99
|
+
# if isinstance(self.annotation, UnionType) else (self.annotation, )
|
|
100
|
+
# "All possible types in a tuple. Ex 'int | str' -> (int, str)"
|
|
101
|
+
#
|
|
102
|
+
#
|
|
103
|
+
# def convert(self):
|
|
104
|
+
# """ Convert the self.value to the given self.type.
|
|
105
|
+
# The value might be in str due to CLI or TUI whereas the programs wants bool.
|
|
106
|
+
# """
|
|
107
|
+
# # if self.value == "True":
|
|
108
|
+
# # return True
|
|
109
|
+
# # if self.value == "False":
|
|
110
|
+
# # return False
|
|
111
|
+
# if type(self.val) is str and str not in self.types:
|
|
112
|
+
# try:
|
|
113
|
+
# return literal_eval(self.val) # ex: int, tuple[int, int]
|
|
114
|
+
# except:
|
|
115
|
+
# raise ValueError(f"{self.name}: Cannot convert value {self.val}")
|
|
116
|
+
# return self.val
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def submit_values(updater: Iterable[tuple["FormField", FFValue]]) -> bool:
|
|
120
|
+
""" Returns whether the form is alright or whether we should revise it.
|
|
121
|
+
Input is tuple of the FormFields and their new values from the UI.
|
|
122
|
+
"""
|
|
123
|
+
# Why list? We need all the FormField values be updates from the UI.
|
|
124
|
+
# If the revision is needed, the UI fetches the values from the FormField.
|
|
125
|
+
# We need the keep the values so that the user does not have to re-write them.
|
|
126
|
+
return all(list(ff.update(ui_value) for ff, ui_value in updater))
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def submit(fd: "FormDict", ui: dict):
|
|
130
|
+
""" Returns whether the form is alright or whether we should revise it.
|
|
131
|
+
Input is the FormDict and the UI dict in the very same form.
|
|
132
|
+
"""
|
|
133
|
+
return FormField.submit_values(zip(flatten(fd), flatten(ui)))
|