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.
Files changed (39) hide show
  1. {mininterface-0.7.2 → mininterface-0.7.4}/PKG-INFO +8 -7
  2. {mininterface-0.7.2 → mininterface-0.7.4}/README.md +4 -0
  3. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/__init__.py +3 -3
  4. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/auxiliary.py +0 -9
  5. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/cli_parser.py +112 -53
  6. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/textual_adaptor.py +5 -0
  7. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/textual_app.py +9 -0
  8. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/textual_button_app.py +3 -0
  9. mininterface-0.7.4/mininterface/textual_interface/textual_facet.py +64 -0
  10. mininterface-0.7.4/mininterface/tk_interface/external_fix.py +74 -0
  11. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/tk_facet.py +7 -4
  12. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/utils.py +1 -0
  13. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/types.py +3 -10
  14. {mininterface-0.7.2 → mininterface-0.7.4}/pyproject.toml +6 -4
  15. mininterface-0.7.2/mininterface/textual_interface/textual_facet.py +0 -31
  16. {mininterface-0.7.2 → mininterface-0.7.4}/LICENSE +0 -0
  17. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/ValidationFail.py +0 -0
  18. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/__main__.py +0 -0
  19. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/exceptions.py +0 -0
  20. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/experimental.py +0 -0
  21. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/facet.py +0 -0
  22. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/form_dict.py +0 -0
  23. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/interfaces.py +0 -0
  24. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/mininterface.py +0 -0
  25. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/redirectable.py +0 -0
  26. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/showcase.py +0 -0
  27. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/start.py +0 -0
  28. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/subcommands.py +0 -0
  29. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tag.py +0 -0
  30. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tag_factory.py +0 -0
  31. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/text_interface.py +0 -0
  32. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/__init__.py +0 -0
  33. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/textual_interface/widgets.py +0 -0
  34. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/__init__.py +0 -0
  35. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/date_entry.py +0 -0
  36. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/redirect_text_tkinter.py +0 -0
  37. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/tk_interface/tk_window.py +0 -0
  38. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/type_stubs.py +0 -0
  39. {mininterface-0.7.2 → mininterface-0.7.4}/mininterface/validators.py +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: mininterface
3
- Version: 0.7.2
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 (==0.8.14)
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 _parse_cli, assure_args
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 = _parse_cli(env_or_list, config_file, add_verbosity, ask_for_missing, args, **kwargs)
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
- _parse_cli(_Empty, None, add_verbosity, ask_for_missing, args)
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._fields import NonpropagatingMissingType
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, yield_defaults
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 isinstance(res, NonpropagatingMissingType):
141
- # NOTE tyro does not work if a required positional is missing tyro.cli() returns just NonpropagatingMissingType.
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 eavesdrop.partition(":")[2].strip().split(", "):
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
- if "default" not in kwargs:
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 _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
214
- config_file: Path | None = None,
215
- add_verbosity=True,
216
- ask_for_missing=True,
217
- args=None,
218
- **kwargs) -> tuple[EnvClass | None, dict, WrongFields]:
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 isinstance(env_or_list, list):
233
- # NOTE. Reading config files when using subcommands is not implemented.
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 who this should work. "
238
- "Describe the developer your usecase so that they might implement this.")
239
- if "default" not in kwargs and not isinstance(env_or_list, list):
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
- if config_file:
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
- env, wrong_fields = run_tyro_parser(env_or_list, kwargs, add_verbosity, ask_for_missing, args)
272
- return env, wrong_fields
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)
@@ -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
- img = img.resize((250, 250))
40
- img = ImageTk.PhotoImage(img)
41
- panel = Label(self.adaptor.frame, image=img)
42
- panel.image = img
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)
@@ -14,6 +14,7 @@ from ..form_dict import TagDict
14
14
  from ..tag import Tag
15
15
  from ..types import DatetimeTag, PathTag
16
16
  from .date_entry import DateEntryFrame
17
+ from .external_fix import __create_widgets_monkeypatched
17
18
 
18
19
  if TYPE_CHECKING:
19
20
  from tk_window import TkWindow
@@ -159,10 +159,7 @@ class PathTag(Tag):
159
159
  @dataclass(repr=False)
160
160
  class DatetimeTag(Tag):
161
161
  """
162
- !!! warning
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
  # ![Time only](asset/datetime_time.avif)
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.2"
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.8.14" # NOTE: 0.9 brings some test breaking changes
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.extras]
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