mininterface 0.7.3__tar.gz → 0.7.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mininterface-0.7.3 → mininterface-0.7.5}/PKG-INFO +12 -9
- {mininterface-0.7.3 → mininterface-0.7.5}/README.md +8 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/__init__.py +8 -8
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/auxiliary.py +5 -10
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/cli_parser.py +96 -42
- mininterface-0.7.5/mininterface/config.py +28 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/experimental.py +0 -4
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/facet.py +6 -2
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/interfaces.py +3 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/mininterface.py +1 -1
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/showcase.py +3 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/textual_adaptor.py +1 -8
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/textual_app.py +14 -1
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/utils.py +5 -5
- {mininterface-0.7.3 → mininterface-0.7.5}/pyproject.toml +4 -4
- {mininterface-0.7.3 → mininterface-0.7.5}/LICENSE +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/ValidationFail.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/__main__.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/exceptions.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/form_dict.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/redirectable.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/start.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/subcommands.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tag.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tag_factory.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/text_interface.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/__init__.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/textual_button_app.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/textual_facet.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/widgets.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/__init__.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/date_entry.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/external_fix.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/tk_facet.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/tk_window.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/type_stubs.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/types.py +0 -0
- {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/validators.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
4
4
|
Summary: A minimal access to GUI, TUI, CLI and config
|
|
5
|
-
Home-page: https://github.com/CZ-NIC/mininterface
|
|
6
5
|
License: GPL-3.0-or-later
|
|
7
6
|
Author: Edvard Rejthar
|
|
8
7
|
Author-email: edvard.rejthar@nic.cz
|
|
@@ -13,20 +12,16 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Provides-Extra: all
|
|
17
|
-
Provides-Extra: gui
|
|
18
|
-
Provides-Extra: img
|
|
19
|
-
Provides-Extra: tui
|
|
20
|
-
Provides-Extra: web
|
|
21
15
|
Requires-Dist: autocombobox (==1.4.2)
|
|
22
16
|
Requires-Dist: humanize
|
|
23
17
|
Requires-Dist: pyyaml
|
|
24
|
-
Requires-Dist: textual (
|
|
18
|
+
Requires-Dist: textual (<2.0.0)
|
|
25
19
|
Requires-Dist: tkinter-tooltip
|
|
26
20
|
Requires-Dist: tkinter_form (==0.2.1)
|
|
27
21
|
Requires-Dist: tkscrollableframe
|
|
28
22
|
Requires-Dist: typing_extensions
|
|
29
|
-
Requires-Dist: tyro (
|
|
23
|
+
Requires-Dist: tyro (>=0.9,<0.10)
|
|
24
|
+
Project-URL: Homepage, https://github.com/CZ-NIC/mininterface
|
|
30
25
|
Description-Content-Type: text/markdown
|
|
31
26
|
|
|
32
27
|
# Mininterface – access to GUI, TUI, CLI and config files
|
|
@@ -70,6 +65,7 @@ if __name__ == "__main__":
|
|
|
70
65
|
- [Background](#background)
|
|
71
66
|
- [Installation](#installation)
|
|
72
67
|
- [Docs](#docs)
|
|
68
|
+
- [Gallery](#gallery)
|
|
73
69
|
- [Examples](#examples)
|
|
74
70
|
|
|
75
71
|
## You got CLI
|
|
@@ -152,6 +148,13 @@ If the GUI does not work on MacOS, you might need to launch: `brew install pytho
|
|
|
152
148
|
# Docs
|
|
153
149
|
See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic.github.io/mininterface/Overview/).
|
|
154
150
|
|
|
151
|
+
# Gallery
|
|
152
|
+
|
|
153
|
+
These projects have the code base reduced thanks to the mininterface:
|
|
154
|
+
|
|
155
|
+
* **[deduplidog](https://github.com/CZ-NIC/deduplidog/)** – Find duplicates in a scattered directory structure
|
|
156
|
+
* **[touch-timestamp](https://github.com/CZ-NIC/touch-timestamp/)** – A powerful dialog to change the files' timestamp
|
|
157
|
+
|
|
155
158
|
# Examples
|
|
156
159
|
|
|
157
160
|
A powerful [`m.form`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.form) dialog method accepts either a dataclass or a dict. Take a look on both.
|
|
@@ -39,6 +39,7 @@ if __name__ == "__main__":
|
|
|
39
39
|
- [Background](#background)
|
|
40
40
|
- [Installation](#installation)
|
|
41
41
|
- [Docs](#docs)
|
|
42
|
+
- [Gallery](#gallery)
|
|
42
43
|
- [Examples](#examples)
|
|
43
44
|
|
|
44
45
|
## You got CLI
|
|
@@ -121,6 +122,13 @@ If the GUI does not work on MacOS, you might need to launch: `brew install pytho
|
|
|
121
122
|
# Docs
|
|
122
123
|
See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic.github.io/mininterface/Overview/).
|
|
123
124
|
|
|
125
|
+
# Gallery
|
|
126
|
+
|
|
127
|
+
These projects have the code base reduced thanks to the mininterface:
|
|
128
|
+
|
|
129
|
+
* **[deduplidog](https://github.com/CZ-NIC/deduplidog/)** – Find duplicates in a scattered directory structure
|
|
130
|
+
* **[touch-timestamp](https://github.com/CZ-NIC/touch-timestamp/)** – A powerful dialog to change the files' timestamp
|
|
131
|
+
|
|
124
132
|
# Examples
|
|
125
133
|
|
|
126
134
|
A powerful [`m.form`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.form) dialog method accepts either a dataclass or a dict. Take a look on both.
|
|
@@ -8,7 +8,7 @@ from .exceptions import Cancelled, InterfaceNotAvailable
|
|
|
8
8
|
from .interfaces import get_interface
|
|
9
9
|
|
|
10
10
|
from . import validators
|
|
11
|
-
from .cli_parser import
|
|
11
|
+
from .cli_parser import parse_cli, assure_args
|
|
12
12
|
from .subcommands import Command, SubcommandPlaceholder
|
|
13
13
|
from .form_dict import DataClass, EnvClass
|
|
14
14
|
from .mininterface import EnvClass, Mininterface
|
|
@@ -183,22 +183,22 @@ def run(env_or_list: Type[EnvClass] | list[Type[Command]] | None = None,
|
|
|
183
183
|
start.choose_subcommand(env_or_list)
|
|
184
184
|
elif env_or_list:
|
|
185
185
|
# Load configuration from CLI and a config file
|
|
186
|
-
env, wrong_fields =
|
|
186
|
+
env, wrong_fields = parse_cli(env_or_list, config_file, add_verbosity, ask_for_missing, args, **kwargs)
|
|
187
187
|
else: # even though there is no configuration, yet we need to parse CLI for meta-commands like --help or --verbose
|
|
188
|
-
|
|
188
|
+
parse_cli(_Empty, None, add_verbosity, ask_for_missing, args)
|
|
189
189
|
|
|
190
190
|
# Build the interface
|
|
191
|
-
|
|
191
|
+
m = get_interface(title, interface, env)
|
|
192
192
|
|
|
193
193
|
# Empty CLI → GUI edit
|
|
194
194
|
if ask_for_missing and wrong_fields:
|
|
195
195
|
# Some fields must be set.
|
|
196
|
-
|
|
197
|
-
{setattr(
|
|
196
|
+
m.form(wrong_fields)
|
|
197
|
+
{setattr(m.env, k, v.val) for k, v in wrong_fields.items()}
|
|
198
198
|
elif ask_on_empty_cli and len(sys.argv) <= 1:
|
|
199
|
-
|
|
199
|
+
m.form()
|
|
200
200
|
|
|
201
|
-
return
|
|
201
|
+
return m
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
__all__ = ["run", "Tag", "validators", "InterfaceNotAvailable", "Cancelled",
|
|
@@ -25,6 +25,7 @@ def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]
|
|
|
25
25
|
yield v
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
# NOTE: Not used.
|
|
28
29
|
def flatten_keys(d: dict[KT, T | dict]) -> Iterable[tuple[KT, T]]:
|
|
29
30
|
""" Recursively traverse whole dict """
|
|
30
31
|
for k, v in d.items():
|
|
@@ -70,15 +71,6 @@ def yield_annotations(dataclass):
|
|
|
70
71
|
yield from (cl.__annotations__ for cl in dataclass.__mro__ if is_dataclass(cl))
|
|
71
72
|
|
|
72
73
|
|
|
73
|
-
def yield_defaults(dataclass):
|
|
74
|
-
""" Return tuple(name, type, default value or MISSING).
|
|
75
|
-
(Default factory is automatically resolved.)
|
|
76
|
-
"""
|
|
77
|
-
return ((f.name,
|
|
78
|
-
f.default_factory() if f.default_factory is not MISSING else f.default)
|
|
79
|
-
for f in fields(dataclass))
|
|
80
|
-
|
|
81
|
-
|
|
82
74
|
def matches_annotation(value, annotation) -> bool:
|
|
83
75
|
""" Check whether the value type corresponds to the annotation.
|
|
84
76
|
Because built-in isinstance is not enough, it cannot determine parametrized generics.
|
|
@@ -138,7 +130,10 @@ def subclass_matches_annotation(cls, annotation) -> bool:
|
|
|
138
130
|
return True
|
|
139
131
|
|
|
140
132
|
# simple types like scalars
|
|
141
|
-
|
|
133
|
+
try:
|
|
134
|
+
return issubclass(cls, annotation) # cls=tuple[int, str] raises an error since Python 3.13
|
|
135
|
+
except TypeError:
|
|
136
|
+
return False
|
|
142
137
|
|
|
143
138
|
|
|
144
139
|
def serialize_structure(obj):
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
import warnings
|
|
7
7
|
from argparse import Action, ArgumentParser
|
|
8
8
|
from contextlib import ExitStack
|
|
9
|
-
from dataclasses import MISSING
|
|
9
|
+
from dataclasses import MISSING, fields, is_dataclass
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from types import SimpleNamespace
|
|
12
12
|
from typing import Optional, Sequence, Type, Union
|
|
@@ -15,12 +15,12 @@ from unittest.mock import patch
|
|
|
15
15
|
import yaml
|
|
16
16
|
from tyro import cli
|
|
17
17
|
from tyro._argparse_formatter import TyroArgumentParser
|
|
18
|
-
from tyro.
|
|
19
|
-
# NOTE in the future versions of tyro, include that way:
|
|
20
|
-
# from tyro._singleton import NonpropagatingMissingType
|
|
18
|
+
from tyro._singleton import MISSING_NONPROP
|
|
21
19
|
from tyro.extras import get_parser
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
|
|
22
|
+
from .auxiliary import yield_annotations
|
|
23
|
+
from .config import MininterfaceConfig, Config
|
|
24
24
|
from .form_dict import EnvClass, MissingTagValue
|
|
25
25
|
from .tag import Tag
|
|
26
26
|
from .tag_factory import tag_factory
|
|
@@ -137,8 +137,9 @@ def run_tyro_parser(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
137
137
|
with ExitStack() as stack:
|
|
138
138
|
[stack.enter_context(p) for p in patches] # apply just the chosen mocks
|
|
139
139
|
res = cli(type_form, args=args, **kwargs)
|
|
140
|
-
if
|
|
141
|
-
# NOTE tyro does not work if a required positional is missing tyro.cli()
|
|
140
|
+
if res is MISSING_NONPROP:
|
|
141
|
+
# NOTE tyro does not work if a required positional is missing tyro.cli()
|
|
142
|
+
# returns just NonpropagatingMissingType (MISSING_NONPROP).
|
|
142
143
|
# If this is supported, I might set other attributes like required (date, time).
|
|
143
144
|
# Fail if missing:
|
|
144
145
|
# files: Positional[list[Path]]
|
|
@@ -217,12 +218,12 @@ def set_default(kwargs, field_name, val):
|
|
|
217
218
|
setattr(kwargs["default"], field_name, val)
|
|
218
219
|
|
|
219
220
|
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
222
|
+
config_file: Path | None = None,
|
|
223
|
+
add_verbosity=True,
|
|
224
|
+
ask_for_missing=True,
|
|
225
|
+
args=None,
|
|
226
|
+
**kwargs) -> tuple[EnvClass | None, dict, WrongFields]:
|
|
226
227
|
""" Parse CLI arguments, possibly merged from a config file.
|
|
227
228
|
|
|
228
229
|
Args:
|
|
@@ -243,41 +244,94 @@ def _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
243
244
|
# Load config file
|
|
244
245
|
if config_file and subcommands:
|
|
245
246
|
# Reading config files when using subcommands is not implemented.
|
|
246
|
-
static = {}
|
|
247
247
|
kwargs["default"] = None
|
|
248
248
|
warnings.warn(f"Config file {config_file} is ignored because subcommands are used."
|
|
249
249
|
" It is not easy to set how this should work."
|
|
250
250
|
" Describe the developer your usecase so that they might implement this.")
|
|
251
|
-
|
|
251
|
+
|
|
252
|
+
if "default" not in kwargs and not subcommands and config_file:
|
|
252
253
|
# Undocumented feature. User put a namespace into kwargs["default"]
|
|
253
254
|
# that already serves for defaults. We do not fetch defaults yet from a config file.
|
|
254
|
-
disk = {}
|
|
255
|
-
if
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
# Fill default fields
|
|
262
|
-
if pydantic and issubclass(env, BaseModel):
|
|
263
|
-
# Unfortunately, pydantic needs to fill the default with the actual values,
|
|
264
|
-
# the default value takes the precedence over the hard coded one, even if missing.
|
|
265
|
-
static = {key: env.model_fields.get(key).default
|
|
266
|
-
for ann in yield_annotations(env) for key in ann if not key.startswith("__") and not key in disk}
|
|
267
|
-
# static = {key: env_.model_fields.get(key).default
|
|
268
|
-
# for key, _ in iterate_attributes(env_) if not key in disk}
|
|
269
|
-
elif attr and attr.has(env):
|
|
270
|
-
# Unfortunately, attrs needs to fill the default with the actual values,
|
|
271
|
-
# the default value takes the precedence over the hard coded one, even if missing.
|
|
272
|
-
# NOTE Might not work for inherited models.
|
|
273
|
-
static = {key: field.default
|
|
274
|
-
for key, field in attr.fields_dict(env).items() if not key.startswith("__") and not key in disk}
|
|
275
|
-
else:
|
|
276
|
-
# To ensure the configuration file does not need to contain all keys, we have to fill in the missing ones.
|
|
277
|
-
# Otherwise, tyro will spawn warnings about missing fields.
|
|
278
|
-
static = {key: val
|
|
279
|
-
for key, val in yield_defaults(env) if not key.startswith("__") and not key in disk}
|
|
280
|
-
kwargs["default"] = SimpleNamespace(**(static | disk))
|
|
255
|
+
disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
|
|
256
|
+
if mininterface := disk.pop("mininterface", None):
|
|
257
|
+
# Section 'mininterface' in the config file changes the global configuration.
|
|
258
|
+
for key, value in vars(_create_with_missing(MininterfaceConfig, mininterface)).items():
|
|
259
|
+
setattr(Config, key, value)
|
|
260
|
+
kwargs["default"] = _create_with_missing(env, disk)
|
|
281
261
|
|
|
282
262
|
# Load configuration from CLI
|
|
283
263
|
return run_tyro_parser(subcommands or env, kwargs, add_verbosity, ask_for_missing, args)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _create_with_missing(env, disk: dict):
|
|
267
|
+
"""
|
|
268
|
+
Create a default instance of an Env object. This is due to provent tyro to spawn warnings about missing fields.
|
|
269
|
+
Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
# Determine model
|
|
273
|
+
if pydantic and issubclass(env, BaseModel):
|
|
274
|
+
m = _process_pydantic
|
|
275
|
+
elif attr and attr.has(env):
|
|
276
|
+
m = _process_attr
|
|
277
|
+
else: # dataclass
|
|
278
|
+
m = _process_dataclass
|
|
279
|
+
|
|
280
|
+
# Fill default fields with the config file values or leave the defaults.
|
|
281
|
+
# Unfortunately, we have to fill the defaults, we cannot leave them empty
|
|
282
|
+
# as the default value takes the precedence over the hard coded one, even if missing.
|
|
283
|
+
out = {}
|
|
284
|
+
for name, v in m(env, disk):
|
|
285
|
+
out[name] = v
|
|
286
|
+
disk.pop(name, None)
|
|
287
|
+
|
|
288
|
+
# Check for unknown fields
|
|
289
|
+
if disk:
|
|
290
|
+
warnings.warn(f"Unknown fields in the configuration file: {', '.join(disk)}")
|
|
291
|
+
|
|
292
|
+
# Safely initialize the model
|
|
293
|
+
return env(**out)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _process_pydantic(env, disk):
|
|
297
|
+
for name, f in env.model_fields.items():
|
|
298
|
+
if name in disk:
|
|
299
|
+
if isinstance(f.default, BaseModel):
|
|
300
|
+
v = _create_with_missing(f.default.__class__, disk[name])
|
|
301
|
+
else:
|
|
302
|
+
v = disk[name]
|
|
303
|
+
elif f.default is not None:
|
|
304
|
+
v = f.default
|
|
305
|
+
yield name, v
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _process_attr(env, disk):
|
|
309
|
+
for f in attr.fields(env):
|
|
310
|
+
if f.name in disk:
|
|
311
|
+
if attr.has(f.default):
|
|
312
|
+
v = _create_with_missing(f.default.__class__, disk[f.name])
|
|
313
|
+
else:
|
|
314
|
+
v = disk[f.name]
|
|
315
|
+
elif f.default is not attr.NOTHING:
|
|
316
|
+
v = f.default
|
|
317
|
+
else:
|
|
318
|
+
v = MISSING_NONPROP
|
|
319
|
+
yield f.name, v
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _process_dataclass(env, disk):
|
|
323
|
+
for f in fields(env):
|
|
324
|
+
if f.name.startswith("__"):
|
|
325
|
+
continue
|
|
326
|
+
elif f.name in disk:
|
|
327
|
+
if is_dataclass(f.type):
|
|
328
|
+
v = _create_with_missing(f.type, disk[f.name])
|
|
329
|
+
else:
|
|
330
|
+
v = disk[f.name]
|
|
331
|
+
elif f.default_factory is not MISSING:
|
|
332
|
+
v = f.default_factory()
|
|
333
|
+
elif f.default is not MISSING:
|
|
334
|
+
v = f.default
|
|
335
|
+
else:
|
|
336
|
+
v = MISSING_NONPROP
|
|
337
|
+
yield f.name, v
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Configuration used by all minterfaces in the program.
|
|
2
|
+
# Might be changed by a 'mininterface' section in a config file.
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Gui:
|
|
9
|
+
combobox_since: int = 5
|
|
10
|
+
""" The threshold to switch from radio buttons to a combobox. """
|
|
11
|
+
test: bool = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Tui:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass # (slots=True)
|
|
20
|
+
class MininterfaceConfig:
|
|
21
|
+
gui: Gui
|
|
22
|
+
tui: Tui
|
|
23
|
+
interface: Literal["gui"] | Literal["tui"] | None = None
|
|
24
|
+
""" Enforce an interface. By default, we choose automatically. """
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Config = MininterfaceConfig(Gui(), Tui())
|
|
28
|
+
""" Global configuration singleton to be accessed by all minterfaces. """
|
|
@@ -19,11 +19,14 @@ if TYPE_CHECKING:
|
|
|
19
19
|
|
|
20
20
|
@dataclass
|
|
21
21
|
class Image:
|
|
22
|
-
""" NOTE. Experimental.
|
|
22
|
+
""" NOTE. Experimental. """
|
|
23
|
+
|
|
23
24
|
src: str | Path
|
|
25
|
+
""" Src to the image. """
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
LayoutElement = TypeVar("LayoutElement", str, Image, Path, "Self")
|
|
29
|
+
""" Either a string, Path or facet.Image. """
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
class BackendAdaptor(ABC):
|
|
@@ -102,7 +105,7 @@ class Facet(Generic[EnvClass]):
|
|
|
102
105
|
print("Title", text)
|
|
103
106
|
|
|
104
107
|
def _layout(self, elements: list[LayoutElement]):
|
|
105
|
-
""" Experimental. """
|
|
108
|
+
""" Experimental. Input is a list of `LayoutElements`."""
|
|
106
109
|
# NOTE remove warn when working in textual
|
|
107
110
|
warn("Facet layout not implemented for this interface.")
|
|
108
111
|
|
|
@@ -120,6 +123,7 @@ class Facet(Generic[EnvClass]):
|
|
|
120
123
|
"My choice": Tag(choices=["one", "two"], on_change=callback)
|
|
121
124
|
})
|
|
122
125
|
# continue here immediately after clicking on a radio button
|
|
126
|
+
```
|
|
123
127
|
|
|
124
128
|
"""
|
|
125
129
|
self.adaptor.post_submit_action = _post_submit
|
|
@@ -3,6 +3,8 @@ from importlib import import_module
|
|
|
3
3
|
import sys
|
|
4
4
|
from typing import Literal, Type
|
|
5
5
|
|
|
6
|
+
|
|
7
|
+
from .config import Config
|
|
6
8
|
from .mininterface import Mininterface
|
|
7
9
|
from .exceptions import InterfaceNotAvailable
|
|
8
10
|
from .text_interface import TextInterface
|
|
@@ -38,6 +40,7 @@ def __getattr__(name):
|
|
|
38
40
|
|
|
39
41
|
def get_interface(title="", interface: InterfaceType = None, env=None):
|
|
40
42
|
args = title, env
|
|
43
|
+
interface = interface or Config.interface
|
|
41
44
|
if isinstance(interface, type) and issubclass(interface, Mininterface):
|
|
42
45
|
# the user gave a specific interface, let them catch InterfaceNotAvailable then
|
|
43
46
|
return interface(*args)
|
|
@@ -115,7 +115,7 @@ class Mininterface(Generic[EnvClass]):
|
|
|
115
115
|
|
|
116
116
|

|
|
117
117
|
|
|
118
|
-
However, when run in a non-interactive session with TUI (ex. no display), [TextInterface](Interfaces.md#
|
|
118
|
+
However, when run in a non-interactive session with TUI (ex. no display), [TextInterface](Interfaces.md#textinterface)
|
|
119
119
|
is used which is able to turn it into an interactive one.
|
|
120
120
|
|
|
121
121
|
```python3
|
|
@@ -26,15 +26,18 @@ class Subcommand1(SharedArgs):
|
|
|
26
26
|
my_local: int = 1
|
|
27
27
|
|
|
28
28
|
def run(self):
|
|
29
|
+
print("Subcommand 1 clicked")
|
|
29
30
|
print("Common:", self.common) # user input
|
|
30
31
|
print("Number:", self.my_local) # 1 or user input
|
|
31
32
|
print("Internal:", self.internal)
|
|
33
|
+
print("The submit button blocked!")
|
|
32
34
|
raise ValidationFail("The submit button blocked!")
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
@dataclass
|
|
36
38
|
class Subcommand2(SharedArgs):
|
|
37
39
|
def run(self):
|
|
40
|
+
print("Subcommand 2 clicked")
|
|
38
41
|
self._facet.set_title("Button clicked") # you can access internal self._facet: Facet
|
|
39
42
|
print("Common files", self.files)
|
|
40
43
|
|
|
@@ -82,18 +82,11 @@ class TextualAdaptor(BackendAdaptor):
|
|
|
82
82
|
if title:
|
|
83
83
|
app.title = title
|
|
84
84
|
|
|
85
|
-
widgets: WidgetList = [f for f in flatten(formdict_to_widgetdict(
|
|
86
|
-
form, self.widgetize), include_keys=self.header)]
|
|
87
|
-
if len(widgets) and isinstance(widgets[0], Rule):
|
|
88
|
-
# there are multiple sections in the list, <hr>ed by Rule elements. However, the first takes much space.
|
|
89
|
-
widgets.pop(0)
|
|
90
|
-
app.widgets = widgets
|
|
91
|
-
|
|
92
85
|
if not app.run():
|
|
93
86
|
raise Cancelled
|
|
94
87
|
|
|
95
88
|
# validate and store the UI value → Tag value → original value
|
|
96
|
-
vals = ((field._link, field.get_ui_value()) for field in widgets if hasattr(field, "_link"))
|
|
89
|
+
vals = ((field._link, field.get_ui_value()) for field in app.widgets if hasattr(field, "_link"))
|
|
97
90
|
if not Tag._submit_values(vals) or not self.submit_done():
|
|
98
91
|
return self.run_dialog(form, title, submit)
|
|
99
92
|
|
|
@@ -5,12 +5,15 @@ from textual.binding import Binding
|
|
|
5
5
|
from textual.containers import VerticalScroll
|
|
6
6
|
from textual.widget import Widget
|
|
7
7
|
from textual.widgets import (Checkbox, Footer, Header, Input, Label,
|
|
8
|
-
RadioButton, Static)
|
|
8
|
+
RadioButton, Static, Rule)
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
from .widgets import (Changeable, MyButton, MyCheckbox, MyInput, MyRadioSet,
|
|
12
12
|
MySubmitButton)
|
|
13
13
|
|
|
14
|
+
from ..form_dict import formdict_to_widgetdict
|
|
15
|
+
|
|
16
|
+
from ..auxiliary import flatten
|
|
14
17
|
from ..facet import BackendAdaptor
|
|
15
18
|
|
|
16
19
|
if TYPE_CHECKING:
|
|
@@ -52,6 +55,16 @@ class TextualApp(App[bool | None]):
|
|
|
52
55
|
self.bind("escape", "exit", description="Cancel")
|
|
53
56
|
|
|
54
57
|
def compose(self) -> ComposeResult:
|
|
58
|
+
# prepare widgets
|
|
59
|
+
# since textual 1.0.0 we have to build widgets not earlier than the context app is ready
|
|
60
|
+
self.widgets = list(flatten(formdict_to_widgetdict(
|
|
61
|
+
self.adaptor.facet._form, self.adaptor.widgetize), include_keys=self.adaptor.header))
|
|
62
|
+
|
|
63
|
+
# there are multiple sections in the list, <hr>ed by Rule elements. However, the first takes much space.
|
|
64
|
+
if len(self.widgets) and isinstance(self.widgets[0], Rule):
|
|
65
|
+
self.widgets.pop(0)
|
|
66
|
+
|
|
67
|
+
# start yielding widgets
|
|
55
68
|
if self.title:
|
|
56
69
|
yield Header()
|
|
57
70
|
yield self.output # NOTE not used
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
from tkinter import Button, Entry, Label, TclError, Variable, Widget, Spinbox
|
|
1
|
+
from tkinter import Button, Entry, TclError, Variable, Widget, Spinbox
|
|
3
2
|
from tkinter.filedialog import askopenfilename, askopenfilenames
|
|
4
3
|
from tkinter.ttk import Checkbutton, Combobox, Frame, Radiobutton, Widget
|
|
5
4
|
from typing import TYPE_CHECKING
|
|
@@ -8,8 +7,9 @@ from autocombobox import AutoCombobox
|
|
|
8
7
|
|
|
9
8
|
from tkinter_form.tkinter_form import Form, FieldForm
|
|
10
9
|
|
|
11
|
-
from ..auxiliary import flatten
|
|
12
|
-
from ..
|
|
10
|
+
from ..auxiliary import flatten
|
|
11
|
+
from ..config import Config
|
|
12
|
+
from ..experimental import FacetCallback, SubmitButton
|
|
13
13
|
from ..form_dict import TagDict
|
|
14
14
|
from ..tag import Tag
|
|
15
15
|
from ..types import DatetimeTag, PathTag
|
|
@@ -113,7 +113,7 @@ def replace_widgets(tk_app: "TkWindow", nested_widgets, form: TagDict):
|
|
|
113
113
|
nested_frame = Frame(master)
|
|
114
114
|
nested_frame.grid(row=grid_info['row'], column=grid_info['column'])
|
|
115
115
|
|
|
116
|
-
if len(tag._get_choices())
|
|
116
|
+
if len(tag._get_choices()) >= Config.gui.combobox_since:
|
|
117
117
|
widget = AutoCombobox(nested_frame, textvariable=variable)
|
|
118
118
|
widget['values'] = list(tag._get_choices())
|
|
119
119
|
widget.pack()
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "mininterface"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.5"
|
|
8
8
|
description = "A minimal access to GUI, TUI, CLI and config"
|
|
9
9
|
authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
@@ -14,18 +14,18 @@ readme = "README.md"
|
|
|
14
14
|
[tool.poetry.dependencies]
|
|
15
15
|
# Minimal requirements
|
|
16
16
|
python = "^3.10"
|
|
17
|
-
tyro = "0.
|
|
17
|
+
tyro = "^0.9"
|
|
18
18
|
typing_extensions = "*"
|
|
19
19
|
pyyaml = "*"
|
|
20
20
|
# Standard requirements
|
|
21
21
|
autocombobox = "1.4.2"
|
|
22
22
|
humanize = "*" # used only in the TkInterface, hence it is not a minimal requirement
|
|
23
|
-
textual = "
|
|
23
|
+
textual = "<2.0.0"
|
|
24
24
|
tkinter-tooltip = "*"
|
|
25
25
|
tkinter_form = "0.2.1"
|
|
26
26
|
tkscrollableframe = "*"
|
|
27
27
|
|
|
28
|
-
[tool.poetry.
|
|
28
|
+
[tool.poetry.project.optional-dependencies]
|
|
29
29
|
web = ["textual-serve"]
|
|
30
30
|
img = ["pillow", "textual_imageview"]
|
|
31
31
|
tui = ["textual_imageview"]
|
|
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-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/textual_button_app.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mininterface-0.7.3 → mininterface-0.7.5}/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
|