mininterface 1.1.1__tar.gz → 1.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mininterface-1.1.1 → mininterface-1.1.2}/PKG-INFO +2 -2
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/__main__.py +1 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/cli_utils.py +19 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/dataclass_creation.py +22 -9
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_mininterface/__init__.py +18 -3
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_mininterface/adaptor.py +15 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_mininterface/mixin.py +6 -6
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_text_interface/__init__.py +10 -6
- mininterface-1.1.2/mininterface/_text_interface/timeout.py +126 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/adaptor.py +6 -5
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/button_contents.py +3 -3
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/textual_app.py +13 -10
- mininterface-1.1.2/mininterface/_textual_interface/timeout.py +35 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/widgets.py +8 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/adaptor.py +15 -6
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/select_input.py +1 -1
- mininterface-1.1.2/mininterface/_tk_interface/timeout.py +38 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_web_interface/child_adaptor.py +6 -1
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/flag.py +1 -1
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/select_tag.py +11 -1
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/tag.py +23 -3
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/tag_factory.py +9 -1
- {mininterface-1.1.1 → mininterface-1.1.2}/pyproject.toml +2 -2
- {mininterface-1.1.1 → mininterface-1.1.2}/LICENSE +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/README.md +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/argparse_support.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/auxiliary.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/cli_flags.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/cli_parser.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/config_file.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/form_dict.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/future_compatibility.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/redirectable.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/run.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/shortcuts.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/showcase.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/start.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_lib/tyro_patches.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_text_interface/adaptor.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/facet.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/file_picker_input.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/form_contents.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/style.tcss +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/date_entry.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/external_fix.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/facet.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/secret_entry.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/utils.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_web_interface/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_web_interface/app.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_web_interface/parent_adaptor.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/cli.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/exceptions.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/experimental.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/facet/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/interfaces.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/settings.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/alias.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/callback_tag.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/datetime_tag.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/internal.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/path_tag.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/secret_tag.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/tag/type_stubs.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.2}/mininterface/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: A minimal access to GUI, TUI, CLI and config
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
License-File: LICENSE
|
|
@@ -34,7 +34,7 @@ Requires-Dist: tkcalendar ; extra == "gui" or extra == "ui" or extra == "all"
|
|
|
34
34
|
Requires-Dist: tkinter-tooltip ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
|
|
35
35
|
Requires-Dist: tkinter_form (==0.2.1) ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
|
|
36
36
|
Requires-Dist: tkscrollableframe ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
|
|
37
|
-
Requires-Dist: tyro (>=0.9,<0.10) ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
|
|
37
|
+
Requires-Dist: tyro (>=0.9.31,<0.10.0) ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
|
|
38
38
|
Project-URL: Homepage, https://github.com/CZ-NIC/mininterface
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
|
|
@@ -150,3 +150,22 @@ class SubcommandPlaceholder(Command):
|
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
SubcommandPlaceholder.__name__ = "subcommand" # show just the shortcut in the CLI
|
|
153
|
+
|
|
154
|
+
# NOTE I'd like the method run to actually run here
|
|
155
|
+
# @dataclass
|
|
156
|
+
# class Console(Command):
|
|
157
|
+
# foo: str = "bar"
|
|
158
|
+
# def run(self):
|
|
159
|
+
# raise ValueError("DVA")
|
|
160
|
+
# self.interface.alert("Console!")
|
|
161
|
+
# @dataclass
|
|
162
|
+
# class Message(Command):
|
|
163
|
+
# text: str
|
|
164
|
+
# def run(self):
|
|
165
|
+
# raise ValueError("RAZ")
|
|
166
|
+
# @dataclass
|
|
167
|
+
# class Env:
|
|
168
|
+
# val: Message | Console
|
|
169
|
+
# m = run(Env) # here
|
|
170
|
+
# m = run([Message, Console]) # and here too
|
|
171
|
+
# Then, add is as a tip to Supported-types.md.
|
|
@@ -108,7 +108,7 @@ def _unwrap_annotated(tp):
|
|
|
108
108
|
return tp
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
def create_with_missing(env: T, disk: dict, wf: Optional[dict] = None,
|
|
111
|
+
def create_with_missing(env: T, disk: dict, wf: Optional[dict] = None, m: Optional["Mininterface"] = None)->T:
|
|
112
112
|
"""
|
|
113
113
|
Create a default instance of an Env object. This is due to provent tyro to spawn warnings about missing fields.
|
|
114
114
|
Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
|
|
@@ -116,6 +116,8 @@ def create_with_missing(env: T, disk: dict, wf: Optional[dict] = None, mint: Opt
|
|
|
116
116
|
The result contains MISSING_NONPROP on the places the original Env object must have a value.
|
|
117
117
|
|
|
118
118
|
And such fields are put into the wf (wrong_fields) dict.
|
|
119
|
+
|
|
120
|
+
Having the `m` defined means we build the dataclass. None means we are still in the config file parsing context.
|
|
119
121
|
"""
|
|
120
122
|
# NOTE a test is missing
|
|
121
123
|
# @dataclass
|
|
@@ -146,7 +148,7 @@ def create_with_missing(env: T, disk: dict, wf: Optional[dict] = None, mint: Opt
|
|
|
146
148
|
# as the default value takes the precedence over the hard coded one, even if missing.
|
|
147
149
|
out = {}
|
|
148
150
|
missings: list[Tag] = []
|
|
149
|
-
for name, v in proc_method(env, disk, wf,
|
|
151
|
+
for name, v in proc_method(env, disk, wf, m):
|
|
150
152
|
out[name] = v
|
|
151
153
|
if v == MISSING_NONPROP and wf is not None:
|
|
152
154
|
# For building config file, the MISSING_NONPROP is alright as we expect tyro to fail
|
|
@@ -174,7 +176,7 @@ def _process_pydantic(env, disk, wf: Optional[dict], m: Optional["Mininterface"]
|
|
|
174
176
|
elif f.default is not None:
|
|
175
177
|
v = f.default
|
|
176
178
|
else:
|
|
177
|
-
v = _process_field(name, f.annotation,
|
|
179
|
+
v = _process_field(name, f.annotation, MISSING_NONPROP, wf, m, default_value=MISSING)
|
|
178
180
|
yield name, v
|
|
179
181
|
|
|
180
182
|
|
|
@@ -187,7 +189,7 @@ def _process_attr(env, disk, wf: Optional[dict], m: Optional["Mininterface"] = N
|
|
|
187
189
|
elif has_default:
|
|
188
190
|
v = f.default
|
|
189
191
|
else:
|
|
190
|
-
v = _process_field(f.name, f.type,
|
|
192
|
+
v = _process_field(f.name, f.type, MISSING_NONPROP, wf, m, default_val)
|
|
191
193
|
yield f.name, v
|
|
192
194
|
|
|
193
195
|
|
|
@@ -243,16 +245,27 @@ def _process_field(fname, ftype, disk_value, wf, m, default_value=MISSING):
|
|
|
243
245
|
else:
|
|
244
246
|
raise ValueError(f"Type {disk_value} not found in {ftype}")
|
|
245
247
|
|
|
246
|
-
if disk_value is not
|
|
248
|
+
if disk_value is not MISSING_NONPROP:
|
|
247
249
|
if _is_struct_type(ftype):
|
|
248
250
|
return _init_struct_value(ftype, disk_value, wf, fname, m)
|
|
251
|
+
# NOTE for handling subcommands in config file, I'll need something like this,
|
|
252
|
+
# combined with a structure like ChosenSubcommand, possibly inheriting from MISSING_NONPROP
|
|
253
|
+
# to be ignored by tyro, yet fetchable by cli_parser
|
|
254
|
+
# elif _is_subcommands:
|
|
255
|
+
# return {cl.__name__: _init_struct_value(cl, {}, wf, fname, m) for cl in get_args(ftype)}
|
|
249
256
|
return coerce_type_to_annotation(disk_value, ftype)
|
|
250
257
|
|
|
258
|
+
if _is_struct_type(ftype):
|
|
259
|
+
# Ex. `foo: Subcommand`
|
|
260
|
+
return _init_struct_value(ftype, {}, wf, fname, m)
|
|
261
|
+
|
|
251
262
|
# We must handle the case when there are multiple subcommands possible.
|
|
252
263
|
# The user decides now which way to go (choose a subcommand).
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
264
|
+
# Ex. `foo: Subcommand1 | Subcommand2`
|
|
265
|
+
_is_subcommands = (origin is Union or origin is UnionType) and all(_is_struct_type(cl) for cl in get_args(ftype))
|
|
266
|
+
if _is_subcommands:
|
|
267
|
+
if not m: # we are parsing the config file. The fields are not defined in the config file
|
|
268
|
+
return MISSING_NONPROP
|
|
256
269
|
ftype = choose_subcommand(get_args(ftype), m)
|
|
257
270
|
return _init_struct_value(ftype, {}, wf, fname, m)
|
|
258
271
|
|
|
@@ -271,7 +284,7 @@ def _process_dataclass(env, disk, wf: Optional[dict], m: Optional["Mininterface"
|
|
|
271
284
|
elif f.default is not MISSING:
|
|
272
285
|
v = f.default
|
|
273
286
|
else:
|
|
274
|
-
v = _process_field(f.name, f.type,
|
|
287
|
+
v = _process_field(f.name, f.type, MISSING_NONPROP, wf, m)
|
|
275
288
|
yield f.name, v
|
|
276
289
|
|
|
277
290
|
|
|
@@ -164,8 +164,13 @@ class Mininterface(Generic[EnvClass]):
|
|
|
164
164
|
def __exit__(self, *_):
|
|
165
165
|
pass
|
|
166
166
|
|
|
167
|
-
def alert(self, text: str) -> None:
|
|
168
|
-
"""Prompt the user to confirm the text.
|
|
167
|
+
def alert(self, text: str, *, timeout: int = 0) -> None:
|
|
168
|
+
"""Prompt the user to confirm the text.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
text: Displayed text
|
|
172
|
+
timeout: Auto-confirm after N seconds (0 = disabled).
|
|
173
|
+
"""
|
|
169
174
|
print("Alert text", text)
|
|
170
175
|
return
|
|
171
176
|
|
|
@@ -255,7 +260,7 @@ class Mininterface(Generic[EnvClass]):
|
|
|
255
260
|
# Output:
|
|
256
261
|
return assure_tag(annotation, validation)._make_default_value()
|
|
257
262
|
|
|
258
|
-
def confirm(self, text: str, default: bool = True) -> bool:
|
|
263
|
+
def confirm(self, text: str, default: bool = True, *, timeout: int = 0) -> bool:
|
|
259
264
|
"""Display confirm box and returns bool.
|
|
260
265
|
|
|
261
266
|
```python
|
|
@@ -265,9 +270,19 @@ class Mininterface(Generic[EnvClass]):
|
|
|
265
270
|
|
|
266
271
|

|
|
267
272
|
|
|
273
|
+
Automatically submit after 10 seconds with the `timeout` parameter.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
m = run()
|
|
277
|
+
print(m.confirm("Is that alright?"), timeout=10) # True/False
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+

|
|
281
|
+
|
|
268
282
|
Args:
|
|
269
283
|
text: Displayed text.
|
|
270
284
|
default: Focus the button with this value.
|
|
285
|
+
timeout: Auto-confirm after N seconds (0 = disabled).
|
|
271
286
|
|
|
272
287
|
Returns:
|
|
273
288
|
bool: Whether the user has chosen the Yes button.
|
|
@@ -140,3 +140,18 @@ class MinAdaptor(BackendAdaptor):
|
|
|
140
140
|
raise SystemExit(validation_fails)
|
|
141
141
|
|
|
142
142
|
return form
|
|
143
|
+
|
|
144
|
+
class Timeout(ABC):
|
|
145
|
+
""" Auto-submit dialogs """
|
|
146
|
+
|
|
147
|
+
def __init__(self, timeout: int, adaptor: "BackendAdaptor"):
|
|
148
|
+
self.timeout = timeout
|
|
149
|
+
self.adaptor = adaptor
|
|
150
|
+
|
|
151
|
+
@abstractmethod
|
|
152
|
+
def countdown(self):
|
|
153
|
+
...
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def cancel(self):
|
|
157
|
+
...
|
|
@@ -13,12 +13,12 @@ from . import Mininterface
|
|
|
13
13
|
class RichUiMixin(Mininterface):
|
|
14
14
|
_adaptor: "RichUiAdaptor"
|
|
15
15
|
|
|
16
|
-
def alert(self, text: str) -> None:
|
|
16
|
+
def alert(self, text: str, *, timeout: int = 0) -> None:
|
|
17
17
|
"""Display the OK dialog with text."""
|
|
18
|
-
self._adaptor.buttons(text, [("Ok", None)])
|
|
18
|
+
self._adaptor.buttons(text, [("Ok", None)], timeout=timeout)
|
|
19
19
|
|
|
20
|
-
def confirm(self, text, default: bool = True) -> bool:
|
|
21
|
-
return self._adaptor.yes_no(text, not default)
|
|
20
|
+
def confirm(self, text, default: bool = True, *, timeout: int = 0) -> bool:
|
|
21
|
+
return self._adaptor.yes_no(text, not default, timeout=timeout)
|
|
22
22
|
|
|
23
23
|
def ask(
|
|
24
24
|
self,
|
|
@@ -36,7 +36,7 @@ class RichUiMixin(Mininterface):
|
|
|
36
36
|
|
|
37
37
|
class RichUiAdaptor(ABC):
|
|
38
38
|
@abstractmethod
|
|
39
|
-
def yes_no(self, text: str, focus_no=True): ...
|
|
39
|
+
def yes_no(self, text: str, focus_no=True, *, timeout: int = 0)->bool: ...
|
|
40
40
|
|
|
41
41
|
@abstractmethod
|
|
42
|
-
def buttons(self, text: str, buttons: list[tuple[str, Any]], focused: int = 1): ...
|
|
42
|
+
def buttons(self, text: str, buttons: list[tuple[str, Any]], focused: int = 1, *, timeout: int = 0): ...
|
|
@@ -2,12 +2,14 @@ import sys
|
|
|
2
2
|
from pprint import pprint
|
|
3
3
|
from typing import TYPE_CHECKING, Iterable, Type, TypeVar
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
from ..tag.tag_factory import assure_tag
|
|
6
7
|
|
|
7
8
|
from ..exceptions import Cancelled, InterfaceNotAvailable
|
|
8
9
|
from .._lib.form_dict import DataClass, EnvClass, FormDict, tag_assure_type
|
|
9
10
|
from .._mininterface import Mininterface
|
|
10
11
|
from ..tag.tag import Tag, TagValue, ValidationCallback
|
|
12
|
+
from .timeout import input_timeout
|
|
11
13
|
from .adaptor import TextAdaptor
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self`
|
|
@@ -75,23 +77,25 @@ class TextInterface(AssureInteractiveTerminal, Mininterface):
|
|
|
75
77
|
|
|
76
78
|
_adaptor: TextAdaptor
|
|
77
79
|
|
|
78
|
-
def alert(self, text: str):
|
|
80
|
+
def alert(self, text: str, *, timeout: int = 0):
|
|
79
81
|
"""Display text and let the user hit any key."""
|
|
80
82
|
with StdinTTYWrapper():
|
|
81
|
-
|
|
83
|
+
input_timeout(text + " Hit any key.", timeout, True)
|
|
82
84
|
|
|
83
85
|
def ask(
|
|
84
86
|
self,
|
|
85
87
|
text: str,
|
|
86
88
|
annotation: Type[TagValue] | Tag = str,
|
|
87
89
|
validation: Iterable[ValidationCallback] | ValidationCallback | None = None,
|
|
90
|
+
*,
|
|
91
|
+
_timeout: int = 0
|
|
88
92
|
) -> TagValue:
|
|
89
93
|
with StdinTTYWrapper():
|
|
90
94
|
if not self.interactive:
|
|
91
95
|
return super().ask(text, annotation=annotation, validation=validation)
|
|
92
96
|
while True:
|
|
93
97
|
try:
|
|
94
|
-
txt =
|
|
98
|
+
txt = input_timeout(text + ":" if text else "", _timeout)
|
|
95
99
|
except EOFError:
|
|
96
100
|
raise Cancelled(".. cancelled")
|
|
97
101
|
t = assure_tag(annotation, validation)
|
|
@@ -133,20 +137,20 @@ class TextInterface(AssureInteractiveTerminal, Mininterface):
|
|
|
133
137
|
pdb.set_trace()
|
|
134
138
|
return form
|
|
135
139
|
|
|
136
|
-
def confirm(self, text: str, default: bool = True):
|
|
140
|
+
def confirm(self, text: str, default: bool = True, *, timeout: int = 0):
|
|
137
141
|
with StdinTTYWrapper():
|
|
138
142
|
if default:
|
|
139
143
|
t = text + " [y]/n"
|
|
140
144
|
else:
|
|
141
145
|
t = text + " y/[n]"
|
|
142
|
-
val = self.ask(text=t).lower()
|
|
146
|
+
val = self.ask(text=t, _timeout=timeout).lower()
|
|
143
147
|
if not val:
|
|
144
148
|
return bool(default)
|
|
145
149
|
if val in ("y", "yes", ""):
|
|
146
150
|
return True
|
|
147
151
|
if val in ("n", "no", ""):
|
|
148
152
|
return False
|
|
149
|
-
return self.confirm(text, default)
|
|
153
|
+
return self.confirm(text, default, timeout=timeout)
|
|
150
154
|
|
|
151
155
|
|
|
152
156
|
class ReplInterface(TextInterface):
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# if not timeout:
|
|
2
|
+
# return input(prompt)
|
|
3
|
+
|
|
4
|
+
# print(f"{prompt} (countdown {int(timeout)} sec)", end='', flush=True)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from io import UnsupportedOperation
|
|
11
|
+
from ..exceptions import Cancelled
|
|
12
|
+
|
|
13
|
+
if sys.platform == "win32":
|
|
14
|
+
import msvcrt
|
|
15
|
+
else:
|
|
16
|
+
import select
|
|
17
|
+
import tty
|
|
18
|
+
import termios
|
|
19
|
+
|
|
20
|
+
def input_timeout(prompt: str, timeout: int = 0, exit_on_keypress: bool = False) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Cross-platform input with timeout and visual dots.
|
|
23
|
+
|
|
24
|
+
- Shows dots every second until user starts typing.
|
|
25
|
+
- exit_on_keypress=True: returns immediately after any key (prints newline immediately).
|
|
26
|
+
- exit_on_keypress=False: disables timeout after first key, waits for Enter (newline printed at Enter).
|
|
27
|
+
- Returns user input string or empty string if timeout expires.
|
|
28
|
+
- Ctrl+C raises Cancelled.
|
|
29
|
+
"""
|
|
30
|
+
input_started = threading.Event()
|
|
31
|
+
inp = []
|
|
32
|
+
|
|
33
|
+
# Thread to print dots
|
|
34
|
+
def dots_thread():
|
|
35
|
+
for _ in range(timeout):
|
|
36
|
+
if input_started.is_set():
|
|
37
|
+
break
|
|
38
|
+
time.sleep(1)
|
|
39
|
+
if not input_started.is_set():
|
|
40
|
+
print(".", end='', flush=True)
|
|
41
|
+
|
|
42
|
+
if timeout:
|
|
43
|
+
print(f"{prompt} (countdown {int(timeout)} sec)", end='', flush=True)
|
|
44
|
+
t_dots = threading.Thread(target=dots_thread, daemon=True)
|
|
45
|
+
t_dots.start()
|
|
46
|
+
timeout_running = True
|
|
47
|
+
else:
|
|
48
|
+
print(prompt + " ", end='', flush=True)
|
|
49
|
+
timeout_running = False
|
|
50
|
+
|
|
51
|
+
start_time = time.time()
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
if sys.platform == "win32":
|
|
55
|
+
while True:
|
|
56
|
+
if msvcrt.kbhit():
|
|
57
|
+
char = msvcrt.getwch()
|
|
58
|
+
if char == '\r':
|
|
59
|
+
print() # newline at Enter
|
|
60
|
+
return "".join(inp)
|
|
61
|
+
elif char == '\x03': # Ctrl+C
|
|
62
|
+
raise Cancelled
|
|
63
|
+
elif char == '\x08': # Backspace
|
|
64
|
+
if inp:
|
|
65
|
+
inp.pop()
|
|
66
|
+
print("\b \b", end='', flush=True)
|
|
67
|
+
continue
|
|
68
|
+
else:
|
|
69
|
+
inp.append(char)
|
|
70
|
+
print(char, end='', flush=True)
|
|
71
|
+
|
|
72
|
+
input_started.set()
|
|
73
|
+
if exit_on_keypress:
|
|
74
|
+
print() # newline immediately
|
|
75
|
+
return "".join(inp)
|
|
76
|
+
|
|
77
|
+
# Stop if timeout exceeded and input not started
|
|
78
|
+
if timeout_running and (time.time() - start_time >= timeout) and not input_started.is_set():
|
|
79
|
+
input_started.set()
|
|
80
|
+
print()
|
|
81
|
+
return ""
|
|
82
|
+
time.sleep(0.01)
|
|
83
|
+
|
|
84
|
+
else:
|
|
85
|
+
fd = sys.stdin.fileno()
|
|
86
|
+
old_settings = termios.tcgetattr(fd)
|
|
87
|
+
tty.setcbreak(fd)
|
|
88
|
+
try:
|
|
89
|
+
while True:
|
|
90
|
+
rlist, _, _ = select.select([sys.stdin], [], [], 0.1)
|
|
91
|
+
if rlist:
|
|
92
|
+
char = sys.stdin.read(1)
|
|
93
|
+
if char == '\n':
|
|
94
|
+
print() # newline at Enter
|
|
95
|
+
return "".join(inp)
|
|
96
|
+
elif char == '\x03':
|
|
97
|
+
raise Cancelled
|
|
98
|
+
elif char == '\x7f': # Backspace
|
|
99
|
+
if inp:
|
|
100
|
+
inp.pop()
|
|
101
|
+
print("\b \b", end='', flush=True)
|
|
102
|
+
continue
|
|
103
|
+
else:
|
|
104
|
+
inp.append(char)
|
|
105
|
+
print(char, end='', flush=True)
|
|
106
|
+
|
|
107
|
+
input_started.set()
|
|
108
|
+
if exit_on_keypress:
|
|
109
|
+
print() # newline immediately
|
|
110
|
+
return "".join(inp)
|
|
111
|
+
|
|
112
|
+
# Stop if timeout exceeded and input not started
|
|
113
|
+
if timeout_running and (time.time() - start_time >= timeout) and not input_started.is_set():
|
|
114
|
+
input_started.set()
|
|
115
|
+
print()
|
|
116
|
+
return ""
|
|
117
|
+
finally:
|
|
118
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
119
|
+
except Exception:
|
|
120
|
+
# Fallback to class input.
|
|
121
|
+
# 'Press any key' will not work.
|
|
122
|
+
# Github actions raises `termios.error: (25, 'Inappropriate ioctl for device')`
|
|
123
|
+
# pytest test_interface.py::TestInterface::test_ask raises `io.UnsupportedOperation: redirected stdin is pseudofile, has no fileno()`
|
|
124
|
+
return input(prompt + " ")
|
|
125
|
+
finally:
|
|
126
|
+
input_started.set() # ensure dots thread stops
|
|
@@ -57,7 +57,7 @@ class TextualAdaptor(BackendAdaptor):
|
|
|
57
57
|
# NOTE: DatetimeTag not implemented
|
|
58
58
|
case SelectTag():
|
|
59
59
|
if tag.multiple:
|
|
60
|
-
selected = set(tag.
|
|
60
|
+
selected = set(tag._get_ui_val())
|
|
61
61
|
o = MySelectionList(tag, *((label, val, val in selected) for label, val, *_ in tag._get_options()))
|
|
62
62
|
else:
|
|
63
63
|
radio_buttons = [
|
|
@@ -100,12 +100,13 @@ class TextualAdaptor(BackendAdaptor):
|
|
|
100
100
|
else:
|
|
101
101
|
return []
|
|
102
102
|
|
|
103
|
-
def yes_no(self, text: str, focus_no=True):
|
|
104
|
-
return self.buttons(text, [("Yes", True), ("No", False)], int(focus_no) + 1)
|
|
103
|
+
def yes_no(self, text: str, focus_no=True, *, timeout: int = 0):
|
|
104
|
+
return self.buttons(text, [("Yes", True), ("No", False)], int(focus_no) + 1, timeout=timeout)
|
|
105
105
|
|
|
106
|
-
def buttons(self, text: str, buttons: list[tuple[str, Any]], focused: int = 1):
|
|
106
|
+
def buttons(self, text: str, buttons: list[tuple[str, Any]], focused: int = 1, *, timeout: int = 0):
|
|
107
107
|
self._build_buttons(text, buttons, focused)
|
|
108
|
-
self.app = app = TextualApp(self, False)
|
|
108
|
+
self.app = app = TextualApp(self, False, timeout=timeout)
|
|
109
|
+
|
|
109
110
|
if not app.run():
|
|
110
111
|
raise Cancelled
|
|
111
112
|
return self._get_buttons_val()
|
{mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/button_contents.py
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING,
|
|
1
|
+
from typing import TYPE_CHECKING, Optional
|
|
2
2
|
|
|
3
3
|
from textual import events
|
|
4
|
-
from textual.app import
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
5
|
from textual.containers import Center, Container
|
|
6
|
-
from textual.widgets import Footer, Label
|
|
6
|
+
from textual.widgets import Footer, Label
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
from .widgets import MySubmitButton
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from typing import TYPE_CHECKING
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
from textual.app import App
|
|
5
5
|
from textual.containers import Container
|
|
6
6
|
from textual.widget import Widget
|
|
7
7
|
|
|
8
|
-
from .form_contents import FormContents
|
|
9
|
-
|
|
10
8
|
from .button_contents import ButtonContents
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
from .form_contents import FormContents
|
|
10
|
+
from .timeout import TextualTimeout
|
|
13
11
|
from .widgets import TagWidget
|
|
14
12
|
|
|
15
|
-
|
|
16
13
|
if TYPE_CHECKING:
|
|
17
14
|
from .adaptor import TextualAdaptor
|
|
18
15
|
|
|
@@ -28,12 +25,13 @@ class TextualApp(App[bool | None]):
|
|
|
28
25
|
# We need to jump out of dir to allow children inherits (WebInterface)
|
|
29
26
|
CSS_PATH = "../_textual_interface/style.tcss"
|
|
30
27
|
|
|
31
|
-
def __init__(self, adaptor: "TextualAdaptor", submit: str | bool = True):
|
|
28
|
+
def __init__(self, adaptor: "TextualAdaptor", submit: str | bool = True, timeout=None):
|
|
32
29
|
super().__init__()
|
|
33
30
|
self.widgets: WidgetList = []
|
|
34
31
|
self.focusable_: WidgetList = []
|
|
35
32
|
self.adaptor = adaptor
|
|
36
33
|
self.submit = submit
|
|
34
|
+
self.timeout = timeout
|
|
37
35
|
|
|
38
36
|
# Form confirmation
|
|
39
37
|
if submit:
|
|
@@ -50,15 +48,20 @@ class TextualApp(App[bool | None]):
|
|
|
50
48
|
self.contents = Container()
|
|
51
49
|
yield self.contents
|
|
52
50
|
|
|
53
|
-
def on_mount(self) -> None:
|
|
51
|
+
async def on_mount(self) -> None:
|
|
54
52
|
self.has_been_confirmed = False
|
|
55
|
-
# def on_mount(self, event: events.Mount) -> None:
|
|
56
53
|
self.contents.remove_children()
|
|
57
54
|
if self.adaptor.button_app:
|
|
58
55
|
c = ButtonContents(self.adaptor, self.adaptor.button_app)
|
|
59
56
|
else:
|
|
60
57
|
c = FormContents(self.adaptor, self.widgets, self.focusable_)
|
|
61
|
-
self.contents.mount(c)
|
|
58
|
+
await self.contents.mount(c)
|
|
59
|
+
|
|
60
|
+
# timeout
|
|
61
|
+
if self.adaptor.button_app and self.timeout:
|
|
62
|
+
if not c.to_focus:
|
|
63
|
+
raise ValueError("Don't know what to timeout")
|
|
64
|
+
TextualTimeout(timeout=self.timeout, adaptor=self.adaptor, button=c.to_focus)
|
|
62
65
|
|
|
63
66
|
def action_confirm(self):
|
|
64
67
|
# next time, start on the same widget
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from .widgets import MyButton as Button
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .._mininterface.adaptor import Timeout
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from . import TextualAdaptor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TextualTimeout(Timeout):
|
|
12
|
+
button: Button
|
|
13
|
+
|
|
14
|
+
def __init__(self, timeout: int, adaptor: "TextualAdaptor", button: Button):
|
|
15
|
+
super().__init__(timeout, adaptor)
|
|
16
|
+
self.button = button
|
|
17
|
+
self.orig = self.button.label
|
|
18
|
+
self._task = asyncio.create_task(self.countdown(timeout))
|
|
19
|
+
self.button.set_blur_callback(lambda event=None: self.cancel())
|
|
20
|
+
|
|
21
|
+
async def countdown(self, count: int):
|
|
22
|
+
self.button.label = f"{self.orig} ({count})"
|
|
23
|
+
|
|
24
|
+
while count > 0:
|
|
25
|
+
await asyncio.sleep(1)
|
|
26
|
+
count -= 1
|
|
27
|
+
self.button.label = f"{self.orig} ({count})"
|
|
28
|
+
|
|
29
|
+
self.button.press()
|
|
30
|
+
|
|
31
|
+
def cancel(self):
|
|
32
|
+
if self._task and not self._task.done():
|
|
33
|
+
self._task.cancel()
|
|
34
|
+
self._task = None
|
|
35
|
+
self.button.label = self.orig
|
|
@@ -117,10 +117,18 @@ class MyButton(TagWidget, Button):
|
|
|
117
117
|
|
|
118
118
|
def __init__(self, tag, *args, **kwargs):
|
|
119
119
|
super().__init__(tag, tag.label, *args, **kwargs)
|
|
120
|
+
self.blur_callback = None
|
|
120
121
|
|
|
121
122
|
def on_button_pressed(self, event):
|
|
122
123
|
self.tag._facet.submit(_post_submit=self.tag._run_callable)
|
|
123
124
|
|
|
125
|
+
def on_blur(self):
|
|
126
|
+
if clb:=self.blur_callback:
|
|
127
|
+
clb()
|
|
128
|
+
|
|
129
|
+
def set_blur_callback(self, clb):
|
|
130
|
+
self.blur_callback = clb
|
|
131
|
+
|
|
124
132
|
def get_ui_value(self):
|
|
125
133
|
return self.tag.val
|
|
126
134
|
|
|
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Callable
|
|
|
5
5
|
|
|
6
6
|
from textual.widgets import Checkbox
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
try:
|
|
9
10
|
from tkscrollableframe import ScrolledFrame
|
|
10
11
|
from tktooltip import ToolTip
|
|
@@ -22,6 +23,7 @@ from ..settings import GuiSettings
|
|
|
22
23
|
from ..tag import Tag
|
|
23
24
|
from .facet import TkFacet
|
|
24
25
|
from .utils import recursive_set_focus, replace_widgets
|
|
26
|
+
from .timeout import TkTimeout
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class TkAdaptor(Tk, RichUiAdaptor, BackendAdaptor):
|
|
@@ -139,20 +141,27 @@ class TkAdaptor(Tk, RichUiAdaptor, BackendAdaptor):
|
|
|
139
141
|
return self.run_dialog(form, title, submit)
|
|
140
142
|
return form
|
|
141
143
|
|
|
142
|
-
def yes_no(self, text: str, focus_no=True):
|
|
143
|
-
return self.buttons(text, [("Yes", True), ("No", False)], int(focus_no) + 1)
|
|
144
|
+
def yes_no(self, text: str, focus_no=True, *, timeout: int = 0):
|
|
145
|
+
return self.buttons(text, [("Yes", True), ("No", False)], int(focus_no) + 1, timeout=timeout)
|
|
144
146
|
|
|
145
|
-
def buttons(self, text: str, buttons: list[tuple[str, Any]], focused: int = 1):
|
|
147
|
+
def buttons(self, text: str, buttons: list[tuple[str, Any]], focused: int = 1, *, timeout: int = 0):
|
|
146
148
|
label = Label(self.frame, text=text)
|
|
147
149
|
label.pack(pady=10)
|
|
150
|
+
b_focused = None
|
|
148
151
|
|
|
149
152
|
for i, (text, value) in enumerate(buttons):
|
|
150
153
|
button = Button(self.frame, text=text, command=lambda v=value: self._ok(v))
|
|
151
154
|
button.pack(side=LEFT, padx=10)
|
|
152
155
|
if i == focused - 1:
|
|
153
156
|
button.focus_set()
|
|
154
|
-
|
|
155
|
-
button.bind("<Return>", lambda _: b.invoke())
|
|
157
|
+
b_focused = button
|
|
158
|
+
button.bind("<Return>", lambda _, b=b_focused: b.invoke())
|
|
159
|
+
|
|
160
|
+
if timeout:
|
|
161
|
+
if not b_focused:
|
|
162
|
+
raise ValueError("Don't know what to timeout")
|
|
163
|
+
TkTimeout(timeout, self, b_focused)
|
|
164
|
+
|
|
156
165
|
return self.mainloop()
|
|
157
166
|
|
|
158
167
|
def _bind_event(self, event, handler):
|
|
@@ -215,7 +224,7 @@ class TkAdaptor(Tk, RichUiAdaptor, BackendAdaptor):
|
|
|
215
224
|
def _destroy(self):
|
|
216
225
|
self.destroy()
|
|
217
226
|
|
|
218
|
-
def bind_shortcut(self, shortcut: str, widget: Widget|Callable[[], Widget]):
|
|
227
|
+
def bind_shortcut(self, shortcut: str, widget: Widget | Callable[[], Widget]):
|
|
219
228
|
def _(event=None):
|
|
220
229
|
nonlocal widget
|
|
221
230
|
if isinstance(widget, FunctionType):
|
|
@@ -104,7 +104,7 @@ class SelectInputWrapper:
|
|
|
104
104
|
taken = False
|
|
105
105
|
|
|
106
106
|
for i, (choice_label, choice_val, tip, tupled_key) in enumerate(options):
|
|
107
|
-
var = BooleanVar(value=choice_val in tag.
|
|
107
|
+
var = BooleanVar(value=choice_val in tag._get_ui_val())
|
|
108
108
|
|
|
109
109
|
def on_toggle(val=choice_val, var=var):
|
|
110
110
|
if var.get():
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from tkinter import Button
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from .._mininterface.adaptor import Timeout
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from . import TkAdaptor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TkTimeout(Timeout):
|
|
11
|
+
adaptor: "TkAdaptor"
|
|
12
|
+
|
|
13
|
+
def __init__(self, timeout: int, adaptor: "TkAdaptor", button: Button):
|
|
14
|
+
super().__init__(timeout, adaptor)
|
|
15
|
+
self.button = button
|
|
16
|
+
self.after_id = None
|
|
17
|
+
self.orig:str = self.button.cget("text")
|
|
18
|
+
|
|
19
|
+
self.countdown(timeout)
|
|
20
|
+
|
|
21
|
+
self.button.bind("<FocusOut>", self.cancel)
|
|
22
|
+
|
|
23
|
+
def countdown(self, count):
|
|
24
|
+
try:
|
|
25
|
+
self.button.config(text=f"{self.orig} ({count})")
|
|
26
|
+
except: # The form has been submitted and the button is not valid anymore
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
if count > 0:
|
|
30
|
+
self.after_id = self.adaptor.after(1000, self.countdown, count - 1)
|
|
31
|
+
else:
|
|
32
|
+
self.button.invoke()
|
|
33
|
+
|
|
34
|
+
def cancel(self, event=None):
|
|
35
|
+
if self.after_id is not None:
|
|
36
|
+
self.adaptor.after_cancel(self.after_id)
|
|
37
|
+
self.after_id = None
|
|
38
|
+
self.button.config(text=self.orig)
|
|
@@ -71,7 +71,12 @@ class SerializedChildAdaptor(TextualAdaptor):
|
|
|
71
71
|
self._original_stdout.buffer.flush()
|
|
72
72
|
return self.receive()
|
|
73
73
|
|
|
74
|
-
def buttons(self, text: str, buttons: ButtonAppType, focused: int = 1):
|
|
74
|
+
def buttons(self, text: str, buttons: ButtonAppType, focused: int = 1, *, timeout: int = 0):
|
|
75
|
+
if timeout:
|
|
76
|
+
# NOTE timeout not implemented
|
|
77
|
+
import warnings
|
|
78
|
+
warnings.warn("Timeout not implemented for web interface")
|
|
79
|
+
print("Warning: Timeout not implemented for web interface")
|
|
75
80
|
return self.send(SerCommand.BUTTONS, text, buttons, focused)
|
|
76
81
|
|
|
77
82
|
def run_dialog(self, form: TagDict, title: str = "", submit: bool | str = True) -> TagDict:
|
|
@@ -75,7 +75,7 @@ def _assure_blank_or_bool(args):
|
|
|
75
75
|
BlankTrue = Annotated[
|
|
76
76
|
bool | None,
|
|
77
77
|
PrimitiveConstructorSpec(
|
|
78
|
-
nargs=(0,1),
|
|
78
|
+
nargs=(0,1),
|
|
79
79
|
metavar="blank=True|BOOL",
|
|
80
80
|
instance_from_str=_assure_blank_or_bool,
|
|
81
81
|
is_instance=lambda instance: True, # NOTE not sure
|
|
@@ -100,6 +100,14 @@ m.select([Color.GREEN, Color.BLUE])
|
|
|
100
100
|
|
|
101
101
|

|
|
102
102
|
|
|
103
|
+
!!! Note
|
|
104
|
+
Enum keys are seen from the CLI (unless modified). Enum values are seen from the UI. Should you need to bear an additional information, not displayed in CLI nor UI, tackle the Enum class.
|
|
105
|
+
For the examples, see [Supported types / Enum](Supported-types.md/#enums).
|
|
106
|
+
|
|
107
|
+
## Literal
|
|
108
|
+
|
|
109
|
+
`typing.Literal` allows you to do a one-liner. Their values are seen from the CLI. See the [Supported types / Literal](Supported-types.md/#literal) examples.
|
|
110
|
+
|
|
103
111
|
## Further examples
|
|
104
112
|
|
|
105
113
|
See [mininterface.select][mininterface.Mininterface.select] or [`SelectTag.options`][mininterface.tag.SelectTag.options] for further usage.
|
|
@@ -254,7 +262,7 @@ class SelectTag(Tag[TagValue]):
|
|
|
254
262
|
def _get_selected_keys(self):
|
|
255
263
|
if not self.multiple:
|
|
256
264
|
raise AttributeError
|
|
257
|
-
return [k for k, val, *_ in self._get_options() if val in self.
|
|
265
|
+
return [k for k, val, *_ in self._get_options() if val in self._get_ui_val()]
|
|
258
266
|
|
|
259
267
|
@classmethod
|
|
260
268
|
def _repr_val(cls, v) -> str:
|
|
@@ -357,6 +365,8 @@ class SelectTag(Tag[TagValue]):
|
|
|
357
365
|
return [(delim.join(key), key) for key in keys]
|
|
358
366
|
|
|
359
367
|
def _make_default_value(self):
|
|
368
|
+
if self.multiple:
|
|
369
|
+
return []
|
|
360
370
|
if d := self._build_options():
|
|
361
371
|
return next(iter(d.values()))
|
|
362
372
|
|
|
@@ -669,6 +669,8 @@ class Tag(Generic[TagValue]):
|
|
|
669
669
|
return [_(subt) for subt in subtype]
|
|
670
670
|
if origin is tuple:
|
|
671
671
|
return origin, list(subtype)
|
|
672
|
+
elif origin is dict:
|
|
673
|
+
return origin, subtype
|
|
672
674
|
elif origin is Literal:
|
|
673
675
|
return origin, subtype
|
|
674
676
|
elif len(subtype) == 1:
|
|
@@ -695,7 +697,7 @@ class Tag(Generic[TagValue]):
|
|
|
695
697
|
def set_error_text(self, s=""):
|
|
696
698
|
""" Mark the field as required and possibly adds an error text to the description. """
|
|
697
699
|
if s:
|
|
698
|
-
self.description = f"{s} {self._original_desc}"
|
|
700
|
+
self.description = f"{s} {self._original_desc}".strip()
|
|
699
701
|
if n := self._original_label:
|
|
700
702
|
# Why checking self._original_label?
|
|
701
703
|
# If for any reason (I do not know the use case) is not set, we would end up with '* None'
|
|
@@ -914,12 +916,30 @@ class Tag(Generic[TagValue]):
|
|
|
914
916
|
elif get_origin(cast_to) is tuple:
|
|
915
917
|
cast_to = get_args(cast_to)
|
|
916
918
|
if len(cast_to) == 2 and cast_to[1] is Ellipsis:
|
|
919
|
+
# ex. `list[tuple[int, ...]]`
|
|
917
920
|
cast_to = cast_to[0]
|
|
918
921
|
candidate = origin(tuple(cast_to(v) for v in tuple_) for tuple_ in literal_eval(ui_value))
|
|
919
|
-
else:
|
|
920
|
-
|
|
922
|
+
else: # ex. `list[tuple[int, Path, str]]`
|
|
923
|
+
tuples = []
|
|
924
|
+
for str_tuple in literal_eval(ui_value):
|
|
925
|
+
try:
|
|
926
|
+
typed_tuple = tuple(
|
|
927
|
+
cast_to_(v) for v, cast_to_ in zip(str_tuple, cast_to, strict=True)
|
|
928
|
+
)
|
|
929
|
+
except ValueError as e:
|
|
930
|
+
# this was the right way, there is no reason to attempt further
|
|
931
|
+
self.set_error_text(f"Bad tuple length for {str_tuple!r}")
|
|
932
|
+
return False
|
|
933
|
+
tuples.append(typed_tuple)
|
|
934
|
+
|
|
935
|
+
candidate = origin(tuples)
|
|
936
|
+
elif origin is dict: # ex. `dict[str, str]`
|
|
937
|
+
# we ignore cast_to as `[str, str]` part will be checked later
|
|
938
|
+
candidate = literal_eval(ui_value)
|
|
921
939
|
else:
|
|
922
940
|
candidate = origin(cast_to(v) for v in literal_eval(ui_value))
|
|
941
|
+
elif cast_to is dict: # `annotation=dict`
|
|
942
|
+
candidate = literal_eval(ui_value) #
|
|
923
943
|
else:
|
|
924
944
|
candidate = cast_to(ui_value)
|
|
925
945
|
except (TypeError, ValueError, SyntaxError):
|
|
@@ -2,7 +2,7 @@ from copy import copy
|
|
|
2
2
|
from datetime import date, time
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any, Iterable, Literal, Type, get_origin, get_type_hints
|
|
5
|
+
from typing import Any, Iterable, Literal, Type, get_args, get_origin, get_type_hints
|
|
6
6
|
|
|
7
7
|
from annotated_types import BaseMetadata, GroupedMetadata, Len
|
|
8
8
|
|
|
@@ -108,6 +108,14 @@ def tag_factory(
|
|
|
108
108
|
elif isinstance(metadata, (BaseMetadata, Len)):
|
|
109
109
|
# Why not checking `GroupedMetadata` instead of `Len`? See below. You won't believe.
|
|
110
110
|
validators.append(metadata)
|
|
111
|
+
elif get_origin(metadata) is Literal:
|
|
112
|
+
if "<class 'mininterface.tag.flag._Blank'>" in (repr(type(f)) for f in field_type.__metadata__):
|
|
113
|
+
# a special case, this is a default CLI value and will be processed by flag.Blank
|
|
114
|
+
# `foo: Annotated[Blank[int], Literal[2]] = None`
|
|
115
|
+
# Using repr and not importing due to (vague) performance reasons.
|
|
116
|
+
continue
|
|
117
|
+
# `variable = 2, 3; foo: Annotated[int, Literal[variable]] = None`
|
|
118
|
+
annotation = metadata
|
|
111
119
|
if not tag:
|
|
112
120
|
tag = tag_assure_type(Tag(val, description, annotation, *args, **kwargs))
|
|
113
121
|
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "mininterface"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.2"
|
|
8
8
|
description = "A minimal access to GUI, TUI, CLI and config"
|
|
9
9
|
authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
|
|
10
10
|
license = "LGPL-3.0-or-later"
|
|
@@ -17,7 +17,7 @@ python = "^3.10"
|
|
|
17
17
|
simple_term_menu = "*"
|
|
18
18
|
annotated-types = "*"
|
|
19
19
|
# Basic requirements (CLI and full .form support (m.form(dataclass_class))
|
|
20
|
-
tyro = { version = "^0.9", optional = true }
|
|
20
|
+
tyro = { version = "^0.9.31", optional = true }
|
|
21
21
|
pyyaml = { version = "*", optional = true }
|
|
22
22
|
humanize = { version = "*", optional = true }
|
|
23
23
|
textual = { version = "<2.0.0", optional = true }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_textual_interface/file_picker_input.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mininterface-1.1.1 → mininterface-1.1.2}/mininterface/_tk_interface/redirect_text_tkinter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|