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.
Files changed (39) hide show
  1. {mininterface-0.7.3 → mininterface-0.7.5}/PKG-INFO +12 -9
  2. {mininterface-0.7.3 → mininterface-0.7.5}/README.md +8 -0
  3. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/__init__.py +8 -8
  4. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/auxiliary.py +5 -10
  5. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/cli_parser.py +96 -42
  6. mininterface-0.7.5/mininterface/config.py +28 -0
  7. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/experimental.py +0 -4
  8. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/facet.py +6 -2
  9. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/interfaces.py +3 -0
  10. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/mininterface.py +1 -1
  11. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/showcase.py +3 -0
  12. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/textual_adaptor.py +1 -8
  13. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/textual_app.py +14 -1
  14. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/utils.py +5 -5
  15. {mininterface-0.7.3 → mininterface-0.7.5}/pyproject.toml +4 -4
  16. {mininterface-0.7.3 → mininterface-0.7.5}/LICENSE +0 -0
  17. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/ValidationFail.py +0 -0
  18. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/__main__.py +0 -0
  19. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/exceptions.py +0 -0
  20. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/form_dict.py +0 -0
  21. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/redirectable.py +0 -0
  22. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/start.py +0 -0
  23. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/subcommands.py +0 -0
  24. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tag.py +0 -0
  25. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tag_factory.py +0 -0
  26. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/text_interface.py +0 -0
  27. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/__init__.py +0 -0
  28. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/textual_button_app.py +0 -0
  29. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/textual_facet.py +0 -0
  30. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/textual_interface/widgets.py +0 -0
  31. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/__init__.py +0 -0
  32. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/date_entry.py +0 -0
  33. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/external_fix.py +0 -0
  34. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/redirect_text_tkinter.py +0 -0
  35. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/tk_facet.py +0 -0
  36. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/tk_interface/tk_window.py +0 -0
  37. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/type_stubs.py +0 -0
  38. {mininterface-0.7.3 → mininterface-0.7.5}/mininterface/types.py +0 -0
  39. {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
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 (>=0.84,<0.85)
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 (==0.8.14)
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 _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,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 = _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
- interface = get_interface(title, interface, env)
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
- interface.form(wrong_fields)
197
- {setattr(interface.env, k, v.val) for k, v in wrong_fields.items()}
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
- interface.form()
199
+ m.form()
200
200
 
201
- return interface
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
- return issubclass(cls, annotation)
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._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
+
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 isinstance(res, NonpropagatingMissingType):
141
- # NOTE tyro does not work if a required positional is missing tyro.cli() returns just NonpropagatingMissingType.
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 _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
221
- config_file: Path | None = None,
222
- add_verbosity=True,
223
- ask_for_missing=True,
224
- args=None,
225
- **kwargs) -> tuple[EnvClass | None, dict, WrongFields]:
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
- if "default" not in kwargs and not subcommands:
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 config_file:
256
- disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
257
- # Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
258
- for key in (key for key, val in disk.items() if isinstance(val, dict)):
259
- disk[key] = env.__annotations__[key](**disk[key])
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. """
@@ -53,7 +53,3 @@ class FacetCallback():
53
53
  """
54
54
  pass
55
55
  # NOTE, just a stub. Deprecated, use CallbackTag instead.
56
-
57
-
58
- # NOTE should we use the dataclasses, isn't that slow?
59
- MININTERFACE_CONFIG = {"gui": {"combobox_since": 5}}
@@ -19,11 +19,14 @@ if TYPE_CHECKING:
19
19
 
20
20
  @dataclass
21
21
  class Image:
22
- """ NOTE. Experimental. Undocumented. """
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
  ![Asking number](asset/ask-number.avif)
117
117
 
118
- However, when run in a non-interactive session with TUI (ex. no display), [TextInterface](Interfaces.md#TextInterface)
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 pathlib import Path, PosixPath
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, flatten_keys
12
- from ..experimental import MININTERFACE_CONFIG, FacetCallback, SubmitButton
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()) > MININTERFACE_CONFIG["gui"]["combobox_since"]:
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.3"
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.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
21
21
  autocombobox = "1.4.2"
22
22
  humanize = "*" # used only in the TkInterface, hence it is not a minimal requirement
23
- textual = "~0.84"
23
+ textual = "<2.0.0"
24
24
  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
30
  img = ["pillow", "textual_imageview"]
31
31
  tui = ["textual_imageview"]
File without changes