mininterface 0.7.2__tar.gz → 0.7.4__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.2 → mininterface-0.7.4}/PKG-INFO +8 -7
- {mininterface-0.7.2 → mininterface-0.7.4}/README.md +4 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/__init__.py +3 -3
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/auxiliary.py +0 -9
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/cli_parser.py +112 -53
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/textual_adaptor.py +5 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/textual_app.py +9 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/textual_button_app.py +3 -0
- mininterface-0.7.4/mininterface/textual_interface/textual_facet.py +64 -0
- mininterface-0.7.4/mininterface/tk_interface/external_fix.py +74 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/tk_facet.py +7 -4
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/utils.py +1 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/types.py +3 -10
- {mininterface-0.7.2 → mininterface-0.7.4}/pyproject.toml +6 -4
- mininterface-0.7.2/mininterface/textual_interface/textual_facet.py +0 -31
- {mininterface-0.7.2 → mininterface-0.7.4}/LICENSE +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/ValidationFail.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/__main__.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/exceptions.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/experimental.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/facet.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/form_dict.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/interfaces.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/mininterface.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/redirectable.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/showcase.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/start.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/subcommands.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tag.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tag_factory.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/text_interface.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/__init__.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/widgets.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/__init__.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/date_entry.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/tk_window.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/type_stubs.py +0 -0
- {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/validators.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
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,9 +12,6 @@ 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: web
|
|
19
15
|
Requires-Dist: autocombobox (==1.4.2)
|
|
20
16
|
Requires-Dist: humanize
|
|
21
17
|
Requires-Dist: pyyaml
|
|
@@ -24,7 +20,8 @@ Requires-Dist: tkinter-tooltip
|
|
|
24
20
|
Requires-Dist: tkinter_form (==0.2.1)
|
|
25
21
|
Requires-Dist: tkscrollableframe
|
|
26
22
|
Requires-Dist: typing_extensions
|
|
27
|
-
Requires-Dist: tyro (
|
|
23
|
+
Requires-Dist: tyro (>=0.9,<0.10)
|
|
24
|
+
Project-URL: Homepage, https://github.com/CZ-NIC/mininterface
|
|
28
25
|
Description-Content-Type: text/markdown
|
|
29
26
|
|
|
30
27
|
# Mininterface – access to GUI, TUI, CLI and config files
|
|
@@ -143,6 +140,10 @@ pip install --no-dependencies mininterface
|
|
|
143
140
|
pip install tyro typing_extensions pyyaml
|
|
144
141
|
```
|
|
145
142
|
|
|
143
|
+
## MacOS GUI
|
|
144
|
+
|
|
145
|
+
If the GUI does not work on MacOS, you might need to launch: `brew install python-tk`
|
|
146
|
+
|
|
146
147
|
# Docs
|
|
147
148
|
See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic.github.io/mininterface/Overview/).
|
|
148
149
|
|
|
@@ -114,6 +114,10 @@ pip install --no-dependencies mininterface
|
|
|
114
114
|
pip install tyro typing_extensions pyyaml
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
## MacOS GUI
|
|
118
|
+
|
|
119
|
+
If the GUI does not work on MacOS, you might need to launch: `brew install python-tk`
|
|
120
|
+
|
|
117
121
|
# Docs
|
|
118
122
|
See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic.github.io/mininterface/Overview/).
|
|
119
123
|
|
|
@@ -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,9 +183,9 @@ 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
|
interface = get_interface(title, interface, env)
|
|
@@ -70,15 +70,6 @@ def yield_annotations(dataclass):
|
|
|
70
70
|
yield from (cl.__annotations__ for cl in dataclass.__mro__ if is_dataclass(cl))
|
|
71
71
|
|
|
72
72
|
|
|
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
73
|
def matches_annotation(value, annotation) -> bool:
|
|
83
74
|
""" Check whether the value type corresponds to the annotation.
|
|
84
75
|
Because built-in isinstance is not enough, it cannot determine parametrized generics.
|
|
@@ -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,10 @@ 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
|
-
from .auxiliary import yield_annotations
|
|
21
|
+
from .auxiliary import yield_annotations
|
|
24
22
|
from .form_dict import EnvClass, MissingTagValue
|
|
25
23
|
from .tag import Tag
|
|
26
24
|
from .tag_factory import tag_factory
|
|
@@ -137,8 +135,9 @@ def run_tyro_parser(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
137
135
|
with ExitStack() as stack:
|
|
138
136
|
[stack.enter_context(p) for p in patches] # apply just the chosen mocks
|
|
139
137
|
res = cli(type_form, args=args, **kwargs)
|
|
140
|
-
if
|
|
141
|
-
# NOTE tyro does not work if a required positional is missing tyro.cli()
|
|
138
|
+
if res is MISSING_NONPROP:
|
|
139
|
+
# NOTE tyro does not work if a required positional is missing tyro.cli()
|
|
140
|
+
# returns just NonpropagatingMissingType (MISSING_NONPROP).
|
|
142
141
|
# If this is supported, I might set other attributes like required (date, time).
|
|
143
142
|
# Fail if missing:
|
|
144
143
|
# files: Positional[list[Path]]
|
|
@@ -150,7 +149,7 @@ def run_tyro_parser(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
150
149
|
if ask_for_missing and getattr(e, "code", None) == 2 and eavesdrop:
|
|
151
150
|
# Some required arguments are missing. Determine which.
|
|
152
151
|
wf = {}
|
|
153
|
-
for arg in
|
|
152
|
+
for arg in _fetch_eavesdrop_args():
|
|
154
153
|
treat_missing(type_form, kwargs, parser, wf, arg)
|
|
155
154
|
|
|
156
155
|
# Second attempt to parse CLI
|
|
@@ -195,7 +194,6 @@ def treat_missing(env_class, kwargs: dict, parser: ArgumentParser, wf: dict, arg
|
|
|
195
194
|
# However, the UI then is not able to use ex. the number filtering capabilities.
|
|
196
195
|
# Putting there None is not a good idea as dataclass_to_tagdict fails if None is not allowed by the annotation.
|
|
197
196
|
tag = wf[field_name] = tag_factory(MissingTagValue(),
|
|
198
|
-
# tag = wf[field_name] = tag_factory(MISSING,
|
|
199
197
|
argument.help.replace("(required)", ""),
|
|
200
198
|
validation=not_empty,
|
|
201
199
|
_src_class=env_class,
|
|
@@ -205,17 +203,25 @@ def treat_missing(env_class, kwargs: dict, parser: ArgumentParser, wf: dict, arg
|
|
|
205
203
|
# A None would be enough because Mininterface will ask for the missing values
|
|
206
204
|
# promply, however, Pydantic model would fail.
|
|
207
205
|
# As it serves only for tyro parsing and the field is marked wrong, the made up value is never used or seen.
|
|
208
|
-
|
|
209
|
-
kwargs["default"] = SimpleNamespace()
|
|
210
|
-
setattr(kwargs["default"], field_name, tag._make_default_value())
|
|
206
|
+
set_default(kwargs, field_name, tag._make_default_value())
|
|
211
207
|
|
|
212
208
|
|
|
213
|
-
def
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
def _fetch_eavesdrop_args():
|
|
210
|
+
return eavesdrop.partition(":")[2].strip().split(", ")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def set_default(kwargs, field_name, val):
|
|
214
|
+
if "default" not in kwargs:
|
|
215
|
+
kwargs["default"] = SimpleNamespace()
|
|
216
|
+
setattr(kwargs["default"], field_name, val)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
220
|
+
config_file: Path | None = None,
|
|
221
|
+
add_verbosity=True,
|
|
222
|
+
ask_for_missing=True,
|
|
223
|
+
args=None,
|
|
224
|
+
**kwargs) -> tuple[EnvClass | None, dict, WrongFields]:
|
|
219
225
|
""" Parse CLI arguments, possibly merged from a config file.
|
|
220
226
|
|
|
221
227
|
Args:
|
|
@@ -228,45 +234,98 @@ def _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
228
234
|
Returns:
|
|
229
235
|
Configuration namespace.
|
|
230
236
|
"""
|
|
237
|
+
if isinstance(env_or_list, list):
|
|
238
|
+
subcommands, env = env_or_list, None
|
|
239
|
+
else:
|
|
240
|
+
subcommands, env = None, env_or_list
|
|
241
|
+
|
|
231
242
|
# Load config file
|
|
232
|
-
if config_file and
|
|
233
|
-
#
|
|
234
|
-
static = {}
|
|
243
|
+
if config_file and subcommands:
|
|
244
|
+
# Reading config files when using subcommands is not implemented.
|
|
235
245
|
kwargs["default"] = None
|
|
236
246
|
warnings.warn(f"Config file {config_file} is ignored because subcommands are used."
|
|
237
|
-
"It is not easy to set
|
|
238
|
-
"Describe the developer your usecase so that they might implement this.")
|
|
239
|
-
|
|
247
|
+
" It is not easy to set how this should work."
|
|
248
|
+
" Describe the developer your usecase so that they might implement this.")
|
|
249
|
+
|
|
250
|
+
if "default" not in kwargs and not subcommands and config_file:
|
|
240
251
|
# Undocumented feature. User put a namespace into kwargs["default"]
|
|
241
252
|
# that already serves for defaults. We do not fetch defaults yet from a config file.
|
|
242
|
-
disk = {}
|
|
243
|
-
|
|
244
|
-
disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
|
|
245
|
-
# Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
|
|
246
|
-
for key in (key for key, val in disk.items() if isinstance(val, dict)):
|
|
247
|
-
disk[key] = env_or_list.__annotations__[key](**disk[key])
|
|
248
|
-
|
|
249
|
-
# Fill default fields
|
|
250
|
-
if pydantic and issubclass(env_or_list, BaseModel):
|
|
251
|
-
# Unfortunately, pydantic needs to fill the default with the actual values,
|
|
252
|
-
# the default value takes the precedence over the hard coded one, even if missing.
|
|
253
|
-
static = {key: env_or_list.model_fields.get(key).default
|
|
254
|
-
for ann in yield_annotations(env_or_list) for key in ann if not key.startswith("__") and not key in disk}
|
|
255
|
-
# static = {key: env_or_list.model_fields.get(key).default
|
|
256
|
-
# for key, _ in iterate_attributes(env_or_list) if not key in disk}
|
|
257
|
-
elif attr and attr.has(env_or_list):
|
|
258
|
-
# Unfortunately, attrs needs to fill the default with the actual values,
|
|
259
|
-
# the default value takes the precedence over the hard coded one, even if missing.
|
|
260
|
-
# NOTE Might not work for inherited models.
|
|
261
|
-
static = {key: field.default
|
|
262
|
-
for key, field in attr.fields_dict(env_or_list).items() if not key.startswith("__") and not key in disk}
|
|
263
|
-
else:
|
|
264
|
-
# To ensure the configuration file does not need to contain all keys, we have to fill in the missing ones.
|
|
265
|
-
# Otherwise, tyro will spawn warnings about missing fields.
|
|
266
|
-
static = {key: val
|
|
267
|
-
for key, val in yield_defaults(env_or_list) if not key.startswith("__") and not key in disk}
|
|
268
|
-
kwargs["default"] = SimpleNamespace(**(disk | static))
|
|
253
|
+
disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
|
|
254
|
+
kwargs["default"] = _create_with_missing(env, disk)
|
|
269
255
|
|
|
270
256
|
# Load configuration from CLI
|
|
271
|
-
|
|
272
|
-
|
|
257
|
+
return run_tyro_parser(subcommands or env, kwargs, add_verbosity, ask_for_missing, args)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _create_with_missing(env, disk: dict):
|
|
261
|
+
"""
|
|
262
|
+
Create a default instance of an Env object. This is due to provent tyro to spawn warnings about missing fields.
|
|
263
|
+
Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
# Determine model
|
|
267
|
+
if pydantic and issubclass(env, BaseModel):
|
|
268
|
+
m = _process_pydantic
|
|
269
|
+
elif attr and attr.has(env):
|
|
270
|
+
m = _process_attr
|
|
271
|
+
else: # dataclass
|
|
272
|
+
m = _process_dataclass
|
|
273
|
+
|
|
274
|
+
# Fill default fields with the config file values or leave the defaults.
|
|
275
|
+
# Unfortunately, we have to fill the defaults, we cannot leave them empty
|
|
276
|
+
# as the default value takes the precedence over the hard coded one, even if missing.
|
|
277
|
+
out = {}
|
|
278
|
+
for name, v in m(env, disk):
|
|
279
|
+
out[name] = v
|
|
280
|
+
disk.pop(name, None)
|
|
281
|
+
|
|
282
|
+
# Check for unknown fields
|
|
283
|
+
if disk:
|
|
284
|
+
warnings.warn(f"Unknown fields in the configuration file: {', '.join(disk)}")
|
|
285
|
+
|
|
286
|
+
# Safely initialize the model
|
|
287
|
+
return env(**out)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _process_pydantic(env, disk):
|
|
291
|
+
for name, f in env.model_fields.items():
|
|
292
|
+
if name in disk:
|
|
293
|
+
if isinstance(f.default, BaseModel):
|
|
294
|
+
v = _create_with_missing(f.default.__class__, disk[name])
|
|
295
|
+
else:
|
|
296
|
+
v = disk[name]
|
|
297
|
+
elif f.default is not None:
|
|
298
|
+
v = f.default
|
|
299
|
+
yield name, v
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _process_attr(env, disk):
|
|
303
|
+
for f in attr.fields(env):
|
|
304
|
+
if f.name in disk:
|
|
305
|
+
if attr.has(f.default):
|
|
306
|
+
v = _create_with_missing(f.default.__class__, disk[f.name])
|
|
307
|
+
else:
|
|
308
|
+
v = disk[f.name]
|
|
309
|
+
elif f.default is not attr.NOTHING:
|
|
310
|
+
v = f.default
|
|
311
|
+
else:
|
|
312
|
+
v = MISSING_NONPROP
|
|
313
|
+
yield f.name, v
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _process_dataclass(env, disk):
|
|
317
|
+
for f in fields(env):
|
|
318
|
+
if f.name.startswith("__"):
|
|
319
|
+
continue
|
|
320
|
+
elif f.name in disk:
|
|
321
|
+
if is_dataclass(f.type):
|
|
322
|
+
v = _create_with_missing(f.type, disk[f.name])
|
|
323
|
+
else:
|
|
324
|
+
v = disk[f.name]
|
|
325
|
+
elif f.default_factory is not MISSING:
|
|
326
|
+
v = f.default_factory()
|
|
327
|
+
elif f.default is not MISSING:
|
|
328
|
+
v = f.default
|
|
329
|
+
else:
|
|
330
|
+
v = MISSING_NONPROP
|
|
331
|
+
yield f.name, v
|
|
@@ -28,6 +28,8 @@ class TextualAdaptor(BackendAdaptor):
|
|
|
28
28
|
def __init__(self, interface: "TextualInterface"):
|
|
29
29
|
self.interface = interface
|
|
30
30
|
self.facet = interface.facet = TextualFacet(self, interface.env)
|
|
31
|
+
self.app: TextualApp | None = None
|
|
32
|
+
self.layout_elements = []
|
|
31
33
|
|
|
32
34
|
@staticmethod
|
|
33
35
|
def widgetize(tag: Tag) -> Widget | Changeable:
|
|
@@ -73,6 +75,9 @@ class TextualAdaptor(BackendAdaptor):
|
|
|
73
75
|
|
|
74
76
|
def run_dialog(self, form: TagDict, title: str = "", submit: bool | str = True) -> TagDict:
|
|
75
77
|
super().run_dialog(form, title, submit)
|
|
78
|
+
# Unfortunately, there seems to be no way to reuse the app.
|
|
79
|
+
# Which blocks using multiple form external .form() calls from the web interface.
|
|
80
|
+
# Textual cannot run in a thread, it seems it cannot run in another process, self.suspend() is of no use.
|
|
76
81
|
self.app = app = TextualApp(self, submit)
|
|
77
82
|
if title:
|
|
78
83
|
app.title = title
|
|
@@ -26,6 +26,14 @@ class TextualApp(App[bool | None]):
|
|
|
26
26
|
# ("down", "go_up", "Go down"),
|
|
27
27
|
# ]
|
|
28
28
|
|
|
29
|
+
DEFAULT_CSS = """
|
|
30
|
+
ImageViewer{
|
|
31
|
+
|
|
32
|
+
height: 20;
|
|
33
|
+
}
|
|
34
|
+
"""
|
|
35
|
+
""" Limit layout image size """
|
|
36
|
+
|
|
29
37
|
def __init__(self, adaptor: "TextualAdaptor", submit: str | bool = True):
|
|
30
38
|
super().__init__()
|
|
31
39
|
self.title = adaptor.facet._title
|
|
@@ -52,6 +60,7 @@ class TextualApp(App[bool | None]):
|
|
|
52
60
|
yield Label(text, id="buffered_text")
|
|
53
61
|
focus_set = False
|
|
54
62
|
with VerticalScroll():
|
|
63
|
+
yield from self.adaptor.layout_elements
|
|
55
64
|
for i, fieldt in enumerate(self.widgets):
|
|
56
65
|
if isinstance(fieldt, Input):
|
|
57
66
|
yield Label(fieldt.placeholder)
|
{mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/textual_button_app.py
RENAMED
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import TYPE_CHECKING, Any
|
|
3
3
|
|
|
4
4
|
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.containers import VerticalScroll
|
|
5
6
|
from textual.widgets import Button, Footer, Label
|
|
6
7
|
|
|
7
8
|
from ..exceptions import Cancelled
|
|
@@ -60,6 +61,7 @@ class TextualButtonApp(App):
|
|
|
60
61
|
self.focused_i: int = 0
|
|
61
62
|
self.values = {}
|
|
62
63
|
self.interface = interface
|
|
64
|
+
self.adaptor = self.interface.adaptor
|
|
63
65
|
|
|
64
66
|
def yes_no(self, text: str, focus_no=True) -> DummyWrapper:
|
|
65
67
|
return self.buttons(text, [("Yes", True), ("No", False)], int(focus_no))
|
|
@@ -78,6 +80,7 @@ class TextualButtonApp(App):
|
|
|
78
80
|
yield Footer()
|
|
79
81
|
if text := self.interface._redirected.join():
|
|
80
82
|
yield Label(text, id="buffered_text")
|
|
83
|
+
yield from self.adaptor.layout_elements
|
|
81
84
|
yield Label(self.text, id="question")
|
|
82
85
|
|
|
83
86
|
self.values.clear()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from warnings import warn
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from textual.widgets import (Checkbox, Footer, Header, Input, Label,
|
|
7
|
+
RadioButton, Static)
|
|
8
|
+
|
|
9
|
+
from humanize import naturalsize
|
|
10
|
+
|
|
11
|
+
from ..exceptions import DependencyRequired
|
|
12
|
+
from ..facet import Facet, Image, LayoutElement
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .textual_adaptor import TextualAdaptor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TextualFacet(Facet):
|
|
18
|
+
adaptor: "TextualAdaptor"
|
|
19
|
+
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
super().__init__(*args, **kwargs)
|
|
22
|
+
# Since TextualApp turns off, we need to have its values stored somewhere
|
|
23
|
+
self._title = ""
|
|
24
|
+
|
|
25
|
+
# NOTE: multiline title will not show up
|
|
26
|
+
def set_title(self, title: str):
|
|
27
|
+
self._title = title
|
|
28
|
+
try:
|
|
29
|
+
self.adaptor.app.title = title
|
|
30
|
+
except:
|
|
31
|
+
# NOTE: When you receive Facet in Command.init, the app does not exist yet
|
|
32
|
+
warn("Setting textual title not implemented well.")
|
|
33
|
+
|
|
34
|
+
def _layout(self, elements: list[LayoutElement]):
|
|
35
|
+
append = self.adaptor.layout_elements.append
|
|
36
|
+
try:
|
|
37
|
+
from PIL import Image as ImagePIL
|
|
38
|
+
from textual_imageview.viewer import ImageViewer
|
|
39
|
+
PIL = True
|
|
40
|
+
except:
|
|
41
|
+
PIL = False
|
|
42
|
+
|
|
43
|
+
for el in elements:
|
|
44
|
+
match el:
|
|
45
|
+
case Image():
|
|
46
|
+
if not PIL:
|
|
47
|
+
raise DependencyRequired("img")
|
|
48
|
+
append(ImageViewer(ImagePIL.open(el.src)))
|
|
49
|
+
case Path():
|
|
50
|
+
size = naturalsize(el.stat().st_size)
|
|
51
|
+
mtime = datetime.fromtimestamp(el.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
|
|
52
|
+
append(Label(f"{el} / {size} / {mtime}"))
|
|
53
|
+
case str():
|
|
54
|
+
append(Label(el))
|
|
55
|
+
case _:
|
|
56
|
+
append(Label("Error in the layout: Unknown {el}"))
|
|
57
|
+
|
|
58
|
+
def submit(self, *args, **kwargs):
|
|
59
|
+
super().submit(*args, **kwargs)
|
|
60
|
+
try:
|
|
61
|
+
self.adaptor.app.action_confirm()
|
|
62
|
+
except:
|
|
63
|
+
# NOTE: When you receive Facet in Command.init, the app does not exist yet
|
|
64
|
+
warn("Setting textual title not implemented well.")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# The purpose of the file is to put the descriptions to the bottom of the widgets
|
|
2
|
+
# as it was in the former version of the tkinter_form and to limit their width.
|
|
3
|
+
from tkinter import ttk
|
|
4
|
+
|
|
5
|
+
from tkinter_form import Form, Value, FieldForm
|
|
6
|
+
|
|
7
|
+
orig = Form._Form__create_widgets
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def __create_widgets_monkeypatched(
|
|
11
|
+
self, form_dict: dict, name_config: str, button_command: callable
|
|
12
|
+
) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Create form widgets
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
form_dict (dict): form dict base
|
|
18
|
+
name_config (str): name_config
|
|
19
|
+
button (bool): button_config
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
index = 0
|
|
23
|
+
for _, (name_key, value) in enumerate(form_dict.items()):
|
|
24
|
+
index += 1
|
|
25
|
+
description = None
|
|
26
|
+
if isinstance(value, Value):
|
|
27
|
+
value, description = value.val, value.description
|
|
28
|
+
|
|
29
|
+
self.rowconfigure(index, weight=1)
|
|
30
|
+
|
|
31
|
+
if isinstance(value, dict):
|
|
32
|
+
widget = Form(self, name_key, value)
|
|
33
|
+
widget.grid(row=index, column=0, columnspan=3, sticky="nesw")
|
|
34
|
+
|
|
35
|
+
self.fields[name_key] = widget
|
|
36
|
+
last_index = index
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
variable = self._Form__type_vars[type(value)]()
|
|
40
|
+
widget = self._Form__type_widgets[type(value)](self)
|
|
41
|
+
|
|
42
|
+
self.columnconfigure(1, weight=1)
|
|
43
|
+
widget.grid(row=index, column=1, sticky="nesw", padx=2, pady=2)
|
|
44
|
+
label = ttk.Label(self, text=name_key)
|
|
45
|
+
self.columnconfigure(0, weight=1)
|
|
46
|
+
label.grid(row=index, column=0, sticky="nes", padx=2, pady=2)
|
|
47
|
+
|
|
48
|
+
# Add a further description to the row below the widget
|
|
49
|
+
description_label = None
|
|
50
|
+
if not description is None:
|
|
51
|
+
index += 1
|
|
52
|
+
description_label = ttk.Label(self, text=description, wraplength=1000)
|
|
53
|
+
description_label.grid(row=index, column=1, columnspan=2, sticky="nesw", padx=2, pady=2)
|
|
54
|
+
|
|
55
|
+
self.fields[name_key] = FieldForm(
|
|
56
|
+
master=self,
|
|
57
|
+
label=label,
|
|
58
|
+
widget=widget,
|
|
59
|
+
variable=variable,
|
|
60
|
+
value=value,
|
|
61
|
+
description=description_label,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
last_index = index
|
|
65
|
+
|
|
66
|
+
if button_command:
|
|
67
|
+
self._Form__command = button_command
|
|
68
|
+
self.button = ttk.Button(
|
|
69
|
+
self, text=name_config, command=self._Form__command_button
|
|
70
|
+
)
|
|
71
|
+
self.button.grid(row=last_index + 1, column=0, columnspan=3, sticky="nesw")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
Form._Form__create_widgets = __create_widgets_monkeypatched
|
|
@@ -36,10 +36,13 @@ class TkFacet(Facet):
|
|
|
36
36
|
raise DependencyRequired("img")
|
|
37
37
|
filename = el.src
|
|
38
38
|
img = ImagePIL.open(filename)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
max_width, max_height = 250, 250
|
|
40
|
+
w_o, h_o = img.size
|
|
41
|
+
scale = min(max_width / w_o, max_height / h_o)
|
|
42
|
+
img = img.resize((int(w_o * scale), int(h_o * scale)), ImagePIL.LANCZOS)
|
|
43
|
+
img_p = ImageTk.PhotoImage(img)
|
|
44
|
+
panel = Label(self.adaptor.frame, image=img_p)
|
|
45
|
+
panel.image = img_p
|
|
43
46
|
panel.pack()
|
|
44
47
|
case Path():
|
|
45
48
|
size = naturalsize(el.stat().st_size)
|
|
@@ -159,10 +159,7 @@ class PathTag(Tag):
|
|
|
159
159
|
@dataclass(repr=False)
|
|
160
160
|
class DatetimeTag(Tag):
|
|
161
161
|
"""
|
|
162
|
-
|
|
163
|
-
Experimental. Still in development.
|
|
164
|
-
|
|
165
|
-
Datetime is supported.
|
|
162
|
+
Datetime, date and time types are supported.
|
|
166
163
|
|
|
167
164
|
```python3
|
|
168
165
|
from datetime import datetime
|
|
@@ -214,13 +211,12 @@ class DatetimeTag(Tag):
|
|
|
214
211
|
# 
|
|
215
212
|
|
|
216
213
|
# NOTE: It would be nice we might put any date format to be parsed.
|
|
217
|
-
# NOTE: The parameters are still ignored.
|
|
218
214
|
|
|
219
215
|
date: bool = False
|
|
220
|
-
""" The date part is active """
|
|
216
|
+
""" The date part is active. True for datetime and date. """
|
|
221
217
|
|
|
222
218
|
time: bool = False
|
|
223
|
-
""" The time part is active """
|
|
219
|
+
""" The time part is active. True for datetime and time. """
|
|
224
220
|
|
|
225
221
|
full_precision: bool = False
|
|
226
222
|
""" Include full time precison, seconds, microseconds. """
|
|
@@ -230,9 +226,6 @@ class DatetimeTag(Tag):
|
|
|
230
226
|
if self.annotation:
|
|
231
227
|
self.date = issubclass(self.annotation, date)
|
|
232
228
|
self.time = issubclass(self.annotation, time) or issubclass(self.annotation, datetime)
|
|
233
|
-
# NOTE: remove
|
|
234
|
-
# if not self.time and self.full_precision:
|
|
235
|
-
# self.full_precision = False
|
|
236
229
|
|
|
237
230
|
def _make_default_value(self):
|
|
238
231
|
return datetime.now()
|
|
@@ -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.4"
|
|
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,7 +14,7 @@ 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
|
|
@@ -25,10 +25,12 @@ 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
|
+
img = ["pillow", "textual_imageview"]
|
|
31
|
+
tui = ["textual_imageview"]
|
|
30
32
|
gui = ["pillow", "tkcalendar"]
|
|
31
|
-
all = ["textual-serve", "pillow", "tkcalendar"]
|
|
33
|
+
all = ["textual-serve", "pillow", "tkcalendar", "textual_imageview"]
|
|
32
34
|
|
|
33
35
|
[tool.poetry.scripts]
|
|
34
36
|
mininterface = "mininterface.__main__:main"
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING
|
|
2
|
-
from warnings import warn
|
|
3
|
-
from ..facet import Facet
|
|
4
|
-
if TYPE_CHECKING:
|
|
5
|
-
from .textual_adaptor import TextualAdaptor
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TextualFacet(Facet):
|
|
9
|
-
adaptor: "TextualAdaptor"
|
|
10
|
-
|
|
11
|
-
def __init__(self, *args, **kwargs):
|
|
12
|
-
super().__init__(*args, **kwargs)
|
|
13
|
-
# Since TextualApp turns off, we need to have its values stored somewhere
|
|
14
|
-
self._title = ""
|
|
15
|
-
|
|
16
|
-
# NOTE: multiline title will not show up
|
|
17
|
-
def set_title(self, title: str):
|
|
18
|
-
self._title = title
|
|
19
|
-
try:
|
|
20
|
-
self.adaptor.app.title = title
|
|
21
|
-
except:
|
|
22
|
-
# NOTE: When you receive Facet in Command.init, the app does not exist yet
|
|
23
|
-
warn("Setting textual title not implemented well.")
|
|
24
|
-
|
|
25
|
-
def submit(self, *args, **kwargs):
|
|
26
|
-
super().submit(*args, **kwargs)
|
|
27
|
-
try:
|
|
28
|
-
self.adaptor.app.action_confirm()
|
|
29
|
-
except:
|
|
30
|
-
# NOTE: When you receive Facet in Command.init, the app does not exist yet
|
|
31
|
-
warn("Setting textual title not implemented well.")
|
|
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-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/redirect_text_tkinter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|