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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mininterface
3
- Version: 0.4.4rc3
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 Config:
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
- 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"
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(Config) as m:
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
  ![Small window with the text 'Your important number'](https://github.com/CZ-NIC/mininterface/blob/main/asset/hello-with-statement.webp?raw=True "With statement to redirect the output")
83
- ![The same in terminal'](https://github.com/CZ-NIC/mininterface/blob/main/asset/hello-with-statement-tui.webp?raw=True "With statement in TUI fallback")
83
+ ![The same in terminal'](https://github.com/CZ-NIC/mininterface/blob/main/asset/hello-with-statement-tui.avif?raw=True "With statement in TUI fallback")
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
- + [`ask_args() -> ConfigInstance`](#ask_args--configinstance)
96
+ + [`ask_env() -> EnvInstance`](#ask_env--configinstance)
97
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)
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
- + [`parse_args(config: Type[ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ConfigInstance`](#parse_argsconfig-type-configinstance-config_file-pathlibpath--none--none-kwargs---configinstance)
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. `mininterface` is here to help.
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 `args.test` be set to `True`.
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
- ```python3
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[ConfigInstance]`: Dataclass with the configuration.
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 `parse_args` if needed.
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
- ### `ask_args() -> ConfigInstance`
198
- Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_args.)
199
- ### `form(args: FormDict, title="") -> dict`
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
- * `args`: Dict of `{labels: default value}`. The form widget infers from the default value type.
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
- ### `ask_number(text: str) -> int`
207
- Prompt the user to input a number. Empty input = 0.
208
- ### `get_args(ask_on_empty_cli=True) -> ConfigInstance`
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
- ### `parse_args(config: Type[ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~ConfigInstance`
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: `ConfigInstance` Configuration namespace.
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 Config:
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
- 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"
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(Config) as m:
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
  ![Small window with the text 'Your important number'](https://github.com/CZ-NIC/mininterface/blob/main/asset/hello-with-statement.webp?raw=True "With statement to redirect the output")
60
- ![The same in terminal'](https://github.com/CZ-NIC/mininterface/blob/main/asset/hello-with-statement-tui.webp?raw=True "With statement in TUI fallback")
60
+ ![The same in terminal'](https://github.com/CZ-NIC/mininterface/blob/main/asset/hello-with-statement-tui.avif?raw=True "With statement in TUI fallback")
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
- + [`ask_args() -> ConfigInstance`](#ask_args--configinstance)
73
+ + [`ask_env() -> EnvInstance`](#ask_env--configinstance)
74
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)
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
- + [`parse_args(config: Type[ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ConfigInstance`](#parse_argsconfig-type-configinstance-config_file-pathlibpath--none--none-kwargs---configinstance)
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. `mininterface` is here to help.
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 `args.test` be set to `True`.
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
- ```python3
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[ConfigInstance]`: Dataclass with the configuration.
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 `parse_args` if needed.
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
- ### `ask_args() -> ConfigInstance`
175
- Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_args.)
176
- ### `form(args: FormDict, title="") -> dict`
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
- * `args`: Dict of `{labels: default value}`. The form widget infers from the default value type.
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
- ### `ask_number(text: str) -> int`
184
- Prompt the user to input a number. Empty input = 0.
185
- ### `get_args(ask_on_empty_cli=True) -> ConfigInstance`
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
- ### `parse_args(config: Type[ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~ConfigInstance`
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: `ConfigInstance` Configuration namespace.
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
- ConfigInstance = TypeVar("ConfigInstance")
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, src=(data, 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 config_to_formdict(args: ConfigInstance, descr: dict, _path="") -> FormDict:
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(args).items():
55
+ for param, val in vars(env).items():
48
56
  annotation = None
49
57
  if val is None:
50
- wanted_type = get_type_hints(args.__class__).get(param)
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, src2=(args, 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, src2=(args, 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 get_args_allow_missing(config: Type[ConfigInstance], kwargs: dict, parser: ArgumentParser) -> ConfigInstance:
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 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,
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
- args = None
106
+ env = None
99
107
  else:
100
- args = []
108
+ env = []
101
109
  try:
102
110
  with patch.object(TyroArgumentParser, 'error', custom_error):
103
- return cli(config, args=args, **kwargs)
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(Value):
29
- """ Bridge between the input values and a UI widget.
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
- name: str | None = None # NOTE: Only TextualInterface uses this by now.
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
- src: tuple[TD, TK] | None = None
41
- """ The original dict to be updated when UI ends. """
42
- src2: tuple[TD, TK] | None = None
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.src:
86
- d, k = self.src
115
+ if self.src_dict:
116
+ d, k = self.src_dict
87
117
  d[k] = fixed_value
88
- elif self.src2:
89
- d, k = self.src2
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, ConfigInstance, Mininterface
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.window.run_dialog({text: ""})[text]
38
+ return self.form({text: ""})[text]
39
39
 
40
- def ask_args(self) -> ConfigInstance:
40
+ def ask_env(self) -> EnvClass:
41
41
  """ Display a window form with all parameters. """
42
- formDict = config_to_formdict(self.args, self.descriptions)
42
+ formDict = config_to_formdict(self.env, self.descriptions)
43
43
 
44
- # formDict automatically fetches the edited values back to the ConfigInstance
44
+ # formDict automatically fetches the edited values back to the EnvInstance
45
45
  self.window.run_dialog(formDict)
46
- return self.args
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 args: Dict of `{labels: default value}`. The form widget infers from the default value type.
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
- return self.window.run_dialog(form, title=title)
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.window.run_dialog({text: 0})[text]
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
- name_form="",
98
- form_dict=formDict,
99
- name_config="Ok",
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
- button.bind("<Return>", lambda _: button.invoke())
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
- from .FormDict import ConfigClass, ConfigInstance, FormDict, get_args_allow_missing
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[ConfigInstance]):
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.args: ConfigInstance = SimpleNamespace()
31
- """ Parsed arguments, fetched from cli by parse.args """
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 ask_args(self) -> ConfigInstance:
54
- """ Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_args.) """
55
- print("Asking the args", self.args)
56
- return self.args
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, data: FormDict, title: str = "") -> dict:
72
+ def form(self, form: FormDict, title: str = "") -> dict:
64
73
  """ Prompt the user to fill up whole form.
65
- :param args: Dict of `{labels: default value}`. The form widget infers from the default value type.
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}", data)
71
- return data # NOTE – this should return dict, not FormDict (get rid of auxiliary.FormField values)
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 get_args(self, ask_on_empty_cli=True) -> ConfigInstance:
74
- """ Returns whole configuration (previously fetched from CLI and config file by parse_args).
75
- If program was launched with no arguments (empty CLI), invokes self.ask_args() to edit the fields. """
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.ask_args()
79
- return self.args
87
+ return self.ask_env()
88
+ return self.env
80
89
 
81
- def parse_args(self, config: Type[ConfigInstance],
90
+ def _parse_env(self, env_class: Type[EnvClass],
82
91
  config_file: Path | None = None,
83
- **kwargs) -> ConfigInstance:
92
+ **kwargs) -> EnvClass:
84
93
  """ Parse CLI arguments, possibly merged from a config file.
85
94
 
86
- :param config: Class with the configuration.
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] = config.__annotations__[key](**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(config, key, MISSING)
101
- for key in config.__annotations__ if not key.startswith("__") and not key in disk}
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(config, **kwargs)
114
+ parser: ArgumentParser = get_parser(env_class, **kwargs)
106
115
  self.descriptions = get_descriptions(parser)
107
- # Why `or self.args`? If Config is not a dataclass but a function, it has no attributes.
108
- # Still, we want to prevent error raised in `ask_args()` if self.args would have been set to None.
109
- self.args = get_args_allow_missing(config, kwargs, parser) or self.args
110
- return self.args
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(Config) as m:
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 ConfigInstance, FormDict
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 ask_args(self) -> ConfigInstance:
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.args, self.descriptions)
26
+ # params_ = dataclass_to_dict(self.env, self.descriptions)
27
27
  # data = FormDict → dict self.window.run_dialog(params_)
28
- # dict_to_dataclass(self.args, params_)
29
- return self.form(self.args)
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 (ConfigInstance, FormDict, config_to_formdict,
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 ask_args(self) -> ConfigInstance:
39
+ def ask_env(self) -> EnvClass:
43
40
  """ Display a window form with all parameters. """
44
- params_ = config_to_formdict(self.args, self.descriptions)
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.args
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
- return TextualApp.run_dialog(TextualApp(), dict_to_formdict(form), title)
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
- # def ask_number(self, text):
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 get_widget(ff: FormField) -> Checkbox | Input:
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 in [True, False]:
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
- widgets: list[Checkbox | Input] = [cls.get_widget(f) for f in flatten(formDict)]
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 ConfigInstance, Mininterface
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(config: Type[ConfigInstance] | None = None,
33
+ def run(env_class: Type[EnvClass] | None = None,
34
34
  interface: Type[Mininterface] = GuiInterface or TuiInterface,
35
- **kwargs) -> Mininterface[ConfigInstance]:
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.args stores the output of the function instead of the Argparse namespace,
52
- methods like `Mininterface.ask_args()` will work unpredictibly.
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 config:
68
- cf = Path(sys.argv[0]).with_suffix(".yaml")
69
- interface.parse_args(config, cf if cf.exists() and not kwargs.get("default") else None, **kwargs)
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[ConfigInstance],
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, configClass, configFile, **kwargs)
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.args).items():
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.4.4rc3"
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"