mininterface 0.4.4rc3__tar.gz → 0.5.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-0.4.4rc3 → mininterface-0.5.0}/PKG-INFO +28 -28
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/README.md +27 -27
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/FormDict.py +23 -15
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/FormField.py +52 -22
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/GuiInterface.py +26 -19
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/Mininterface.py +53 -29
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/Redirectable.py +1 -1
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/TextInterface.py +5 -5
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/TextualInterface.py +20 -17
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/__init__.py +25 -14
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/__main__.py +3 -3
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/pyproject.toml +1 -1
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/auxiliary.py +0 -0
- {mininterface-0.4.4rc3 → mininterface-0.5.0}/mininterface/common.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -37,14 +37,14 @@ from dataclasses import dataclass
|
|
|
37
37
|
from mininterface import run
|
|
38
38
|
|
|
39
39
|
@dataclass
|
|
40
|
-
class
|
|
40
|
+
class Env:
|
|
41
41
|
"""Set of options."""
|
|
42
42
|
test: bool = False # My testing flag
|
|
43
43
|
important_number: int = 4 # This number is very important
|
|
44
44
|
|
|
45
45
|
if __name__ == "__main__":
|
|
46
|
-
|
|
47
|
-
print(
|
|
46
|
+
env = run(Env, prog="My application").get_env()
|
|
47
|
+
print(env.important_number) # suggested by the IDE with the hint text "This number is very important"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## You got CLI
|
|
@@ -74,13 +74,13 @@ important_number: 555
|
|
|
74
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.
|
|
75
75
|
|
|
76
76
|
```python
|
|
77
|
-
with run(
|
|
78
|
-
print(f"Your important number is {m}")
|
|
77
|
+
with run(Env) as m:
|
|
78
|
+
print(f"Your important number is {m.env.important_number}")
|
|
79
79
|
boolean = m.is_yes("Is that alright?")
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|

|
|
83
|
-

|
|
84
84
|
|
|
85
85
|
# Contents
|
|
86
86
|
- [Mininterface – GUI, TUI, CLI and config](#mininterface-gui-tui-cli-and-config)
|
|
@@ -93,24 +93,24 @@ with run(Config) as m:
|
|
|
93
93
|
+ [`Mininterface(title: str = '')`](#mininterfacetitle-str--)
|
|
94
94
|
+ [`alert(text: str)`](#alerttext-str)
|
|
95
95
|
+ [`ask(text: str) -> str`](#asktext-str---str)
|
|
96
|
-
+ [`
|
|
96
|
+
+ [`ask_env() -> EnvInstance`](#ask_env--configinstance)
|
|
97
97
|
+ [`ask_number(text: str) -> int`](#ask_numbertext-str---int)
|
|
98
|
-
+ [`form(
|
|
99
|
-
+ [`
|
|
98
|
+
+ [`form(env: FormDict, title="") -> int`](#formenv-formdict-title---dict)
|
|
99
|
+
+ [`get_env(ask_on_empty_cli=True) -> ~EnvInstance`](#get_envask_on_empty_clitrue---configinstance)
|
|
100
100
|
+ [`is_no(text: str) -> bool`](#is_notext-str---bool)
|
|
101
101
|
+ [`is_yes(text: str) -> bool`](#is_yestext-str---bool)
|
|
102
|
-
+ [`
|
|
102
|
+
+ [`parse_env(config: Type[EnvInstance], config_file: pathlib.Path | None = None, **kwargs) -> EnvInstance`](#parse_envconfig-type-configinstance-config_file-pathlibpath--none--none-kwargs---configinstance)
|
|
103
103
|
* [Standalone](#standalone)
|
|
104
104
|
|
|
105
105
|
# Background
|
|
106
106
|
|
|
107
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.
|
|
108
108
|
|
|
109
|
-
Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. `
|
|
109
|
+
Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. `Mininterface` is here to help.
|
|
110
110
|
|
|
111
111
|
The config variables needed by your program are kept in cozy dataclasses. Write less! The syntax of [tyro](https://github.com/brentyi/tyro) does not require any overhead (as its `argparse` alternatives do). You just annotate a class attribute, append a simple docstring and get a fully functional application:
|
|
112
112
|
* Call it as `program.py --help` to display full help.
|
|
113
|
-
* Use any flag in CLI: `program.py --test` causes `
|
|
113
|
+
* Use any flag in CLI: `program.py --test` causes `env.test` be set to `True`.
|
|
114
114
|
* The main benefit: Launch it without parameters as `program.py` to get a full working window with all the flags ready to be edited.
|
|
115
115
|
* Running on a remote machine? Automatic regression to the text interface.
|
|
116
116
|
|
|
@@ -118,7 +118,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
118
118
|
|
|
119
119
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
120
120
|
|
|
121
|
-
```
|
|
121
|
+
```bash
|
|
122
122
|
pip install mininterface
|
|
123
123
|
```
|
|
124
124
|
|
|
@@ -161,9 +161,9 @@ $./program.py --further.host example.net
|
|
|
161
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.
|
|
162
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`.
|
|
163
163
|
|
|
164
|
-
* `config:Type[
|
|
164
|
+
* `config:Type[EnvInstance]`: Dataclass with the configuration.
|
|
165
165
|
* `interface`: Which interface to prefer. By default, we use the GUI, the fallback is the REPL.
|
|
166
|
-
* `**kwargs`: The same as for [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html).
|
|
166
|
+
* `**kwargs`: The same as for [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser).
|
|
167
167
|
* Returns: `interface` Interface used.
|
|
168
168
|
|
|
169
169
|
You cay context manage the function by a `with` statement. The stdout will be redirected to the interface (GUI window).
|
|
@@ -181,7 +181,7 @@ Several interfaces exist:
|
|
|
181
181
|
* `TextInterface` – Plain text only interface with no dependency as a fallback.
|
|
182
182
|
* `ReplInterface` – A debug terminal. Invokes a breakpoint after every dialog.
|
|
183
183
|
|
|
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 `
|
|
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_env` if needed.
|
|
185
185
|
|
|
186
186
|
```python
|
|
187
187
|
with TuiInterface("My program") as m:
|
|
@@ -194,20 +194,20 @@ Initialize.
|
|
|
194
194
|
Prompt the user to confirm the text.
|
|
195
195
|
### `ask(text: str) -> str`
|
|
196
196
|
Prompt the user to input a text.
|
|
197
|
-
### `
|
|
198
|
-
Allow the user to edit whole configuration. (Previously fetched from CLI and config file by
|
|
199
|
-
### `
|
|
197
|
+
### `ask_env() -> EnvInstance`
|
|
198
|
+
Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_env.)
|
|
199
|
+
### `ask_number(text: str) -> int`
|
|
200
|
+
Prompt the user to input a number. Empty input = 0.
|
|
201
|
+
### `form(env: FormDict, title="") -> dict`
|
|
200
202
|
Prompt the user to fill up whole form.
|
|
201
|
-
* `
|
|
203
|
+
* `env`: Dict of `{labels: default value}`. The form widget infers from the default value type.
|
|
202
204
|
The dict can be nested, it can contain a subgroup.
|
|
203
205
|
The default value might be `mininterface.FormField` that allows you to add descriptions.
|
|
204
206
|
A checkbox example: `{"my label": FormField(True, "my description")}`
|
|
205
207
|
* `title`: Optional form title.
|
|
206
|
-
### `
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
Returns whole configuration (previously fetched from CLI and config file by parse_args).
|
|
210
|
-
If program was launched with no arguments (empty CLI), invokes self.ask_args() to edit the fields.
|
|
208
|
+
### `get_env(ask_on_empty_cli=True) -> EnvInstance`
|
|
209
|
+
Returns whole configuration (previously fetched from CLI and config file by parse_env).
|
|
210
|
+
If program was launched with no arguments (empty CLI), invokes self.ask_env() to edit the fields.
|
|
211
211
|
### `is_no(text: str) -> bool`
|
|
212
212
|
Display confirm box, focusing no.
|
|
213
213
|
### `is_yes(text: str) -> bool`
|
|
@@ -218,12 +218,12 @@ m = run(prog="My program")
|
|
|
218
218
|
print(m.ask_yes("Is it true?")) # True/False
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
### `
|
|
221
|
+
### `parse_env(config: Type[EnvInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~EnvInstance`
|
|
222
222
|
Parse CLI arguments, possibly merged from a config file.
|
|
223
223
|
* `config`: Dataclass with the configuration.
|
|
224
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.
|
|
225
225
|
* `**kwargs` The same as for argparse.ArgumentParser.
|
|
226
|
-
* Returns: `
|
|
226
|
+
* Returns: `EnvInstance` Configuration namespace.
|
|
227
227
|
|
|
228
228
|
## Standalone
|
|
229
229
|
|
|
@@ -14,14 +14,14 @@ from dataclasses import dataclass
|
|
|
14
14
|
from mininterface import run
|
|
15
15
|
|
|
16
16
|
@dataclass
|
|
17
|
-
class
|
|
17
|
+
class Env:
|
|
18
18
|
"""Set of options."""
|
|
19
19
|
test: bool = False # My testing flag
|
|
20
20
|
important_number: int = 4 # This number is very important
|
|
21
21
|
|
|
22
22
|
if __name__ == "__main__":
|
|
23
|
-
|
|
24
|
-
print(
|
|
23
|
+
env = run(Env, prog="My application").get_env()
|
|
24
|
+
print(env.important_number) # suggested by the IDE with the hint text "This number is very important"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## You got CLI
|
|
@@ -51,13 +51,13 @@ important_number: 555
|
|
|
51
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.
|
|
52
52
|
|
|
53
53
|
```python
|
|
54
|
-
with run(
|
|
55
|
-
print(f"Your important number is {m}")
|
|
54
|
+
with run(Env) as m:
|
|
55
|
+
print(f"Your important number is {m.env.important_number}")
|
|
56
56
|
boolean = m.is_yes("Is that alright?")
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|

|
|
60
|
-

|
|
61
61
|
|
|
62
62
|
# Contents
|
|
63
63
|
- [Mininterface – GUI, TUI, CLI and config](#mininterface-gui-tui-cli-and-config)
|
|
@@ -70,24 +70,24 @@ with run(Config) as m:
|
|
|
70
70
|
+ [`Mininterface(title: str = '')`](#mininterfacetitle-str--)
|
|
71
71
|
+ [`alert(text: str)`](#alerttext-str)
|
|
72
72
|
+ [`ask(text: str) -> str`](#asktext-str---str)
|
|
73
|
-
+ [`
|
|
73
|
+
+ [`ask_env() -> EnvInstance`](#ask_env--configinstance)
|
|
74
74
|
+ [`ask_number(text: str) -> int`](#ask_numbertext-str---int)
|
|
75
|
-
+ [`form(
|
|
76
|
-
+ [`
|
|
75
|
+
+ [`form(env: FormDict, title="") -> int`](#formenv-formdict-title---dict)
|
|
76
|
+
+ [`get_env(ask_on_empty_cli=True) -> ~EnvInstance`](#get_envask_on_empty_clitrue---configinstance)
|
|
77
77
|
+ [`is_no(text: str) -> bool`](#is_notext-str---bool)
|
|
78
78
|
+ [`is_yes(text: str) -> bool`](#is_yestext-str---bool)
|
|
79
|
-
+ [`
|
|
79
|
+
+ [`parse_env(config: Type[EnvInstance], config_file: pathlib.Path | None = None, **kwargs) -> EnvInstance`](#parse_envconfig-type-configinstance-config_file-pathlibpath--none--none-kwargs---configinstance)
|
|
80
80
|
* [Standalone](#standalone)
|
|
81
81
|
|
|
82
82
|
# Background
|
|
83
83
|
|
|
84
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.
|
|
85
85
|
|
|
86
|
-
Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. `
|
|
86
|
+
Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. `Mininterface` is here to help.
|
|
87
87
|
|
|
88
88
|
The config variables needed by your program are kept in cozy dataclasses. Write less! The syntax of [tyro](https://github.com/brentyi/tyro) does not require any overhead (as its `argparse` alternatives do). You just annotate a class attribute, append a simple docstring and get a fully functional application:
|
|
89
89
|
* Call it as `program.py --help` to display full help.
|
|
90
|
-
* Use any flag in CLI: `program.py --test` causes `
|
|
90
|
+
* Use any flag in CLI: `program.py --test` causes `env.test` be set to `True`.
|
|
91
91
|
* The main benefit: Launch it without parameters as `program.py` to get a full working window with all the flags ready to be edited.
|
|
92
92
|
* Running on a remote machine? Automatic regression to the text interface.
|
|
93
93
|
|
|
@@ -95,7 +95,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
95
95
|
|
|
96
96
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
97
97
|
|
|
98
|
-
```
|
|
98
|
+
```bash
|
|
99
99
|
pip install mininterface
|
|
100
100
|
```
|
|
101
101
|
|
|
@@ -138,9 +138,9 @@ $./program.py --further.host example.net
|
|
|
138
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.
|
|
139
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`.
|
|
140
140
|
|
|
141
|
-
* `config:Type[
|
|
141
|
+
* `config:Type[EnvInstance]`: Dataclass with the configuration.
|
|
142
142
|
* `interface`: Which interface to prefer. By default, we use the GUI, the fallback is the REPL.
|
|
143
|
-
* `**kwargs`: The same as for [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html).
|
|
143
|
+
* `**kwargs`: The same as for [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser).
|
|
144
144
|
* Returns: `interface` Interface used.
|
|
145
145
|
|
|
146
146
|
You cay context manage the function by a `with` statement. The stdout will be redirected to the interface (GUI window).
|
|
@@ -158,7 +158,7 @@ Several interfaces exist:
|
|
|
158
158
|
* `TextInterface` – Plain text only interface with no dependency as a fallback.
|
|
159
159
|
* `ReplInterface` – A debug terminal. Invokes a breakpoint after every dialog.
|
|
160
160
|
|
|
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 `
|
|
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_env` if needed.
|
|
162
162
|
|
|
163
163
|
```python
|
|
164
164
|
with TuiInterface("My program") as m:
|
|
@@ -171,20 +171,20 @@ Initialize.
|
|
|
171
171
|
Prompt the user to confirm the text.
|
|
172
172
|
### `ask(text: str) -> str`
|
|
173
173
|
Prompt the user to input a text.
|
|
174
|
-
### `
|
|
175
|
-
Allow the user to edit whole configuration. (Previously fetched from CLI and config file by
|
|
176
|
-
### `
|
|
174
|
+
### `ask_env() -> EnvInstance`
|
|
175
|
+
Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_env.)
|
|
176
|
+
### `ask_number(text: str) -> int`
|
|
177
|
+
Prompt the user to input a number. Empty input = 0.
|
|
178
|
+
### `form(env: FormDict, title="") -> dict`
|
|
177
179
|
Prompt the user to fill up whole form.
|
|
178
|
-
* `
|
|
180
|
+
* `env`: Dict of `{labels: default value}`. The form widget infers from the default value type.
|
|
179
181
|
The dict can be nested, it can contain a subgroup.
|
|
180
182
|
The default value might be `mininterface.FormField` that allows you to add descriptions.
|
|
181
183
|
A checkbox example: `{"my label": FormField(True, "my description")}`
|
|
182
184
|
* `title`: Optional form title.
|
|
183
|
-
### `
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
Returns whole configuration (previously fetched from CLI and config file by parse_args).
|
|
187
|
-
If program was launched with no arguments (empty CLI), invokes self.ask_args() to edit the fields.
|
|
185
|
+
### `get_env(ask_on_empty_cli=True) -> EnvInstance`
|
|
186
|
+
Returns whole configuration (previously fetched from CLI and config file by parse_env).
|
|
187
|
+
If program was launched with no arguments (empty CLI), invokes self.ask_env() to edit the fields.
|
|
188
188
|
### `is_no(text: str) -> bool`
|
|
189
189
|
Display confirm box, focusing no.
|
|
190
190
|
### `is_yes(text: str) -> bool`
|
|
@@ -195,12 +195,12 @@ m = run(prog="My program")
|
|
|
195
195
|
print(m.ask_yes("Is it true?")) # True/False
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
-
### `
|
|
198
|
+
### `parse_env(config: Type[EnvInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~EnvInstance`
|
|
199
199
|
Parse CLI arguments, possibly merged from a config file.
|
|
200
200
|
* `config`: Dataclass with the configuration.
|
|
201
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.
|
|
202
202
|
* `**kwargs` The same as for argparse.ArgumentParser.
|
|
203
|
-
* Returns: `
|
|
203
|
+
* Returns: `EnvInstance` Configuration namespace.
|
|
204
204
|
|
|
205
205
|
## Standalone
|
|
206
206
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""
|
|
4
4
|
import logging
|
|
5
5
|
from argparse import Action, ArgumentParser
|
|
6
|
-
from typing import Callable, Optional, Type, TypeVar, Union, get_type_hints
|
|
6
|
+
from typing import Any, Callable, Optional, Type, TypeVar, Union, get_type_hints
|
|
7
7
|
from unittest.mock import patch
|
|
8
8
|
|
|
9
9
|
from tyro import cli
|
|
@@ -13,8 +13,7 @@ from .FormField import FormField
|
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
ConfigClass = Type[ConfigInstance]
|
|
16
|
+
EnvClass = TypeVar("EnvInstance")
|
|
18
17
|
FormDict = dict[str, Union[FormField, 'FormDict']]
|
|
19
18
|
""" Nested form that can have descriptions (through FormField) instead of plain values. """
|
|
20
19
|
|
|
@@ -36,18 +35,27 @@ def dict_to_formdict(data: dict) -> FormDict:
|
|
|
36
35
|
fd[key] = dict_to_formdict(val)
|
|
37
36
|
else: # scalar value
|
|
38
37
|
# NOTE name=param is not set (yet?) in `config_to_formdict`, neither `src`
|
|
39
|
-
fd[key] = FormField(val, "", name=key,
|
|
38
|
+
fd[key] = FormField(val, "", name=key, src_dict=(data, key)) if not isinstance(val, FormField) else val
|
|
40
39
|
return fd
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
def
|
|
42
|
+
def formdict_to_widgetdict(d: FormDict | Any, widgetize_callback: Callable):
|
|
43
|
+
if isinstance(d, dict):
|
|
44
|
+
return {k: formdict_to_widgetdict(v, widgetize_callback) for k, v in d.items()}
|
|
45
|
+
elif isinstance(d, FormField):
|
|
46
|
+
return widgetize_callback(d)
|
|
47
|
+
else:
|
|
48
|
+
return d
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def config_to_formdict(env: EnvClass, descr: dict, _path="") -> FormDict:
|
|
44
52
|
""" Convert the dataclass produced by tyro into dict of dicts. """
|
|
45
53
|
main = ""
|
|
46
54
|
params = {main: {}} if not _path else {}
|
|
47
|
-
for param, val in vars(
|
|
55
|
+
for param, val in vars(env).items():
|
|
48
56
|
annotation = None
|
|
49
57
|
if val is None:
|
|
50
|
-
wanted_type = get_type_hints(
|
|
58
|
+
wanted_type = get_type_hints(env.__class__).get(param)
|
|
51
59
|
if wanted_type in (Optional[int], Optional[str]):
|
|
52
60
|
# Since tkinter_form does not handle None yet, we have help it.
|
|
53
61
|
# We need it to be able to write a number and if empty, return None.
|
|
@@ -65,13 +73,13 @@ def config_to_formdict(args: ConfigInstance, descr: dict, _path="") -> FormDict:
|
|
|
65
73
|
if hasattr(val, "__dict__"): # nested config hierarchy
|
|
66
74
|
params[param] = config_to_formdict(val, descr, _path=f"{_path}{param}.")
|
|
67
75
|
elif not _path: # scalar value in root
|
|
68
|
-
params[main][param] = FormField(val, descr.get(param), annotation, param,
|
|
76
|
+
params[main][param] = FormField(val, descr.get(param), annotation, param, src_obj=(env, param))
|
|
69
77
|
else: # scalar value in nested
|
|
70
|
-
params[param] = FormField(val, descr.get(f"{_path}{param}"), annotation, param,
|
|
78
|
+
params[param] = FormField(val, descr.get(f"{_path}{param}"), annotation, param, src_obj=(env, param))
|
|
71
79
|
return params
|
|
72
80
|
|
|
73
81
|
|
|
74
|
-
def
|
|
82
|
+
def get_env_allow_missing(config: Type[EnvClass], kwargs: dict, parser: ArgumentParser) -> EnvClass:
|
|
75
83
|
""" Fetch missing required options in GUI. """
|
|
76
84
|
# On missing argument, tyro fail. We cannot determine which one was missing, except by intercepting
|
|
77
85
|
# the error message function. Then, we reconstruct the missing options.
|
|
@@ -86,8 +94,8 @@ def get_args_allow_missing(config: Type[ConfigInstance], kwargs: dict, parser: A
|
|
|
86
94
|
eavesdrop = message
|
|
87
95
|
raise SystemExit(2) # will be catched
|
|
88
96
|
|
|
89
|
-
# Set
|
|
90
|
-
# Why settings
|
|
97
|
+
# Set env to determine whether to use sys.argv.
|
|
98
|
+
# Why settings env? Prevent tyro using sys.argv if we are in an interactive shell like Jupyter,
|
|
91
99
|
# as sys.argv is non-related there.
|
|
92
100
|
try:
|
|
93
101
|
# Note wherease `"get_ipython" in globals()` returns True in Jupyter, it is still False
|
|
@@ -95,12 +103,12 @@ def get_args_allow_missing(config: Type[ConfigInstance], kwargs: dict, parser: A
|
|
|
95
103
|
global get_ipython
|
|
96
104
|
get_ipython()
|
|
97
105
|
except:
|
|
98
|
-
|
|
106
|
+
env = None
|
|
99
107
|
else:
|
|
100
|
-
|
|
108
|
+
env = []
|
|
101
109
|
try:
|
|
102
110
|
with patch.object(TyroArgumentParser, 'error', custom_error):
|
|
103
|
-
return cli(config, args=
|
|
111
|
+
return cli(config, args=env, **kwargs)
|
|
104
112
|
except BaseException as e:
|
|
105
113
|
if hasattr(e, "code") and e.code == 2 and eavesdrop: # Some arguments are missing. Determine which.
|
|
106
114
|
for arg in eavesdrop.partition(":")[2].strip().split(", "):
|
|
@@ -6,17 +6,6 @@ from .auxiliary import flatten
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
7
|
from .FormDict import FormDict
|
|
8
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
9
|
FFValue = TypeVar("FFValue")
|
|
21
10
|
TD = TypeVar("TD")
|
|
22
11
|
""" dict """
|
|
@@ -25,31 +14,55 @@ TK = TypeVar("TK")
|
|
|
25
14
|
|
|
26
15
|
|
|
27
16
|
@dataclass
|
|
28
|
-
class FormField
|
|
29
|
-
"""
|
|
17
|
+
class FormField:
|
|
18
|
+
""" This class helps to enrich the field with a description.
|
|
19
|
+
Bridge between the input values and a UI widget.
|
|
30
20
|
Helps to creates a widget from the input value (includes description etc.),
|
|
31
21
|
then transforms the value back (str to int conversion etc).
|
|
32
22
|
|
|
33
23
|
(Ex: Merge the dict of dicts from the GUI back into the object holding the configuration.)
|
|
34
24
|
"""
|
|
35
25
|
|
|
26
|
+
val: Any
|
|
27
|
+
""" The value being enriched by this object. """
|
|
28
|
+
description: str
|
|
29
|
+
""" The description. """
|
|
30
|
+
|
|
36
31
|
annotation: Any | None = None
|
|
37
|
-
""" Used for validation. To convert an empty '' to None.
|
|
38
|
-
|
|
32
|
+
""" Used for validation. To convert an empty '' to None.
|
|
33
|
+
If not set, will be determined automatically from the `val` type.
|
|
34
|
+
"""
|
|
35
|
+
name: str | None = None
|
|
36
|
+
""" NOTE: Only TextualInterface uses this by now.
|
|
37
|
+
GuiInterface reads the name from the dict.
|
|
38
|
+
In the future, Textual should be able to do the same
|
|
39
|
+
and both, Gui and Textual should use FormField.name as override.
|
|
40
|
+
"""
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
""" The original dict to be updated when UI ends.
|
|
42
|
-
|
|
42
|
+
src_dict: tuple[TD, TK] | None = None
|
|
43
|
+
""" The original dict to be updated when UI ends.
|
|
44
|
+
The processed value is in the self.processed_value too.
|
|
45
|
+
"""
|
|
46
|
+
src_obj: tuple[TD, TK] | None = None
|
|
43
47
|
""" The original object to be updated when UI ends.
|
|
48
|
+
The processed value is in the self.processed_value too.
|
|
44
49
|
NOTE should be merged to `src`
|
|
45
50
|
"""
|
|
46
51
|
|
|
52
|
+
processed_value = None
|
|
53
|
+
""" The value set while processed through the UI. """
|
|
54
|
+
|
|
47
55
|
def __post_init__(self):
|
|
56
|
+
if not self.annotation:
|
|
57
|
+
self.annotation = type(self.val)
|
|
48
58
|
self._original_desc = self.description
|
|
49
59
|
|
|
50
60
|
def set_error_text(self, s):
|
|
51
61
|
self.description = f"{s} {self._original_desc}"
|
|
52
62
|
|
|
63
|
+
def remove_error_text(self):
|
|
64
|
+
self.description = self._original_desc
|
|
65
|
+
|
|
53
66
|
def update(self, ui_value):
|
|
54
67
|
""" UI value → FormField value → original value. (With type conversion and checks.)
|
|
55
68
|
|
|
@@ -61,6 +74,7 @@ class FormField(Value):
|
|
|
61
74
|
(Ex: Some values might be nulled from "".)
|
|
62
75
|
"""
|
|
63
76
|
fixed_value = ui_value
|
|
77
|
+
self.remove_error_text()
|
|
64
78
|
if self.annotation:
|
|
65
79
|
if ui_value == "" and type(None) in get_args(self.annotation):
|
|
66
80
|
# The user is not able to set the value to None, they left it empty.
|
|
@@ -73,20 +87,36 @@ class FormField(Value):
|
|
|
73
87
|
except ValueError:
|
|
74
88
|
pass
|
|
75
89
|
|
|
90
|
+
if not isinstance(fixed_value, self.annotation):
|
|
91
|
+
if isinstance(fixed_value, str):
|
|
92
|
+
try:
|
|
93
|
+
# Textual ask_number -> user writes '123', this has to be converted to int 123
|
|
94
|
+
# NOTE: Unfortunately, type(list) looks awful here. @see TextualInterface.form comment.
|
|
95
|
+
fixed_value = self.annotation(ui_value)
|
|
96
|
+
except (TypeError, ValueError):
|
|
97
|
+
# Automatic conversion failed
|
|
98
|
+
pass
|
|
99
|
+
|
|
76
100
|
if not isinstance(fixed_value, self.annotation):
|
|
77
101
|
self.set_error_text(f"Type must be `{self.annotation}`!")
|
|
78
102
|
return False # revision needed
|
|
79
103
|
|
|
80
104
|
# keep values if revision needed
|
|
81
105
|
# We merge new data to the origin. If form is re-submitted, the values will stay there.
|
|
106
|
+
# NOTE: We might store `self.val = fixed_value`.
|
|
107
|
+
# This would help when the user defines FormField themselves
|
|
108
|
+
# because there is no other way to access fixed_value from outside (we try self.processed_value).
|
|
109
|
+
# However `self.val = fixed_value`` looks awful when TextualInterface have a list of strings
|
|
110
|
+
# and the form is recreated, strings split to letters, @see TextualInterface.form comment.
|
|
82
111
|
self.val = ui_value
|
|
112
|
+
self.processed_value = fixed_value
|
|
83
113
|
|
|
84
114
|
# Store to the source user data
|
|
85
|
-
if self.
|
|
86
|
-
d, k = self.
|
|
115
|
+
if self.src_dict:
|
|
116
|
+
d, k = self.src_dict
|
|
87
117
|
d[k] = fixed_value
|
|
88
|
-
elif self.
|
|
89
|
-
d, k = self.
|
|
118
|
+
elif self.src_obj:
|
|
119
|
+
d, k = self.src_obj
|
|
90
120
|
setattr(d, k, fixed_value)
|
|
91
121
|
else:
|
|
92
122
|
# This might be user-created object. The user reads directly from this. There is no need to update anything.
|
|
@@ -4,18 +4,18 @@ from typing import Any, Callable
|
|
|
4
4
|
try:
|
|
5
5
|
from tkinter import TclError, LEFT, Button, Frame, Label, Text, Tk
|
|
6
6
|
from tktooltip import ToolTip
|
|
7
|
-
from tkinter_form import Form
|
|
7
|
+
from tkinter_form import Form, Value
|
|
8
8
|
except ImportError:
|
|
9
9
|
from .common import InterfaceNotAvailable
|
|
10
10
|
raise InterfaceNotAvailable
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
from .common import InterfaceNotAvailable
|
|
14
|
-
from .FormDict import FormDict, config_to_formdict
|
|
14
|
+
from .FormDict import FormDict, config_to_formdict, dict_to_formdict, formdict_to_widgetdict
|
|
15
15
|
from .auxiliary import recursive_set_focus, flatten
|
|
16
16
|
from .Redirectable import RedirectTextTkinter, Redirectable
|
|
17
17
|
from .FormField import FormField
|
|
18
|
-
from .Mininterface import Cancelled,
|
|
18
|
+
from .Mininterface import BackendAdaptor, Cancelled, EnvClass, Mininterface
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class GuiInterface(Redirectable, Mininterface):
|
|
@@ -35,28 +35,29 @@ class GuiInterface(Redirectable, Mininterface):
|
|
|
35
35
|
self.window.buttons(text, [("Ok", None)])
|
|
36
36
|
|
|
37
37
|
def ask(self, text: str) -> str:
|
|
38
|
-
return self.
|
|
38
|
+
return self.form({text: ""})[text]
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def ask_env(self) -> EnvClass:
|
|
41
41
|
""" Display a window form with all parameters. """
|
|
42
|
-
formDict = config_to_formdict(self.
|
|
42
|
+
formDict = config_to_formdict(self.env, self.descriptions)
|
|
43
43
|
|
|
44
|
-
# formDict automatically fetches the edited values back to the
|
|
44
|
+
# formDict automatically fetches the edited values back to the EnvInstance
|
|
45
45
|
self.window.run_dialog(formDict)
|
|
46
|
-
return self.
|
|
46
|
+
return self.env
|
|
47
47
|
|
|
48
48
|
def form(self, form: FormDict, title: str = "") -> dict:
|
|
49
49
|
""" Prompt the user to fill up whole form.
|
|
50
|
-
:param
|
|
50
|
+
:param form: Dict of `{labels: default value}`. The form widget infers from the default value type.
|
|
51
51
|
The dict can be nested, it can contain a subgroup.
|
|
52
52
|
The default value might be `mininterface.FormField` that allows you to add descriptions.
|
|
53
53
|
A checkbox example: {"my label": FormField(True, "my description")}
|
|
54
54
|
:param title: Optional form title.
|
|
55
55
|
"""
|
|
56
|
-
|
|
56
|
+
self.window.run_dialog(dict_to_formdict(form), title=title)
|
|
57
|
+
return form
|
|
57
58
|
|
|
58
59
|
def ask_number(self, text: str) -> int:
|
|
59
|
-
return self.
|
|
60
|
+
return self.form({text: 0})[text]
|
|
60
61
|
|
|
61
62
|
def is_yes(self, text):
|
|
62
63
|
return self.window.yes_no(text, False)
|
|
@@ -65,7 +66,7 @@ class GuiInterface(Redirectable, Mininterface):
|
|
|
65
66
|
return self.window.yes_no(text, True)
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
class TkWindow(Tk):
|
|
69
|
+
class TkWindow(Tk, BackendAdaptor):
|
|
69
70
|
""" An editing window. """
|
|
70
71
|
|
|
71
72
|
def __init__(self, interface: GuiInterface):
|
|
@@ -85,6 +86,11 @@ class TkWindow(Tk):
|
|
|
85
86
|
self.pending_buffer = []
|
|
86
87
|
""" Text that has been written to the text widget but might not be yet seen by user. Because no mainloop was invoked. """
|
|
87
88
|
|
|
89
|
+
@staticmethod
|
|
90
|
+
def widgetize(ff: FormField) -> Value:
|
|
91
|
+
""" Wrap FormField to a textual widget. """
|
|
92
|
+
return Value(ff.val, ff.description)
|
|
93
|
+
|
|
88
94
|
def run_dialog(self, formDict: FormDict, title: str = "") -> FormDict:
|
|
89
95
|
""" Let the user edit the form_dict values in a GUI window.
|
|
90
96
|
On abrupt window close, the program exits.
|
|
@@ -92,12 +98,11 @@ class TkWindow(Tk):
|
|
|
92
98
|
if title:
|
|
93
99
|
label = Label(self.frame, text=title)
|
|
94
100
|
label.pack(pady=10)
|
|
95
|
-
|
|
96
101
|
self.form = Form(self.frame,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
name_form="",
|
|
103
|
+
form_dict=formdict_to_widgetdict(formDict, self.widgetize),
|
|
104
|
+
name_config="Ok",
|
|
105
|
+
)
|
|
101
106
|
self.form.pack()
|
|
102
107
|
|
|
103
108
|
# Set the submit and exit options
|
|
@@ -123,9 +128,11 @@ class TkWindow(Tk):
|
|
|
123
128
|
label = Label(self.frame, text=text)
|
|
124
129
|
label.pack(pady=10)
|
|
125
130
|
|
|
126
|
-
for text, value in buttons:
|
|
131
|
+
for i, (text, value) in enumerate(buttons):
|
|
127
132
|
button = Button(self.frame, text=text, command=lambda v=value: self._ok(v))
|
|
128
|
-
|
|
133
|
+
if i == focused-1:
|
|
134
|
+
b = button
|
|
135
|
+
button.bind("<Return>", lambda _: b.invoke())
|
|
129
136
|
button.pack(side=LEFT, padx=10)
|
|
130
137
|
self.frame.winfo_children()[focused].focus_set()
|
|
131
138
|
return self.mainloop()
|
|
@@ -9,7 +9,9 @@ from typing import Generic, Type
|
|
|
9
9
|
import yaml
|
|
10
10
|
from tyro.extras import get_parser
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
from .FormField import FormField
|
|
14
|
+
from .FormDict import EnvClass, FormDict, get_env_allow_missing
|
|
13
15
|
from .auxiliary import get_descriptions
|
|
14
16
|
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
@@ -20,18 +22,25 @@ class Cancelled(SystemExit):
|
|
|
20
22
|
pass
|
|
21
23
|
|
|
22
24
|
|
|
23
|
-
class Mininterface(Generic[
|
|
25
|
+
class Mininterface(Generic[EnvClass]):
|
|
24
26
|
""" The base interface.
|
|
25
27
|
Does not require any user input and hence is suitable for headless testing.
|
|
26
28
|
"""
|
|
27
29
|
|
|
28
|
-
def __init__(self, title: str = ""
|
|
30
|
+
def __init__(self, title: str = "",
|
|
31
|
+
env_class: Type[EnvClass] | None = None,
|
|
32
|
+
config_file: Path | str = "",
|
|
33
|
+
**kwargs):
|
|
29
34
|
self.title = title or "Mininterface"
|
|
30
|
-
self.
|
|
31
|
-
""" Parsed arguments, fetched from cli by
|
|
35
|
+
self.env: EnvClass = SimpleNamespace()
|
|
36
|
+
""" Parsed arguments, fetched from cli by self.parse_env """
|
|
32
37
|
self.descriptions = {}
|
|
33
38
|
""" Field descriptions """
|
|
34
39
|
|
|
40
|
+
# Load configuration from CLI and a config file
|
|
41
|
+
if env_class:
|
|
42
|
+
self._parse_env(env_class, config_file, **kwargs)
|
|
43
|
+
|
|
35
44
|
def __enter__(self) -> "Mininterface":
|
|
36
45
|
""" When used in the with statement, the GUI window does not vanish between dialogs
|
|
37
46
|
and it redirects the stdout to a text area. """
|
|
@@ -50,40 +59,40 @@ class Mininterface(Generic[ConfigInstance]):
|
|
|
50
59
|
print("Asking", text)
|
|
51
60
|
raise Cancelled(".. cancelled")
|
|
52
61
|
|
|
53
|
-
def
|
|
54
|
-
""" Allow the user to edit whole configuration. (Previously fetched from CLI and config file by
|
|
55
|
-
print("Asking the
|
|
56
|
-
return self.
|
|
62
|
+
def ask_env(self) -> EnvClass:
|
|
63
|
+
""" Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_env.) """
|
|
64
|
+
print("Asking the env", self.env)
|
|
65
|
+
return self.env
|
|
57
66
|
|
|
58
67
|
def ask_number(self, text: str) -> int:
|
|
59
68
|
""" Prompt the user to input a number. Empty input = 0. """
|
|
60
69
|
print("Asking number", text)
|
|
61
70
|
return 0
|
|
62
71
|
|
|
63
|
-
def form(self,
|
|
72
|
+
def form(self, form: FormDict, title: str = "") -> dict:
|
|
64
73
|
""" Prompt the user to fill up whole form.
|
|
65
|
-
:param
|
|
74
|
+
:param data: Dict of `{labels: default value}`. The form widget infers from the default value type.
|
|
66
75
|
The dict can be nested, it can contain a subgroup.
|
|
67
76
|
The default value might be `mininterface.FormField` that allows you to add descriptions.
|
|
68
77
|
A checkbox example: `{"my label": FormField(True, "my description")}`
|
|
69
78
|
"""
|
|
70
|
-
print(f"Asking the form {title}",
|
|
71
|
-
return
|
|
79
|
+
print(f"Asking the form {title}", form)
|
|
80
|
+
return form # NOTE – this should return dict, not FormDict (get rid of auxiliary.FormField values)
|
|
72
81
|
|
|
73
|
-
def
|
|
74
|
-
""" Returns whole configuration (previously fetched from CLI and config file by
|
|
75
|
-
If program was launched with no arguments (empty CLI), invokes self.
|
|
82
|
+
def get_env(self, ask_on_empty_cli=True) -> EnvClass:
|
|
83
|
+
""" Returns whole configuration (previously fetched from CLI and config file by parse_env).
|
|
84
|
+
If program was launched with no arguments (empty CLI), invokes self.ask_env() to edit the fields. """
|
|
76
85
|
# Empty CLI → GUI edit
|
|
77
86
|
if ask_on_empty_cli and len(sys.argv) <= 1:
|
|
78
|
-
return self.
|
|
79
|
-
return self.
|
|
87
|
+
return self.ask_env()
|
|
88
|
+
return self.env
|
|
80
89
|
|
|
81
|
-
def
|
|
90
|
+
def _parse_env(self, env_class: Type[EnvClass],
|
|
82
91
|
config_file: Path | None = None,
|
|
83
|
-
**kwargs) ->
|
|
92
|
+
**kwargs) -> EnvClass:
|
|
84
93
|
""" Parse CLI arguments, possibly merged from a config file.
|
|
85
94
|
|
|
86
|
-
:param
|
|
95
|
+
:param env_class: Class with the configuration.
|
|
87
96
|
:param config_file: File to load YAML to be merged with the configuration.
|
|
88
97
|
You do not have to re-define all the settings in the config file, you can choose a few.
|
|
89
98
|
:param **kwargs The same as for argparse.ArgumentParser.
|
|
@@ -94,20 +103,20 @@ class Mininterface(Generic[ConfigInstance]):
|
|
|
94
103
|
disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
|
|
95
104
|
# Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
|
|
96
105
|
for key in (key for key, val in disk.items() if isinstance(val, dict)):
|
|
97
|
-
disk[key] =
|
|
106
|
+
disk[key] = env_class.__annotations__[key](**disk[key])
|
|
98
107
|
# To ensure the configuration file does not need to contain all keys, we have to fill in the missing ones.
|
|
99
108
|
# Otherwise, tyro will spawn warnings about missing fields.
|
|
100
|
-
static = {key: getattr(
|
|
101
|
-
for key in
|
|
109
|
+
static = {key: getattr(env_class, key, MISSING)
|
|
110
|
+
for key in env_class.__annotations__ if not key.startswith("__") and not key in disk}
|
|
102
111
|
kwargs["default"] = SimpleNamespace(**(disk | static))
|
|
103
112
|
|
|
104
113
|
# Load configuration from CLI
|
|
105
|
-
parser: ArgumentParser = get_parser(
|
|
114
|
+
parser: ArgumentParser = get_parser(env_class, **kwargs)
|
|
106
115
|
self.descriptions = get_descriptions(parser)
|
|
107
|
-
# Why `or self.
|
|
108
|
-
# Still, we want to prevent error raised in `
|
|
109
|
-
self.
|
|
110
|
-
return self.
|
|
116
|
+
# Why `or self.env`? If Env is not a dataclass but a function, it has no attributes.
|
|
117
|
+
# Still, we want to prevent error raised in `ask_env()` if self.env would have been set to None.
|
|
118
|
+
self.env = get_env_allow_missing(env_class, kwargs, parser) or self.env
|
|
119
|
+
return self.env
|
|
111
120
|
|
|
112
121
|
def is_yes(self, text: str) -> bool:
|
|
113
122
|
""" Display confirm box, focusing yes. """
|
|
@@ -118,3 +127,18 @@ class Mininterface(Generic[ConfigInstance]):
|
|
|
118
127
|
""" Display confirm box, focusing no. """
|
|
119
128
|
print("Asking no:", text)
|
|
120
129
|
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
from abc import ABC, abstractmethod
|
|
133
|
+
class BackendAdaptor(ABC):
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def widgetize(ff: FormField):
|
|
138
|
+
""" Wrap FormField to a textual widget. """
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
def run_dialog(self, formDict: FormDict, title: str = "") -> FormDict:
|
|
143
|
+
""" Let the user edit the dict values. """
|
|
144
|
+
pass
|
|
@@ -52,7 +52,7 @@ class Redirectable:
|
|
|
52
52
|
# NOTE When used in the with statement, the TUI window should not vanish between dialogues.
|
|
53
53
|
# The same way the GUI does not vanish.
|
|
54
54
|
# NOTE: Current implementation will show only after a dialog submit, not continuously.
|
|
55
|
-
# # with run(
|
|
55
|
+
# # with run(Env) as m:
|
|
56
56
|
# print("First")
|
|
57
57
|
# sleep(1)
|
|
58
58
|
# print("Second")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pprint import pprint
|
|
2
2
|
|
|
3
|
-
from .FormDict import
|
|
3
|
+
from .FormDict import EnvClass, FormDict
|
|
4
4
|
from .Mininterface import Cancelled, Mininterface
|
|
5
5
|
|
|
6
6
|
|
|
@@ -20,13 +20,13 @@ class TextInterface(Mininterface):
|
|
|
20
20
|
raise Cancelled(".. cancelled")
|
|
21
21
|
return txt
|
|
22
22
|
|
|
23
|
-
def
|
|
23
|
+
def ask_env(self) -> EnvClass:
|
|
24
24
|
# NOTE: This is minimal implementation that should rather go the ReplInterface.
|
|
25
25
|
# I might build some menu of changing dict through:
|
|
26
|
-
# params_ = dataclass_to_dict(self.
|
|
26
|
+
# params_ = dataclass_to_dict(self.env, self.descriptions)
|
|
27
27
|
# data = FormDict → dict self.window.run_dialog(params_)
|
|
28
|
-
# dict_to_dataclass(self.
|
|
29
|
-
return self.form(self.
|
|
28
|
+
# dict_to_dataclass(self.env, params_)
|
|
29
|
+
return self.form(self.env)
|
|
30
30
|
|
|
31
31
|
def form(self, form: FormDict) -> dict:
|
|
32
32
|
# NOTE: This is minimal implementation that should rather go the ReplInterface.
|
|
@@ -13,16 +13,13 @@ except ImportError:
|
|
|
13
13
|
raise InterfaceNotAvailable
|
|
14
14
|
|
|
15
15
|
from .auxiliary import flatten
|
|
16
|
-
from .FormDict import (
|
|
17
|
-
dict_to_formdict)
|
|
16
|
+
from .FormDict import (EnvClass, FormDict, config_to_formdict,
|
|
17
|
+
dict_to_formdict, formdict_to_widgetdict)
|
|
18
18
|
from .FormField import FormField
|
|
19
|
-
from .Mininterface import Cancelled
|
|
19
|
+
from .Mininterface import BackendAdaptor, Cancelled
|
|
20
20
|
from .Redirectable import Redirectable
|
|
21
21
|
from .TextInterface import TextInterface
|
|
22
22
|
|
|
23
|
-
# TODO with statement hello world example image is wrong (Textual already redirects the output as GuiInterface does)
|
|
24
|
-
|
|
25
|
-
|
|
26
23
|
@dataclass
|
|
27
24
|
class DummyWrapper:
|
|
28
25
|
""" Value wrapped, since I do not know how to get it from textual app.
|
|
@@ -39,19 +36,23 @@ class TextualInterface(Redirectable, TextInterface):
|
|
|
39
36
|
def ask(self, text: str = None):
|
|
40
37
|
return self.form({text: ""})[text]
|
|
41
38
|
|
|
42
|
-
def
|
|
39
|
+
def ask_env(self) -> EnvClass:
|
|
43
40
|
""" Display a window form with all parameters. """
|
|
44
|
-
params_ = config_to_formdict(self.
|
|
41
|
+
params_ = config_to_formdict(self.env, self.descriptions)
|
|
45
42
|
|
|
46
43
|
# fetch the dict of dicts values from the form back to the namespace of the dataclasses
|
|
47
|
-
TextualApp.run_dialog(TextualApp(), params_)
|
|
48
|
-
return self.
|
|
44
|
+
TextualApp.run_dialog(TextualApp(self), params_)
|
|
45
|
+
return self.env
|
|
49
46
|
|
|
47
|
+
# NOTE: This works bad with lists. GuiInterface considers list as combobox,
|
|
48
|
+
# TextualInterface as str. We should decide what should happen. Is there a tyro default for list?
|
|
50
49
|
def form(self, form: FormDict, title: str = "") -> dict:
|
|
51
|
-
|
|
50
|
+
TextualApp.run_dialog(TextualApp(self), dict_to_formdict(form), title)
|
|
51
|
+
return form
|
|
52
52
|
|
|
53
53
|
# NOTE we should implement better, now the user does not know it needs an int
|
|
54
|
-
|
|
54
|
+
def ask_number(self, text):
|
|
55
|
+
return self.form({text: FormField("", "", int, text)})[text].processed_value
|
|
55
56
|
|
|
56
57
|
def is_yes(self, text):
|
|
57
58
|
return TextualButtonApp(self).yes_no(text, False).val
|
|
@@ -60,6 +61,7 @@ class TextualInterface(Redirectable, TextInterface):
|
|
|
60
61
|
return TextualButtonApp(self).yes_no(text, True).val
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
# NOTE: For a metaclass conflict I was not able to inherit from BackendAdaptor
|
|
63
65
|
class TextualApp(App[bool | None]):
|
|
64
66
|
|
|
65
67
|
BINDINGS = [
|
|
@@ -82,11 +84,11 @@ class TextualApp(App[bool | None]):
|
|
|
82
84
|
self.interface = interface
|
|
83
85
|
|
|
84
86
|
@staticmethod
|
|
85
|
-
def
|
|
87
|
+
def widgetize(ff: FormField) -> Checkbox | Input:
|
|
86
88
|
""" Wrap FormField to a textual widget. """
|
|
87
89
|
|
|
88
|
-
if ff.annotation is bool or not ff.annotation and ff.val
|
|
89
|
-
o = Checkbox(ff.name, ff.val)
|
|
90
|
+
if ff.annotation is bool or not ff.annotation and (ff.val is True or ff.val is False):
|
|
91
|
+
o = Checkbox(ff.name or "", ff.val)
|
|
90
92
|
else:
|
|
91
93
|
o = Input(str(ff.val), placeholder=ff.name or "")
|
|
92
94
|
o._link = ff # The Textual widgets need to get back to this value
|
|
@@ -99,7 +101,8 @@ class TextualApp(App[bool | None]):
|
|
|
99
101
|
window.title = title
|
|
100
102
|
|
|
101
103
|
# NOTE Sections (~ nested dicts) are not implemented, they flatten
|
|
102
|
-
|
|
104
|
+
# Maybe just 'flatten' might be removed.
|
|
105
|
+
widgets: list[Checkbox | Input] = [f for f in flatten(formdict_to_widgetdict(formDict, cls.widgetize))]
|
|
103
106
|
window.widgets = widgets
|
|
104
107
|
|
|
105
108
|
if not window.run():
|
|
@@ -107,7 +110,7 @@ class TextualApp(App[bool | None]):
|
|
|
107
110
|
|
|
108
111
|
# validate and store the UI value → FormField value → original value
|
|
109
112
|
if not FormField.submit_values((field._link, field.value) for field in widgets):
|
|
110
|
-
return cls.run_dialog(TextualApp(), formDict, title)
|
|
113
|
+
return cls.run_dialog(TextualApp(window.interface), formDict, title)
|
|
111
114
|
return formDict
|
|
112
115
|
|
|
113
116
|
def compose(self) -> ComposeResult:
|
|
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Type
|
|
|
4
4
|
from unittest.mock import patch
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
from .Mininterface import
|
|
7
|
+
from .Mininterface import EnvClass, Mininterface
|
|
8
8
|
from .TextInterface import ReplInterface, TextInterface
|
|
9
9
|
from .FormField import FormField
|
|
10
10
|
from .common import InterfaceNotAvailable
|
|
@@ -30,9 +30,10 @@ class TuiInterface(TextualInterface or TextInterface):
|
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def run(
|
|
33
|
+
def run(env_class: Type[EnvClass] | None = None,
|
|
34
34
|
interface: Type[Mininterface] = GuiInterface or TuiInterface,
|
|
35
|
-
|
|
35
|
+
config_file: Path|str = "",
|
|
36
|
+
**kwargs) -> Mininterface[EnvClass]:
|
|
36
37
|
"""
|
|
37
38
|
Main access.
|
|
38
39
|
Wrap your configuration dataclass into `run` to access the interface. An interface is chosen automatically,
|
|
@@ -48,37 +49,47 @@ def run(config: Type[ConfigInstance] | None = None,
|
|
|
48
49
|
:return: Interface used.
|
|
49
50
|
|
|
50
51
|
Undocumented: The `config` may be function as well. We invoke its parameters.
|
|
51
|
-
However, as Mininterface.
|
|
52
|
-
methods like `Mininterface.
|
|
52
|
+
However, as Mininterface.env stores the output of the function instead of the Argparse namespace,
|
|
53
|
+
methods like `Mininterface.ask_env()` will work unpredictibly.
|
|
53
54
|
Also, the config file seems to be fetched only for positional (missing) parameters,
|
|
54
55
|
and ignored for keyword (filled) parameters.
|
|
55
56
|
It seems to be this is the tyro's deal and hence it might start working any time.
|
|
56
57
|
If not, we might help it this way:
|
|
57
58
|
`if isinstance(config, FunctionType): config = lambda: config(**kwargs["default"])`
|
|
58
59
|
"""
|
|
60
|
+
|
|
61
|
+
# Prepare the config file
|
|
62
|
+
if not config_file and not kwargs.get("default") and env_class:
|
|
63
|
+
cf = Path(sys.argv[0]).with_suffix(".yaml")
|
|
64
|
+
if cf.exists():
|
|
65
|
+
config_file = cf
|
|
66
|
+
|
|
67
|
+
|
|
59
68
|
# Build the interface
|
|
60
|
-
prog = kwargs.get("prog") or sys.argv[0]
|
|
69
|
+
prog = kwargs.get("prog") or Path(sys.argv[0]).name
|
|
61
70
|
try:
|
|
62
|
-
interface = interface(prog)
|
|
71
|
+
interface = interface(prog, env_class, config_file, **kwargs)
|
|
63
72
|
except InterfaceNotAvailable: # Fallback to a different interface
|
|
64
|
-
interface = TuiInterface(prog)
|
|
73
|
+
interface = TuiInterface(prog, env_class, config_file, **kwargs)
|
|
65
74
|
|
|
66
75
|
# Load configuration from CLI and a config file
|
|
67
|
-
if
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
# if env_class:
|
|
77
|
+
# cf = Path(sys.argv[0]).with_suffix(".yaml")
|
|
78
|
+
# interface._parse_env(env_class, cf if cf.exists() and not kwargs.get("default") else None, **kwargs)
|
|
70
79
|
|
|
71
80
|
# NOTE draft – move the functionality inside Mininterface?
|
|
72
81
|
# What will be the most used params?
|
|
73
|
-
# run(config: Type[
|
|
82
|
+
# run(config: Type[EnvInstance],
|
|
74
83
|
# prog="merge to kwargs later",
|
|
75
84
|
# config_file:Path|str="",
|
|
76
85
|
# interface: Type[Mininterface] = GuiInterface or TuiInterface,
|
|
77
86
|
# **kwargs)
|
|
78
87
|
# title = prog or sys.argv
|
|
79
|
-
# Mininterface(title,
|
|
88
|
+
# Mininterface(title, env_class, config_file, **kwargs)
|
|
80
89
|
|
|
81
90
|
return interface
|
|
82
91
|
|
|
83
92
|
|
|
84
|
-
__all__ = ["run", "FormField"
|
|
93
|
+
__all__ = ["run", "FormField", "InterfaceNotAvailable",
|
|
94
|
+
"Mininterface", "GuiInterface", "TuiInterface", "TextInterface", "TextualInterface"
|
|
95
|
+
]
|
|
@@ -13,9 +13,9 @@ class CliInteface:
|
|
|
13
13
|
ask_number: str = ""
|
|
14
14
|
""" Prompt the user to input a number. Empty input = 0. """
|
|
15
15
|
is_yes: str = ""
|
|
16
|
-
""" Display confirm box, focusing yes. """
|
|
16
|
+
""" Display confirm box, focusing 'yes'. """
|
|
17
17
|
is_no: str = ""
|
|
18
|
-
""" Display confirm box, focusing no. """
|
|
18
|
+
""" Display confirm box, focusing 'no'. """
|
|
19
19
|
|
|
20
20
|
# TODO does not work in REPL interface: mininterface --alert hello
|
|
21
21
|
def main():
|
|
@@ -23,7 +23,7 @@ def main():
|
|
|
23
23
|
# TODO It DOES make sense. Change in README. It s a good fallback.
|
|
24
24
|
result = []
|
|
25
25
|
with run(CliInteface, prog="Mininterface", description=__doc__) as m:
|
|
26
|
-
for method, label in vars(m.
|
|
26
|
+
for method, label in vars(m.env).items():
|
|
27
27
|
if label:
|
|
28
28
|
result.append(getattr(m, method)(label))
|
|
29
29
|
# Displays each result on a new line. Currently, this is an undocumented feature.
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "mininterface"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "A minimal access to GUI, TUI, CLI and config"
|
|
9
9
|
authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
File without changes
|
|
File without changes
|