mininterface 1.1.1__tar.gz → 1.1.3__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.3}/PKG-INFO +3 -4
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/__main__.py +1 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/cli_parser.py +41 -8
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/cli_utils.py +19 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/config_file.py +6 -10
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/dataclass_creation.py +26 -10
- mininterface-1.1.3/mininterface/_lib/future_compatibility.py +9 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/run.py +26 -14
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/start.py +11 -11
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_mininterface/__init__.py +19 -3
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_mininterface/adaptor.py +16 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_mininterface/mixin.py +6 -6
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_text_interface/__init__.py +10 -6
- mininterface-1.1.3/mininterface/_text_interface/timeout.py +129 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/adaptor.py +6 -5
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/button_contents.py +3 -3
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/textual_app.py +13 -10
- mininterface-1.1.3/mininterface/_textual_interface/timeout.py +39 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/widgets.py +8 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/adaptor.py +15 -6
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/select_input.py +1 -1
- mininterface-1.1.3/mininterface/_tk_interface/timeout.py +41 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/utils.py +4 -3
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_web_interface/child_adaptor.py +6 -1
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/interfaces.py +5 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/settings.py +124 -2
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/flag.py +1 -1
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/select_tag.py +11 -1
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/tag.py +23 -3
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/tag_factory.py +9 -1
- {mininterface-1.1.1 → mininterface-1.1.3}/pyproject.toml +4 -3
- mininterface-1.1.1/mininterface/_lib/future_compatibility.py +0 -6
- {mininterface-1.1.1 → mininterface-1.1.3}/LICENSE +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/README.md +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/argparse_support.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/auxiliary.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/cli_flags.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/form_dict.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/redirectable.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/shortcuts.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/showcase.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/tyro_patches.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_text_interface/adaptor.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/facet.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/file_picker_input.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/form_contents.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/style.tcss +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/date_entry.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/external_fix.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/facet.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/secret_entry.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_web_interface/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_web_interface/app.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_web_interface/parent_adaptor.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/cli.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/exceptions.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/experimental.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/facet/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/alias.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/callback_tag.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/datetime_tag.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/internal.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/path_tag.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/secret_tag.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/type_stubs.py +0 -0
- {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/validators.py +0 -0
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
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
|
|
7
7
|
Author: Edvard Rejthar
|
|
8
8
|
Author-email: edvard.rejthar@nic.cz
|
|
9
|
-
Requires-Python: >=3.10,<
|
|
9
|
+
Requires-Python: >=3.10,<3.14
|
|
10
10
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
17
16
|
Provides-Extra: all
|
|
18
17
|
Provides-Extra: basic
|
|
19
18
|
Provides-Extra: gui
|
|
@@ -34,7 +33,7 @@ Requires-Dist: tkcalendar ; extra == "gui" or extra == "ui" or extra == "all"
|
|
|
34
33
|
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
34
|
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
35
|
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"
|
|
36
|
+
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
37
|
Project-URL: Homepage, https://github.com/CZ-NIC/mininterface
|
|
39
38
|
Description-Content-Type: text/markdown
|
|
40
39
|
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
# CLI and config file parsing.
|
|
3
3
|
#
|
|
4
4
|
from dataclasses import asdict
|
|
5
|
+
from functools import reduce
|
|
5
6
|
import sys
|
|
6
7
|
from collections import deque
|
|
7
8
|
from contextlib import ExitStack
|
|
8
|
-
from typing import Optional, Sequence, Type, Union
|
|
9
|
+
from typing import Annotated, Optional, Sequence, Type, Union
|
|
9
10
|
from unittest.mock import patch
|
|
10
11
|
|
|
11
|
-
|
|
12
12
|
from .cli_flags import CliFlags
|
|
13
13
|
|
|
14
14
|
from ..cli import Command
|
|
15
|
+
from ..settings import CliSettings
|
|
15
16
|
|
|
16
17
|
from ..exceptions import Cancelled
|
|
17
18
|
from .auxiliary import (
|
|
@@ -33,6 +34,7 @@ try:
|
|
|
33
34
|
from tyro._argparse import _SubParsersAction, ArgumentParser
|
|
34
35
|
from tyro._argparse_formatter import TyroArgumentParser
|
|
35
36
|
from tyro._singleton import MISSING_NONPROP
|
|
37
|
+
from tyro.conf import OmitArgPrefixes, OmitSubcommandPrefixes, DisallowNone, FlagCreatePairsOff
|
|
36
38
|
|
|
37
39
|
from .tyro_patches import (
|
|
38
40
|
_crawling,
|
|
@@ -75,6 +77,7 @@ def parse_cli(
|
|
|
75
77
|
ask_for_missing: bool = True,
|
|
76
78
|
args: Optional[Sequence[str]] = None,
|
|
77
79
|
ask_on_empty_cli: Optional[bool] = None,
|
|
80
|
+
cli_settings: Optional[CliSettings] = None,
|
|
78
81
|
_crawled=None,
|
|
79
82
|
_req_fields=None,
|
|
80
83
|
) -> tuple[EnvClass, bool]:
|
|
@@ -118,18 +121,43 @@ def parse_cli(
|
|
|
118
121
|
# Special CLI parsing
|
|
119
122
|
if sys.modules.get("mininterface.tag.flag") is not None:
|
|
120
123
|
from ..tag.flag import _custom_registry
|
|
121
|
-
else:
|
|
124
|
+
else: # run only if the user imported the flags. (Untested) performance reasons.
|
|
122
125
|
_custom_registry = None
|
|
123
126
|
|
|
127
|
+
annotations = None
|
|
128
|
+
if cli_settings:
|
|
129
|
+
annotations = [
|
|
130
|
+
cls
|
|
131
|
+
for cond, cls in (
|
|
132
|
+
(cli_settings.omit_arg_prefixes, OmitArgPrefixes),
|
|
133
|
+
(cli_settings.omit_subcommand_prefixes, OmitSubcommandPrefixes),
|
|
134
|
+
(cli_settings.disallow_none, DisallowNone),
|
|
135
|
+
(cli_settings.flag_create_pairs_off, FlagCreatePairsOff),
|
|
136
|
+
)
|
|
137
|
+
if cond
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
def annot(type_form):
|
|
141
|
+
if annotations:
|
|
142
|
+
if sys.version_info >= (3, 11):
|
|
143
|
+
from .future_compatibility import spread_annotated
|
|
144
|
+
|
|
145
|
+
return spread_annotated(type_form, annotations)
|
|
146
|
+
else:
|
|
147
|
+
from warnings import warn
|
|
148
|
+
|
|
149
|
+
warn(f"Cannot apply {annotations} on Python <= 3.11.")
|
|
150
|
+
return type_form
|
|
151
|
+
|
|
124
152
|
try:
|
|
125
153
|
with ExitStack() as stack:
|
|
126
154
|
[stack.enter_context(p) for p in patches] # apply just the chosen mocks
|
|
127
155
|
try:
|
|
128
|
-
env = cli(type_form, args=args, registry=_custom_registry, **kwargs)
|
|
156
|
+
env = cli(annot(type_form), args=args, registry=_custom_registry, **kwargs)
|
|
129
157
|
except BaseException:
|
|
130
158
|
# Why this exception handling? Try putting this out and test_strange_error_mitigation fails.
|
|
131
159
|
if len(env_classes) > 1 and kwargs.get("default"):
|
|
132
|
-
env = cli(kwargs["default"].__class__, args=args[1:], registry=_custom_registry, **kwargs)
|
|
160
|
+
env = cli(annot(kwargs["default"].__class__), args=args[1:], registry=_custom_registry, **kwargs)
|
|
133
161
|
else:
|
|
134
162
|
raise
|
|
135
163
|
# Why setting m.env instead of putting into into a constructor of a new get_interface() call?
|
|
@@ -144,7 +172,9 @@ def parse_cli(
|
|
|
144
172
|
m.env = env
|
|
145
173
|
except BaseException as exception:
|
|
146
174
|
if ask_for_missing and getattr(exception, "code", None) == 2 and failed_fields.get():
|
|
147
|
-
env = _dialog_missing(
|
|
175
|
+
env = _dialog_missing(
|
|
176
|
+
env_classes, kwargs, m, cf, ask_for_missing, args, cli_settings, _crawled, _req_fields
|
|
177
|
+
)
|
|
148
178
|
|
|
149
179
|
if final_call:
|
|
150
180
|
# Ask for the wrong fields
|
|
@@ -240,6 +270,7 @@ def _dialog_missing(
|
|
|
240
270
|
cf: Optional[CliFlags],
|
|
241
271
|
ask_for_missing: bool,
|
|
242
272
|
args: Optional[Sequence[str]],
|
|
273
|
+
cli_settings,
|
|
243
274
|
crawled,
|
|
244
275
|
req_fields: Optional[TagDict],
|
|
245
276
|
) -> EnvClass:
|
|
@@ -301,7 +332,7 @@ def _dialog_missing(
|
|
|
301
332
|
# We have just put a default values for missing fields so that tyro will not fail.
|
|
302
333
|
# If we succeeded (no exotic case), this will pass through.
|
|
303
334
|
# Then, we impose the user to fill the missing values.
|
|
304
|
-
env, _ = parse_cli(env_classes, kwargs, m, cf, ask_for_missing, args, None, crawled, req_fields)
|
|
335
|
+
env, _ = parse_cli(env_classes, kwargs, m, cf, ask_for_missing, args, None, cli_settings, crawled, req_fields)
|
|
305
336
|
td = dataclass_to_tagdict(env, m)
|
|
306
337
|
# Remove teporary defaults to be correctly displayed in the dialog form
|
|
307
338
|
# so that user must fill them.
|
|
@@ -345,7 +376,9 @@ def _fetch_currently_failed(requireds) -> TagDict:
|
|
|
345
376
|
# Here, we pick the field unknown to the CLI parser too.
|
|
346
377
|
# As whole subparser was unknown here, we safely consider all its fields wrong fields.
|
|
347
378
|
if fname:
|
|
348
|
-
get_or_create_parent_dict(missing_req, fname, True)[fname_raw] = get_or_create_parent_dict(
|
|
379
|
+
get_or_create_parent_dict(missing_req, fname, True)[fname_raw] = get_or_create_parent_dict(
|
|
380
|
+
requireds, fname
|
|
381
|
+
)
|
|
349
382
|
else:
|
|
350
383
|
# This is the default subparser, without a field name:
|
|
351
384
|
# ex. `run([List, Run])`
|
|
@@ -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.
|
|
@@ -18,21 +18,19 @@ except ImportError:
|
|
|
18
18
|
def parse_config_file(
|
|
19
19
|
env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
20
20
|
config_file: Path | None = None,
|
|
21
|
-
settings: Optional[MininterfaceSettings] = None,
|
|
22
21
|
**kwargs,
|
|
23
|
-
) -> tuple[dict,
|
|
22
|
+
) -> tuple[dict, dict | None]:
|
|
24
23
|
"""Fetches the config file into the program defaults kwargs["default"] and UI settings.
|
|
25
24
|
|
|
26
25
|
Args:
|
|
27
26
|
env_class: Class with the configuration.
|
|
28
27
|
config_file: File to load YAML to be merged with the configuration.
|
|
29
28
|
You do not have to re-define all the settings in the config file, you can choose a few.
|
|
30
|
-
settings: Used to complement the 'mininterface' config file section-
|
|
31
29
|
Kwargs:
|
|
32
30
|
The same as for argparse.ArgumentParser.
|
|
33
31
|
|
|
34
32
|
Returns:
|
|
35
|
-
Tuple of kwargs and
|
|
33
|
+
Tuple of kwargs and dict (section 'mininterface' in the config file).
|
|
36
34
|
"""
|
|
37
35
|
if isinstance(env_or_list, list):
|
|
38
36
|
subcommands, env = env_or_list, None
|
|
@@ -50,23 +48,21 @@ def parse_config_file(
|
|
|
50
48
|
" Describe the developer your usecase so that they might implement this."
|
|
51
49
|
)
|
|
52
50
|
|
|
51
|
+
confopt = None
|
|
53
52
|
if "default" not in kwargs and not subcommands and config_file:
|
|
54
53
|
# Undocumented feature. User put a namespace into kwargs["default"]
|
|
55
54
|
# that already serves for defaults. We do not fetch defaults yet from a config file.
|
|
56
55
|
disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
|
|
57
56
|
try:
|
|
58
|
-
|
|
59
|
-
# Section 'mininterface' in the config file.
|
|
60
|
-
settings = _merge_settings(settings, confopt)
|
|
61
|
-
|
|
57
|
+
confopt = disk.pop("mininterface", None)
|
|
62
58
|
kwargs["default"] = create_with_missing(env, disk)
|
|
63
59
|
except TypeError:
|
|
64
60
|
raise SyntaxError(f"Config file parsing failed for {config_file}")
|
|
65
61
|
|
|
66
|
-
return kwargs,
|
|
62
|
+
return kwargs, confopt
|
|
67
63
|
|
|
68
64
|
|
|
69
|
-
def
|
|
65
|
+
def ensure_settings_inheritance(
|
|
70
66
|
runopt: MininterfaceSettings | None, confopt: dict, _def_fact=MininterfaceSettings
|
|
71
67
|
) -> MininterfaceSettings:
|
|
72
68
|
# Settings inheritance:
|
|
@@ -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
|
|
|
@@ -235,24 +237,38 @@ def _process_field(fname, ftype, disk_value, wf, m, default_value=MISSING):
|
|
|
235
237
|
|
|
236
238
|
# the subcommand has been already chosen by CLI parser
|
|
237
239
|
if isinstance(disk_value, ChosenSubcommand): # `(class Message | class Console)`
|
|
240
|
+
# there might be a subcommand prefix, ex. 'val:message' -> 'message'
|
|
241
|
+
name = disk_value.name.partition(":")[2] or disk_value.name
|
|
242
|
+
|
|
238
243
|
for _subcomm in get_args(_unwrap_annotated(ftype)):
|
|
239
|
-
if
|
|
244
|
+
if name == to_kebab_case(_subcomm.__name__):
|
|
240
245
|
ftype = _subcomm # `class Message` only
|
|
241
246
|
disk_value = disk_value.subdict
|
|
242
247
|
break
|
|
243
248
|
else:
|
|
244
249
|
raise ValueError(f"Type {disk_value} not found in {ftype}")
|
|
245
250
|
|
|
246
|
-
if disk_value is not
|
|
251
|
+
if disk_value is not MISSING_NONPROP:
|
|
247
252
|
if _is_struct_type(ftype):
|
|
248
253
|
return _init_struct_value(ftype, disk_value, wf, fname, m)
|
|
254
|
+
# NOTE for handling subcommands in config file, I'll need something like this,
|
|
255
|
+
# combined with a structure like ChosenSubcommand, possibly inheriting from MISSING_NONPROP
|
|
256
|
+
# to be ignored by tyro, yet fetchable by cli_parser
|
|
257
|
+
# elif _is_subcommands:
|
|
258
|
+
# return {cl.__name__: _init_struct_value(cl, {}, wf, fname, m) for cl in get_args(ftype)}
|
|
249
259
|
return coerce_type_to_annotation(disk_value, ftype)
|
|
250
260
|
|
|
261
|
+
if _is_struct_type(ftype):
|
|
262
|
+
# Ex. `foo: Subcommand`
|
|
263
|
+
return _init_struct_value(ftype, {}, wf, fname, m)
|
|
264
|
+
|
|
251
265
|
# We must handle the case when there are multiple subcommands possible.
|
|
252
266
|
# The user decides now which way to go (choose a subcommand).
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
267
|
+
# Ex. `foo: Subcommand1 | Subcommand2`
|
|
268
|
+
_is_subcommands = (origin is Union or origin is UnionType) and all(_is_struct_type(cl) for cl in get_args(ftype))
|
|
269
|
+
if _is_subcommands:
|
|
270
|
+
if not m: # we are parsing the config file. The fields are not defined in the config file
|
|
271
|
+
return MISSING_NONPROP
|
|
256
272
|
ftype = choose_subcommand(get_args(ftype), m)
|
|
257
273
|
return _init_struct_value(ftype, {}, wf, fname, m)
|
|
258
274
|
|
|
@@ -271,7 +287,7 @@ def _process_dataclass(env, disk, wf: Optional[dict], m: Optional["Mininterface"
|
|
|
271
287
|
elif f.default is not MISSING:
|
|
272
288
|
v = f.default
|
|
273
289
|
else:
|
|
274
|
-
v = _process_field(f.name, f.type,
|
|
290
|
+
v = _process_field(f.name, f.type, MISSING_NONPROP, wf, m)
|
|
275
291
|
yield f.name, v
|
|
276
292
|
|
|
277
293
|
|
|
@@ -5,10 +5,11 @@ from os import environ
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Literal, Optional, Sequence, Type
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
from .._mininterface import Mininterface
|
|
9
10
|
from ..exceptions import DependencyRequired, ValidationFail
|
|
10
11
|
from ..interfaces import get_interface
|
|
11
|
-
from ..settings import MininterfaceSettings
|
|
12
|
+
from ..settings import CliSettings, MininterfaceSettings, UiSettings
|
|
12
13
|
from .form_dict import EnvClass
|
|
13
14
|
|
|
14
15
|
try:
|
|
@@ -16,11 +17,11 @@ try:
|
|
|
16
17
|
from .argparse_support import parser_to_dataclass
|
|
17
18
|
from .cli_flags import CliFlags as _CliFlags
|
|
18
19
|
from .cli_parser import assure_args, parse_cli
|
|
19
|
-
from .config_file import parse_config_file
|
|
20
|
+
from .config_file import parse_config_file, ensure_settings_inheritance
|
|
20
21
|
from .dataclass_creation import choose_subcommand, to_kebab_case
|
|
21
22
|
from .start import Start
|
|
22
23
|
except DependencyRequired as e:
|
|
23
|
-
assure_args, parse_cli, parse_config_file, parser_to_dataclass = (e,) *
|
|
24
|
+
assure_args, parse_cli, parse_config_file, ensure_settings_inheritance, parser_to_dataclass = (e,) * 5
|
|
24
25
|
Start, SubcommandPlaceholder = (e,) * 2
|
|
25
26
|
to_kebab_case, choose_subcommand, _CliFlags = (e,) * 3
|
|
26
27
|
|
|
@@ -40,7 +41,7 @@ def run(
|
|
|
40
41
|
# We do not use InterfaceType as a type here because we want the documentation to show full alias:
|
|
41
42
|
interface: Type[Mininterface] | Literal["gui"] | Literal["tui"] | Literal["text"] | Literal["web"] | None = None,
|
|
42
43
|
args: Optional[Sequence[str]] = None,
|
|
43
|
-
settings: Optional[MininterfaceSettings] = None,
|
|
44
|
+
settings: Optional[MininterfaceSettings | UiSettings | CliSettings] = None,
|
|
44
45
|
**kwargs,
|
|
45
46
|
) -> Mininterface[EnvClass]:
|
|
46
47
|
"""The main access, start here.
|
|
@@ -192,7 +193,7 @@ def run(
|
|
|
192
193
|
see the full [list](Interfaces.md) of possible interfaces.
|
|
193
194
|
If not set, we look also for an environment variable [`MININTERFACE_INTERFACE`](Interfaces.md#environment-variable-mininterface_interface) and in the config file.
|
|
194
195
|
args: Parse arguments from a sequence instead of the command line.
|
|
195
|
-
settings: Default settings. These might be further modified by the 'mininterface' section in the config file.
|
|
196
|
+
settings: Default settings. These might be further modified by the 'mininterface' section in the config file. See the [Settings](Settings.md) section.
|
|
196
197
|
Kwargs:
|
|
197
198
|
The same as for [argparse.ArgumentParser](https://docs.python.org/3/library/argparse.html).
|
|
198
199
|
|
|
@@ -275,8 +276,22 @@ def run(
|
|
|
275
276
|
if isinstance(env_or_list, ArgumentParser):
|
|
276
277
|
env_or_list, add_version = parser_to_dataclass(env_or_list)
|
|
277
278
|
|
|
278
|
-
# Parse
|
|
279
|
-
|
|
279
|
+
# Parse config file
|
|
280
|
+
kwargs, settings_conf = parse_config_file(env_or_list or _Empty, config_file, **kwargs)
|
|
281
|
+
|
|
282
|
+
# Ensure settings inheritance
|
|
283
|
+
if isinstance(settings, CliSettings):
|
|
284
|
+
settings = MininterfaceSettings(cli=settings)
|
|
285
|
+
elif isinstance(settings, UiSettings):
|
|
286
|
+
# Ex. `settings=GuiSettings(...)` -> `settings=MininterfaceSettings(gui=GuiSettings(...)`
|
|
287
|
+
attr_name = settings.__class__.__name__.removesuffix("Settings").lower()
|
|
288
|
+
settings = MininterfaceSettings(**{attr_name: settings})
|
|
289
|
+
if settings or settings_conf:
|
|
290
|
+
# previous settings are used to complement the 'mininterface' config file section
|
|
291
|
+
settings = ensure_settings_inheritance(settings, settings_conf or {})
|
|
292
|
+
cliset = settings.cli if settings else CliSettings()
|
|
293
|
+
|
|
294
|
+
# Choose an interface
|
|
280
295
|
m = get_interface(interface, title, settings)
|
|
281
296
|
|
|
282
297
|
# Resolve SubcommandPlaceholder
|
|
@@ -290,17 +305,14 @@ def run(
|
|
|
290
305
|
):
|
|
291
306
|
args[0] = to_kebab_case(choose_subcommand(env_or_list, m).__name__)
|
|
292
307
|
|
|
293
|
-
#
|
|
294
|
-
# C) No Env object
|
|
295
|
-
|
|
296
|
-
kwargs, settings = parse_config_file(env_or_list or _Empty, config_file, settings, **kwargs)
|
|
308
|
+
# Parse CLI arguments, possibly merged from a config file.
|
|
297
309
|
cf = _CliFlags(add_verbose, add_version, add_version_package, add_quiet)
|
|
298
310
|
if env_or_list:
|
|
299
|
-
#
|
|
311
|
+
# A single Env object, or a list of such objects (with one is not/being selected via args)
|
|
300
312
|
# Load configuration from CLI and a config file
|
|
301
313
|
try:
|
|
302
314
|
parse_cli(
|
|
303
|
-
env_or_list, kwargs, m, cf, ask_for_missing, args, ask_on_empty_cli
|
|
315
|
+
env_or_list, kwargs, m, cf, ask_for_missing, args, ask_on_empty_cli, cliset
|
|
304
316
|
)
|
|
305
317
|
except Exception as e:
|
|
306
318
|
# Undocumented MININTERFACE_DEBUG flag. Note ipdb package requirement.
|
|
@@ -321,7 +333,7 @@ def run(
|
|
|
321
333
|
else:
|
|
322
334
|
# C) No Env object
|
|
323
335
|
# even though there is no configuration, yet we need to parse CLI for meta-commands like --help or --verbose
|
|
324
|
-
parse_cli(_Empty, {}, m, cf, ask_for_missing, args)
|
|
336
|
+
parse_cli(_Empty, {}, m, cf, ask_for_missing, args, None, cliset)
|
|
325
337
|
|
|
326
338
|
return m
|
|
327
339
|
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# Starting and maintaining a program, using mininterface, in the system.
|
|
2
2
|
import sys
|
|
3
|
-
from collections import defaultdict
|
|
4
|
-
from dataclasses import is_dataclass
|
|
3
|
+
# from collections import defaultdict
|
|
4
|
+
# from dataclasses import is_dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from subprocess import run
|
|
7
7
|
from typing import Optional, Type
|
|
8
|
-
from warnings import warn
|
|
8
|
+
# from warnings import warn
|
|
9
9
|
|
|
10
10
|
from .._mininterface import Mininterface
|
|
11
|
-
from ..cli import Command, SubcommandPlaceholder
|
|
12
|
-
from ..exceptions import DependencyRequired
|
|
11
|
+
# from ..cli import Command, SubcommandPlaceholder
|
|
12
|
+
# from ..exceptions import DependencyRequired
|
|
13
13
|
from ..interfaces import get_interface
|
|
14
|
-
from ..tag import Tag
|
|
15
|
-
from .form_dict import DataClass, EnvClass, TagDict, dataclass_to_tagdict
|
|
14
|
+
# from ..tag import Tag
|
|
15
|
+
# from .form_dict import DataClass, EnvClass, TagDict, dataclass_to_tagdict
|
|
16
16
|
|
|
17
|
-
try:
|
|
18
|
-
|
|
19
|
-
except DependencyRequired as e:
|
|
20
|
-
|
|
17
|
+
# try:
|
|
18
|
+
# from .cli_parser import parse_cli
|
|
19
|
+
# except DependencyRequired as e:
|
|
20
|
+
# parse_cli = e
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class Start:
|
|
@@ -90,6 +90,7 @@ class Mininterface(Generic[EnvClass]):
|
|
|
90
90
|
# NOTE docs that
|
|
91
91
|
# m = run([Env, Env2]) -> .env will be the chosen one.
|
|
92
92
|
|
|
93
|
+
# NOTE In Python3.14, type(self).__annotations__ will work.
|
|
93
94
|
self._adaptor = self.__annotations__["_adaptor"](self, settings)
|
|
94
95
|
|
|
95
96
|
def __enter__(self) -> "Self":
|
|
@@ -164,8 +165,13 @@ class Mininterface(Generic[EnvClass]):
|
|
|
164
165
|
def __exit__(self, *_):
|
|
165
166
|
pass
|
|
166
167
|
|
|
167
|
-
def alert(self, text: str) -> None:
|
|
168
|
-
"""Prompt the user to confirm the text.
|
|
168
|
+
def alert(self, text: str, *, timeout: int = 0) -> None:
|
|
169
|
+
"""Prompt the user to confirm the text.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
text: Displayed text
|
|
173
|
+
timeout: Auto-confirm after N seconds (0 = disabled).
|
|
174
|
+
"""
|
|
169
175
|
print("Alert text", text)
|
|
170
176
|
return
|
|
171
177
|
|
|
@@ -255,7 +261,7 @@ class Mininterface(Generic[EnvClass]):
|
|
|
255
261
|
# Output:
|
|
256
262
|
return assure_tag(annotation, validation)._make_default_value()
|
|
257
263
|
|
|
258
|
-
def confirm(self, text: str, default: bool = True) -> bool:
|
|
264
|
+
def confirm(self, text: str, default: bool = True, *, timeout: int = 0) -> bool:
|
|
259
265
|
"""Display confirm box and returns bool.
|
|
260
266
|
|
|
261
267
|
```python
|
|
@@ -265,9 +271,19 @@ class Mininterface(Generic[EnvClass]):
|
|
|
265
271
|
|
|
266
272
|

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

|
|
282
|
+
|
|
268
283
|
Args:
|
|
269
284
|
text: Displayed text.
|
|
270
285
|
default: Focus the button with this value.
|
|
286
|
+
timeout: Auto-confirm after N seconds (0 = disabled).
|
|
271
287
|
|
|
272
288
|
Returns:
|
|
273
289
|
bool: Whether the user has chosen the Yes button.
|
|
@@ -26,6 +26,7 @@ class BackendAdaptor(ABC):
|
|
|
26
26
|
|
|
27
27
|
def __init__(self, interface: "Mininterface", settings: UiSettings | None):
|
|
28
28
|
self.interface = interface
|
|
29
|
+
# NOTE self.__annotations__ will pose problem at Python3.14
|
|
29
30
|
self.facet = interface.facet = self.__annotations__["facet"](self, interface.env)
|
|
30
31
|
self.settings = settings or self.__annotations__["settings"]()
|
|
31
32
|
|
|
@@ -140,3 +141,18 @@ class MinAdaptor(BackendAdaptor):
|
|
|
140
141
|
raise SystemExit(validation_fails)
|
|
141
142
|
|
|
142
143
|
return form
|
|
144
|
+
|
|
145
|
+
class Timeout(ABC):
|
|
146
|
+
""" Auto-submit dialogs """
|
|
147
|
+
|
|
148
|
+
def __init__(self, timeout: int, adaptor: "BackendAdaptor"):
|
|
149
|
+
self.timeout = timeout
|
|
150
|
+
self.adaptor = adaptor
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def countdown(self):
|
|
154
|
+
...
|
|
155
|
+
|
|
156
|
+
@abstractmethod
|
|
157
|
+
def cancel(self):
|
|
158
|
+
...
|
|
@@ -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):
|