mininterface 1.1.2__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.2 → mininterface-1.1.3}/PKG-INFO +2 -3
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/cli_parser.py +41 -8
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/config_file.py +6 -10
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/dataclass_creation.py +4 -1
- mininterface-1.1.3/mininterface/_lib/future_compatibility.py +9 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/run.py +26 -14
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/start.py +11 -11
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_mininterface/__init__.py +1 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_mininterface/adaptor.py +1 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_text_interface/timeout.py +6 -3
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/timeout.py +5 -1
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/timeout.py +4 -1
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/utils.py +4 -3
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/interfaces.py +5 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/settings.py +124 -2
- {mininterface-1.1.2 → mininterface-1.1.3}/pyproject.toml +3 -2
- mininterface-1.1.2/mininterface/_lib/future_compatibility.py +0 -6
- {mininterface-1.1.2 → mininterface-1.1.3}/LICENSE +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/README.md +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/__init__.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/__main__.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/argparse_support.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/auxiliary.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/cli_flags.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/cli_utils.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/form_dict.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/redirectable.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/shortcuts.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/showcase.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_lib/tyro_patches.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_mininterface/mixin.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_text_interface/__init__.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_text_interface/adaptor.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/__init__.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/adaptor.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/button_contents.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/facet.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/file_picker_input.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/form_contents.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/style.tcss +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/textual_app.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_textual_interface/widgets.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/__init__.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/adaptor.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/date_entry.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/external_fix.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/facet.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/secret_entry.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_tk_interface/select_input.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_web_interface/__init__.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_web_interface/app.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_web_interface/child_adaptor.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/_web_interface/parent_adaptor.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/cli.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/exceptions.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/experimental.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/facet/__init__.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/alias.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/callback_tag.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/datetime_tag.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/flag.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/internal.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/path_tag.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/secret_tag.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/select_tag.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/tag.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/tag_factory.py +0 -0
- {mininterface-1.1.2 → mininterface-1.1.3}/mininterface/tag/type_stubs.py +0 -0
- {mininterface-1.1.2 → 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
|
|
@@ -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])`
|
|
@@ -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:
|
|
@@ -237,8 +237,11 @@ def _process_field(fname, ftype, disk_value, wf, m, default_value=MISSING):
|
|
|
237
237
|
|
|
238
238
|
# the subcommand has been already chosen by CLI parser
|
|
239
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
|
+
|
|
240
243
|
for _subcomm in get_args(_unwrap_annotated(ftype)):
|
|
241
|
-
if
|
|
244
|
+
if name == to_kebab_case(_subcomm.__name__):
|
|
242
245
|
ftype = _subcomm # `class Message` only
|
|
243
246
|
disk_value = disk_value.subdict
|
|
244
247
|
break
|
|
@@ -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":
|
|
@@ -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
|
|
|
@@ -60,6 +60,8 @@ def input_timeout(prompt: str, timeout: int = 0, exit_on_keypress: bool = False)
|
|
|
60
60
|
return "".join(inp)
|
|
61
61
|
elif char == '\x03': # Ctrl+C
|
|
62
62
|
raise Cancelled
|
|
63
|
+
elif char == '\x1b': # Escape
|
|
64
|
+
raise Cancelled
|
|
63
65
|
elif char == '\x08': # Backspace
|
|
64
66
|
if inp:
|
|
65
67
|
inp.pop()
|
|
@@ -74,7 +76,6 @@ def input_timeout(prompt: str, timeout: int = 0, exit_on_keypress: bool = False)
|
|
|
74
76
|
print() # newline immediately
|
|
75
77
|
return "".join(inp)
|
|
76
78
|
|
|
77
|
-
# Stop if timeout exceeded and input not started
|
|
78
79
|
if timeout_running and (time.time() - start_time >= timeout) and not input_started.is_set():
|
|
79
80
|
input_started.set()
|
|
80
81
|
print()
|
|
@@ -93,7 +94,9 @@ def input_timeout(prompt: str, timeout: int = 0, exit_on_keypress: bool = False)
|
|
|
93
94
|
if char == '\n':
|
|
94
95
|
print() # newline at Enter
|
|
95
96
|
return "".join(inp)
|
|
96
|
-
elif char == '\x03':
|
|
97
|
+
elif char == '\x03': # Ctrl+C
|
|
98
|
+
raise Cancelled
|
|
99
|
+
elif char == '\x1b': # Escape
|
|
97
100
|
raise Cancelled
|
|
98
101
|
elif char == '\x7f': # Backspace
|
|
99
102
|
if inp:
|
|
@@ -109,13 +112,13 @@ def input_timeout(prompt: str, timeout: int = 0, exit_on_keypress: bool = False)
|
|
|
109
112
|
print() # newline immediately
|
|
110
113
|
return "".join(inp)
|
|
111
114
|
|
|
112
|
-
# Stop if timeout exceeded and input not started
|
|
113
115
|
if timeout_running and (time.time() - start_time >= timeout) and not input_started.is_set():
|
|
114
116
|
input_started.set()
|
|
115
117
|
print()
|
|
116
118
|
return ""
|
|
117
119
|
finally:
|
|
118
120
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
121
|
+
|
|
119
122
|
except Exception:
|
|
120
123
|
# Fallback to class input.
|
|
121
124
|
# 'Press any key' will not work.
|
|
@@ -16,7 +16,11 @@ class TextualTimeout(Timeout):
|
|
|
16
16
|
self.button = button
|
|
17
17
|
self.orig = self.button.label
|
|
18
18
|
self._task = asyncio.create_task(self.countdown(timeout))
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
# Cancel countdown on focusing out
|
|
21
|
+
# Why checking .focused? If we jump to another app with Alt+Tab, we do not want the countdown to stop
|
|
22
|
+
# (in such cases, .focused is empty).
|
|
23
|
+
self.button.set_blur_callback(lambda event=None: self.cancel() if adaptor.app.focused else None)
|
|
20
24
|
|
|
21
25
|
async def countdown(self, count: int):
|
|
22
26
|
self.button.label = f"{self.orig} ({count})"
|
|
@@ -18,7 +18,10 @@ class TkTimeout(Timeout):
|
|
|
18
18
|
|
|
19
19
|
self.countdown(timeout)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# Cancel countdown on FocusOut
|
|
22
|
+
# Why checking .focus_get()? If we jump to another app with Alt+Tab, we do not want the countdown to stop
|
|
23
|
+
# (in such cases, .focus_get() is empty).
|
|
24
|
+
self.button.bind("<FocusOut>", lambda e: self.cancel() if e.widget.focus_get() else None)
|
|
22
25
|
|
|
23
26
|
def countdown(self, count):
|
|
24
27
|
try:
|
|
@@ -18,7 +18,7 @@ from ..tag.internal import CallbackButtonWidget, FacetButtonWidget, SubmitButton
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
from .._lib.auxiliary import flatten
|
|
21
|
-
from .._lib.form_dict import TagDict
|
|
21
|
+
from .._lib.form_dict import TagDict, dict_removed_main
|
|
22
22
|
|
|
23
23
|
from ..tag import Tag
|
|
24
24
|
from ..tag.secret_tag import SecretTag
|
|
@@ -165,7 +165,8 @@ def replace_widgets(adaptor: "TkAdaptor", nested_widgets, form: TagDict):
|
|
|
165
165
|
# Only prevent form submission, don't affect Tab navigation
|
|
166
166
|
return None # Allow event propagation for other handlers
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
fform = list(flatten(form))
|
|
169
|
+
for tag, field_form in zip(fform, flatten(nested_widgets)):
|
|
169
170
|
tag: Tag
|
|
170
171
|
field_form: FieldForm
|
|
171
172
|
label1: Widget = field_form.label
|
|
@@ -193,7 +194,7 @@ def replace_widgets(adaptor: "TkAdaptor", nested_widgets, form: TagDict):
|
|
|
193
194
|
match tag:
|
|
194
195
|
case SelectTag():
|
|
195
196
|
grid_info = widget.grid_info()
|
|
196
|
-
wrapper = SelectInputWrapper(master, tag, grid_info, widget, adaptor, len(
|
|
197
|
+
wrapper = SelectInputWrapper(master, tag, grid_info, widget, adaptor, len(fform) == 1)
|
|
197
198
|
select_tag = True
|
|
198
199
|
variable = wrapper.variable_wrapper
|
|
199
200
|
# since tkinter variables do not allow objects,
|
|
@@ -65,6 +65,11 @@ def _choose_settings(type_: Mininterface, settings: Optional[MininterfaceSetting
|
|
|
65
65
|
opt = settings.text
|
|
66
66
|
case "WebInterface":
|
|
67
67
|
opt = settings.web
|
|
68
|
+
case "Mininterface":
|
|
69
|
+
# Even though this has not any sense to have some settings for Mininterface,
|
|
70
|
+
# they are not used, we test it in TestRun.test_settings_run.
|
|
71
|
+
# An according to MinAdaptor, it should have UiSettings.
|
|
72
|
+
opt = settings.ui
|
|
68
73
|
return opt
|
|
69
74
|
|
|
70
75
|
|
|
@@ -5,6 +5,117 @@ from typing import Literal, Optional
|
|
|
5
5
|
# We do not use InterfaceType as a type in run because we want the documentation to show full alias.
|
|
6
6
|
InterfaceName = Literal["gui"] | Literal["tui"] | Literal["textual"] | Literal["text"]
|
|
7
7
|
|
|
8
|
+
@_dataclass
|
|
9
|
+
class CliSettings:
|
|
10
|
+
omit_arg_prefixes: bool = False
|
|
11
|
+
""" Simplify argument names by removing parent field prefixes from flags.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
$ ./program.py --help
|
|
15
|
+
# omit_arg_prefixes = False
|
|
16
|
+
usage: program.py [-h] [-v] --arg.subarg.text STR
|
|
17
|
+
# omit_arg_prefixes = True
|
|
18
|
+
usage: program.py [-h] [-v] --text STR
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
??? Code
|
|
22
|
+
```python
|
|
23
|
+
@dataclass
|
|
24
|
+
class SubMessage:
|
|
25
|
+
text: str
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Message:
|
|
29
|
+
subarg: SubMessage
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Env:
|
|
33
|
+
arg: Message
|
|
34
|
+
|
|
35
|
+
run(Env, settings=CliSettings(omit_arg_prefixes=True/False))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See: [https://brentyi.github.io/tyro/api/tyro/conf/#tyro.conf.OmitArgPrefixes]()
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
omit_subcommand_prefixes: bool = False
|
|
42
|
+
"""
|
|
43
|
+
Simplify subcommand names by removing parent field prefixes from subcommands.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
$ ./program.py --help
|
|
47
|
+
# omit_subcommand_prefixes = False
|
|
48
|
+
usage: program.py [-h] [-v] {subcommand:message,subcommand:console}
|
|
49
|
+
# omit_subcommand_prefixes = True
|
|
50
|
+
usage: program.py [-h] [-v] {message,console}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
??? Code
|
|
54
|
+
```python
|
|
55
|
+
@dataclass
|
|
56
|
+
class SubMessage:
|
|
57
|
+
text: str
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Message:
|
|
61
|
+
subarg: SubMessage
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Env:
|
|
65
|
+
subcommand: Message | Console
|
|
66
|
+
|
|
67
|
+
run(Env, settings=CliSettings(omit_subcommand_prefixes=True/False))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
See: [https://brentyi.github.io/tyro/api/tyro/conf/#tyro.conf.OmitSubcommandPrefixes]()
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
disallow_none: bool = False
|
|
74
|
+
""" Disallow passing None in via the command-line interface for union types containing None.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
$ ./program.py --help
|
|
78
|
+
# disallow_none = False
|
|
79
|
+
usage: program.py [-h] [-v] [--field {None}|INT]
|
|
80
|
+
# disallow_none = True
|
|
81
|
+
usage: program.py [-h] [-v] [--field INT]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
??? Code
|
|
85
|
+
```python
|
|
86
|
+
@dataclass
|
|
87
|
+
class Env:
|
|
88
|
+
field: int | None = None
|
|
89
|
+
run(Env, settings=CliSettings(disallow_none=True/False))
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
See: [https://brentyi.github.io/tyro/api/tyro/conf/#tyro.conf.DisallowNone]()
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
flag_create_pairs_off: bool = False
|
|
96
|
+
""" Disable creation of matching flag pairs for boolean types.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
$ ./program.py --help
|
|
100
|
+
# flag_create_pairs_off = False
|
|
101
|
+
usage: program.py [-h] [-v] [--foo | --no-foo]
|
|
102
|
+
# flag_create_pairs_off = True
|
|
103
|
+
usage: program.py [-h] [-v] [--foo]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
??? Code
|
|
107
|
+
```python
|
|
108
|
+
@dataclass
|
|
109
|
+
class Env:
|
|
110
|
+
foo: bool = False
|
|
111
|
+
run(Env, settings=CliSettings(flag_create_pairs_off=True/False))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See: [https://brentyi.github.io/tyro/api/tyro/conf/#tyro.conf.FlagCreatePairsOff]()
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
8
119
|
@_dataclass
|
|
9
120
|
class UiSettings:
|
|
10
121
|
toggle_widget: str = "f4"
|
|
@@ -37,9 +148,19 @@ class UiSettings:
|
|
|
37
148
|
class GuiSettings(UiSettings):
|
|
38
149
|
# If multiple Gui interfaces exist, this had to be TkSettings instead.
|
|
39
150
|
|
|
40
|
-
|
|
41
151
|
combobox_since: int = 10
|
|
42
|
-
""" The threshold to switch from radio buttons to a combobox.
|
|
152
|
+
""" The threshold to switch from radio buttons to a combobox.
|
|
153
|
+
|
|
154
|
+
Without combobox:
|
|
155
|
+
|
|
156
|
+

|
|
157
|
+
|
|
158
|
+
With combobox:
|
|
159
|
+
|
|
160
|
+

|
|
161
|
+
|
|
162
|
+
(Note, there must be multiple fields for combobox to appear.)
|
|
163
|
+
"""
|
|
43
164
|
|
|
44
165
|
radio_select_on_focus: bool = False
|
|
45
166
|
""" Select the radio button on focus. Ex. when navigating by arrows. """
|
|
@@ -122,5 +243,6 @@ class MininterfaceSettings:
|
|
|
122
243
|
textual: TextualSettings = _field(default_factory=TextualSettings)
|
|
123
244
|
text: TextSettings = _field(default_factory=TextSettings)
|
|
124
245
|
web: WebSettings = _field(default_factory=WebSettings)
|
|
246
|
+
cli: CliSettings = _field(default_factory=CliSettings)
|
|
125
247
|
interface: Optional[InterfaceName] = None
|
|
126
248
|
""" Enforce an interface. By default, we choose automatically. """
|
|
@@ -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.3"
|
|
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"
|
|
@@ -13,7 +13,8 @@ readme = "README.md"
|
|
|
13
13
|
|
|
14
14
|
[tool.poetry.dependencies]
|
|
15
15
|
# Minimal requirements (dialogs with partial .form support (m.form(dict) or m.form(dataclass_instance))
|
|
16
|
-
python = "
|
|
16
|
+
python = ">=3.10,<3.14"
|
|
17
|
+
# python = "^3.10" NOTE when tyro accepts Python3.14, get this line back
|
|
17
18
|
simple_term_menu = "*"
|
|
18
19
|
annotated-types = "*"
|
|
19
20
|
# Basic requirements (CLI and full .form support (m.form(dataclass_class))
|
|
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.2 → mininterface-1.1.3}/mininterface/_textual_interface/button_contents.py
RENAMED
|
File without changes
|
|
File without changes
|
{mininterface-1.1.2 → mininterface-1.1.3}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mininterface-1.1.2 → mininterface-1.1.3}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|