mininterface 0.7.0__tar.gz → 0.7.1__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 (41) hide show
  1. {mininterface-0.7.0 → mininterface-0.7.1}/PKG-INFO +14 -4
  2. {mininterface-0.7.0 → mininterface-0.7.1}/README.md +8 -2
  3. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/__init__.py +2 -4
  4. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/__main__.py +8 -4
  5. mininterface-0.7.1/mininterface/auxiliary.py +151 -0
  6. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/cli_parser.py +14 -9
  7. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/facet.py +27 -1
  8. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/form_dict.py +9 -20
  9. mininterface-0.7.1/mininterface/interfaces.py +58 -0
  10. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/mininterface.py +70 -6
  11. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/redirectable.py +9 -1
  12. mininterface-0.7.1/mininterface/showcase.py +82 -0
  13. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/tag.py +92 -34
  14. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/tag_factory.py +37 -8
  15. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/text_interface.py +55 -7
  16. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/textual_interface/__init__.py +8 -2
  17. mininterface-0.7.1/mininterface/tk_interface/date_entry.py +221 -0
  18. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/tk_interface/redirect_text_tkinter.py +9 -0
  19. mininterface-0.7.1/mininterface/tk_interface/tk_facet.py +55 -0
  20. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/tk_interface/tk_window.py +35 -10
  21. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/tk_interface/utils.py +15 -8
  22. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/types.py +86 -8
  23. {mininterface-0.7.0 → mininterface-0.7.1}/pyproject.toml +7 -3
  24. mininterface-0.7.0/mininterface/auxiliary.py +0 -68
  25. mininterface-0.7.0/mininterface/interfaces.py +0 -46
  26. mininterface-0.7.0/mininterface/showcase.py +0 -42
  27. mininterface-0.7.0/mininterface/tk_interface/tk_facet.py +0 -21
  28. {mininterface-0.7.0 → mininterface-0.7.1}/LICENSE +0 -0
  29. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/ValidationFail.py +0 -0
  30. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/exceptions.py +0 -0
  31. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/experimental.py +0 -0
  32. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/start.py +0 -0
  33. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/subcommands.py +0 -0
  34. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/textual_interface/textual_adaptor.py +0 -0
  35. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/textual_interface/textual_app.py +0 -0
  36. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/textual_interface/textual_button_app.py +0 -0
  37. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/textual_interface/textual_facet.py +0 -0
  38. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/textual_interface/widgets.py +0 -0
  39. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/tk_interface/__init__.py +0 -0
  40. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/type_stubs.py +0 -0
  41. {mininterface-0.7.0 → mininterface-0.7.1}/mininterface/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mininterface
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: A minimal access to GUI, TUI, CLI and config
5
5
  Home-page: https://github.com/CZ-NIC/mininterface
6
6
  License: GPL-3.0-or-later
@@ -14,14 +14,18 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Provides-Extra: all
17
+ Provides-Extra: img
17
18
  Provides-Extra: web
18
19
  Requires-Dist: autocombobox (==1.4.2)
20
+ Requires-Dist: humanize
19
21
  Requires-Dist: pyyaml
20
22
  Requires-Dist: textual (>=0.84,<0.85)
23
+ Requires-Dist: tkcalendar
21
24
  Requires-Dist: tkinter-tooltip
22
25
  Requires-Dist: tkinter_form (==0.1.5.2)
26
+ Requires-Dist: tkscrollableframe
23
27
  Requires-Dist: typing_extensions
24
- Requires-Dist: tyro
28
+ Requires-Dist: tyro (==0.8.14)
25
29
  Description-Content-Type: text/markdown
26
30
 
27
31
  # Mininterface – access to GUI, TUI, CLI and config files
@@ -72,7 +76,7 @@ It was all the code you need. No lengthy blocks of code imposed by an external d
72
76
 
73
77
 
74
78
  ```bash
75
- $ ./hello.py --help
79
+ $ ./program.py --help
76
80
  usage: My application [-h] [-v] [--my-flag | --no-my-flag] [--my-number INT]
77
81
 
78
82
  This calculates something.
@@ -93,6 +97,12 @@ Loading config file is a piece of cake. Alongside `program.py`, put `program.yam
93
97
  my_number: 555
94
98
  ```
95
99
 
100
+ ```bash
101
+ $ program.py --help
102
+ ...
103
+ │ --my-number INT This number is very important (default: 555) │
104
+ ```
105
+
96
106
  ## You got dialogues
97
107
  Check out several useful methods to handle user dialogues. Here we bound the interface to a `with` statement that redirects stdout directly to the window.
98
108
 
@@ -139,7 +149,7 @@ See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic
139
149
 
140
150
  # Examples
141
151
 
142
- A powerful [`m.form`][mininterface.Mininterface.form] dialog method accepts either a dataclass or a dict. Take a look on both.
152
+ 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.
143
153
 
144
154
  ## A complex dataclass.
145
155
 
@@ -46,7 +46,7 @@ It was all the code you need. No lengthy blocks of code imposed by an external d
46
46
 
47
47
 
48
48
  ```bash
49
- $ ./hello.py --help
49
+ $ ./program.py --help
50
50
  usage: My application [-h] [-v] [--my-flag | --no-my-flag] [--my-number INT]
51
51
 
52
52
  This calculates something.
@@ -67,6 +67,12 @@ Loading config file is a piece of cake. Alongside `program.py`, put `program.yam
67
67
  my_number: 555
68
68
  ```
69
69
 
70
+ ```bash
71
+ $ program.py --help
72
+ ...
73
+ │ --my-number INT This number is very important (default: 555) │
74
+ ```
75
+
70
76
  ## You got dialogues
71
77
  Check out several useful methods to handle user dialogues. Here we bound the interface to a `with` statement that redirects stdout directly to the window.
72
78
 
@@ -113,7 +119,7 @@ See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic
113
119
 
114
120
  # Examples
115
121
 
116
- A powerful [`m.form`][mininterface.Mininterface.form] dialog method accepts either a dataclass or a dict. Take a look on both.
122
+ 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.
117
123
 
118
124
  ## A complex dataclass.
119
125
 
@@ -1,8 +1,7 @@
1
1
  import sys
2
2
  from dataclasses import dataclass
3
3
  from pathlib import Path
4
- from types import UnionType
5
- from typing import TYPE_CHECKING, Literal, Optional, Sequence, Type, Union
4
+ from typing import Literal, Optional, Sequence, Type
6
5
 
7
6
  from .exceptions import Cancelled, InterfaceNotAvailable
8
7
 
@@ -15,7 +14,6 @@ from .form_dict import DataClass, EnvClass
15
14
  from .mininterface import EnvClass, Mininterface
16
15
  from .start import Start
17
16
  from .tag import Tag
18
- from .text_interface import ReplInterface, TextInterface
19
17
  from .types import Choices, PathTag, Validation
20
18
 
21
19
  # NOTE:
@@ -38,7 +36,7 @@ def run(env_or_list: Type[EnvClass] | list[Type[Command]] | None = None,
38
36
  config_file: Path | str | bool = True,
39
37
  add_verbosity: bool = True,
40
38
  ask_for_missing: bool = True,
41
- interface: Type[Mininterface] | Literal["gui"] | Literal["tui"] = None,
39
+ interface: Type[Mininterface] | Literal["gui"] | Literal["tui"] | None = None,
42
40
  args: Optional[Sequence[str]] = None,
43
41
  **kwargs) -> Mininterface[EnvClass]:
44
42
  """ The main access, start here.
@@ -1,6 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  import sys
3
- from typing import Literal
3
+ from typing import Literal, Optional
4
4
  from tyro.conf import FlagConversionOff
5
5
 
6
6
  from .exceptions import DependencyRequired
@@ -25,6 +25,10 @@ class Web:
25
25
  port: int = 64646
26
26
 
27
27
 
28
+ InterfaceType = Literal["gui"] | Literal["tui"] | Literal["all"]
29
+ Showcase = Literal[1] | Literal[2]
30
+
31
+
28
32
  @dataclass
29
33
  class CliInteface:
30
34
  web: Web
@@ -39,8 +43,8 @@ class CliInteface:
39
43
  is_no: str = ""
40
44
  """ Display confirm box, focusing 'no'. """
41
45
 
42
- showcase: Literal["gui"] | Literal["tui"] | Literal["all"] = None
43
- """ Prints various form just to show what's possible. """
46
+ showcase: Optional[tuple[InterfaceType, Showcase]] = None
47
+ """ Prints various form just to show what's possible."""
44
48
 
45
49
 
46
50
  def web(m: Mininterface):
@@ -71,7 +75,7 @@ def main():
71
75
  if m.env.web.cmd:
72
76
  web(m)
73
77
  if m.env.showcase:
74
- showcase(m.env.showcase)
78
+ showcase(*m.env.showcase)
75
79
 
76
80
 
77
81
  if __name__ == "__main__":
@@ -0,0 +1,151 @@
1
+ from typing import get_args, get_origin, Union
2
+ from dataclasses import MISSING, fields, is_dataclass
3
+ import os
4
+ import re
5
+ from argparse import ArgumentParser
6
+ from types import UnionType
7
+ from typing import Callable, Iterable, Optional, TypeVar, Union, get_args, get_origin
8
+
9
+ from tyro.extras import get_parser
10
+
11
+ T = TypeVar("T")
12
+ KT = str
13
+ common_iterables = list, tuple, set
14
+ """ collections, and not a str """
15
+
16
+
17
+ def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]] = None) -> Iterable[T]:
18
+ """ Recursively traverse whole dict """
19
+ for k, v in d.items():
20
+ if isinstance(v, dict):
21
+ if include_keys:
22
+ yield from include_keys(k)
23
+ yield from flatten(v)
24
+ else:
25
+ yield v
26
+
27
+
28
+ def flatten_keys(d: dict[KT, T | dict]) -> Iterable[tuple[KT, T]]:
29
+ """ Recursively traverse whole dict """
30
+ for k, v in d.items():
31
+ if isinstance(v, dict):
32
+ yield from flatten_keys(v)
33
+ else:
34
+ yield k, v
35
+
36
+
37
+ def guess_type(val: T) -> type[T]:
38
+ t = type(val)
39
+ if t in common_iterables and len(common_iterables):
40
+ elements_type = set(type(x) for x in val)
41
+ if len(elements_type) == 1:
42
+ return t[list(elements_type)[0]]
43
+ return t
44
+
45
+
46
+ def get_terminal_size():
47
+ try:
48
+ # XX when piping the input IN, it writes
49
+ # echo "434" | convey -f base64 --debug
50
+ # stty: 'standard input': Inappropriate ioctl for device
51
+ # I do not know how to suppress this warning.
52
+ height, width = (int(s) for s in os.popen('stty size', 'r').read().split())
53
+ return height, width
54
+ except (OSError, ValueError):
55
+ return 0, 0
56
+
57
+
58
+ def get_descriptions(parser: ArgumentParser) -> dict:
59
+ """ Load descriptions from the parser. Strip argparse info about the default value as it will be editable in the form. """
60
+ # clean-up tyro stuff that may have a meaning in the CLI, but not in the UI
61
+ return {action.dest.replace("-", "_"): re.sub(r"\((default|fixed to|required).*\)", "", action.help or "")
62
+ for action in parser._actions}
63
+
64
+
65
+ def get_description(obj, param: str) -> str:
66
+ return get_descriptions(get_parser(obj))[param]
67
+
68
+
69
+ def yield_annotations(dataclass):
70
+ yield from (cl.__annotations__ for cl in dataclass.__mro__ if is_dataclass(cl))
71
+
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
+ def matches_annotation(value, annotation) -> bool:
83
+ """ Check whether the value type corresponds to the annotation.
84
+ Because built-in isinstance is not enough, it cannot determine parametrized generics.
85
+ """
86
+ # union, including Optional and UnionType
87
+ if isinstance(annotation, UnionType) or get_origin(annotation) is Union:
88
+ return any(matches_annotation(value, arg) for arg in get_args(annotation))
89
+
90
+ # generics, ex. list, tuple
91
+ origin = get_origin(annotation)
92
+ if origin:
93
+ if not isinstance(value, origin):
94
+ return False
95
+
96
+ subtypes = get_args(annotation)
97
+ if origin is list:
98
+ return all(matches_annotation(item, subtypes[0]) for item in value)
99
+ elif origin is tuple:
100
+ if len(subtypes) != len(value):
101
+ return False
102
+ return all(matches_annotation(v, t) for v, t in zip(value, subtypes))
103
+ elif origin is dict:
104
+ key_type, value_type = subtypes
105
+ return all(matches_annotation(k, key_type) and matches_annotation(v, value_type) for k, v in value.items())
106
+ else:
107
+ return True
108
+
109
+ # ex. annotation=int
110
+ return isinstance(value, annotation)
111
+
112
+
113
+ def subclass_matches_annotation(cls, annotation) -> bool:
114
+ """
115
+ Check whether the type in the value corresponds to the annotation.
116
+ """
117
+ # Union (Optional and UnionType)
118
+ if isinstance(annotation, UnionType) or get_origin(annotation) is Union:
119
+ return any(subclass_matches_annotation(cls, arg) for arg in get_args(annotation))
120
+
121
+ # generics (list[int], tuple[int, str])
122
+ origin = get_origin(annotation)
123
+ if origin:
124
+ # origin match (ex. list, tuple)
125
+ if not issubclass(cls, origin):
126
+ return False
127
+
128
+ # subtype match (ex. `int` v `list[int]`)
129
+ subtypes = get_args(annotation)
130
+ if origin is list or origin is set: # list and set have the single subtype
131
+ return all(subclass_matches_annotation(object, subtypes[0]))
132
+ elif origin is tuple: # tuple has multiple subtypes
133
+ return all(subclass_matches_annotation(object, t) for t in subtypes)
134
+ elif origin is dict:
135
+ key_type, value_type = subtypes
136
+ return subclass_matches_annotation(object, key_type) and subclass_matches_annotation(object, value_type)
137
+ else:
138
+ return True
139
+
140
+ # simple types like scalars
141
+ return issubclass(cls, annotation)
142
+
143
+
144
+ def serialize_structure(obj):
145
+ """ Ex: [Path("/tmp"), Path("/usr"), 1] -> ["/tmp", "/usr", 1]. """
146
+ if isinstance(obj, (str, int, float)):
147
+ return obj
148
+ elif isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)):
149
+ return type(obj)(serialize_structure(item) for item in obj)
150
+ else:
151
+ return str(obj)
@@ -16,10 +16,12 @@ import yaml
16
16
  from tyro import cli
17
17
  from tyro._argparse_formatter import TyroArgumentParser
18
18
  from tyro._fields import NonpropagatingMissingType
19
+ # NOTE in the future versions of tyro, include that way:
20
+ # from tyro._singleton import NonpropagatingMissingType
19
21
  from tyro.extras import get_parser
20
22
 
21
- from .auxiliary import yield_annotations
22
- from .form_dict import EnvClass
23
+ from .auxiliary import yield_annotations, yield_defaults
24
+ from .form_dict import EnvClass, MissingTagValue
23
25
  from .tag import Tag
24
26
  from .tag_factory import tag_factory
25
27
  from .validators import not_empty
@@ -189,20 +191,23 @@ def treat_missing(env_class, kwargs: dict, parser: ArgumentParser, wf: dict, arg
189
191
  if not any(field_name in ann for ann in yield_annotations(env_class)):
190
192
  raise ValueError(f"Cannot find {field_name} in the configuration object")
191
193
 
192
- # NOTE: We put '' to the UI to clearly state that the value is missing.
193
- # However, the UI then is not able to use the number filtering capabilities.
194
- tag = wf[field_name] = tag_factory("",
194
+ # NOTE: We put MissingTagValue to the UI to clearly state that the value is missing.
195
+ # However, the UI then is not able to use ex. the number filtering capabilities.
196
+ # Putting there None is not a good idea as dataclass_to_tagdict fails if None is not allowed by the annotation.
197
+ tag = wf[field_name] = tag_factory(MissingTagValue(),
198
+ # tag = wf[field_name] = tag_factory(MISSING,
195
199
  argument.help.replace("(required)", ""),
196
200
  validation=not_empty,
197
201
  _src_class=env_class,
198
202
  _src_key=field_name
199
203
  )
200
- # Why `type_()`? We need to put a default value so that the parsing will not fail.
204
+ # Why `_make_default_value`? We need to put a default value so that the parsing will not fail.
201
205
  # A None would be enough because Mininterface will ask for the missing values
202
206
  # promply, however, Pydantic model would fail.
207
+ # As it serves only for tyro parsing and the field is marked wrong, the made up value is never used or seen.
203
208
  if "default" not in kwargs:
204
209
  kwargs["default"] = SimpleNamespace()
205
- setattr(kwargs["default"], field_name, tag.annotation())
210
+ setattr(kwargs["default"], field_name, tag._make_default_value())
206
211
 
207
212
 
208
213
  def _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
@@ -258,8 +263,8 @@ def _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
258
263
  else:
259
264
  # To ensure the configuration file does not need to contain all keys, we have to fill in the missing ones.
260
265
  # Otherwise, tyro will spawn warnings about missing fields.
261
- static = {key: getattr(env_or_list, key, MISSING)
262
- for ann in yield_annotations(env_or_list) for key in ann if not key.startswith("__") and not key in disk}
266
+ static = {key: val
267
+ for key, val in yield_defaults(env_or_list) if not key.startswith("__") and not key in disk}
263
268
  kwargs["default"] = SimpleNamespace(**(disk | static))
264
269
 
265
270
  # Load configuration from CLI
@@ -1,5 +1,10 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import TYPE_CHECKING, Callable, Generic, Literal, Optional
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Callable, Generic, Literal, Optional, TypeVar
5
+ from warnings import warn
6
+
7
+ from .redirectable import Redirectable
3
8
 
4
9
  from .exceptions import ValidationFail
5
10
 
@@ -9,6 +14,16 @@ from .tag import Tag
9
14
 
10
15
  if TYPE_CHECKING:
11
16
  from . import Mininterface
17
+ from typing import Self # remove the line as of Python3.11 and make `"Self" -> Self`
18
+
19
+
20
+ @dataclass
21
+ class Image:
22
+ """ NOTE. Experimental. Undocumented. """
23
+ src: str | Path
24
+
25
+
26
+ LayoutElement = TypeVar("LayoutElement", str, Image, Path, "Self")
12
27
 
13
28
 
14
29
  class BackendAdaptor(ABC):
@@ -76,10 +91,21 @@ class Facet(Generic[EnvClass]):
76
91
  def _fetch_from_adaptor(self, form: TagDict):
77
92
  self._form = form
78
93
 
94
+ def _clear(self):
95
+ """ Experimental.
96
+ Clear redirected text. """
97
+ if isinstance(self.adaptor.interface, Redirectable):
98
+ self.adaptor.interface._redirected.clear()
99
+
79
100
  def set_title(self, text):
80
101
  """ Set the main heading. """
81
102
  print("Title", text)
82
103
 
104
+ def _layout(self, elements: list[LayoutElement]):
105
+ """ Experimental. """
106
+ # NOTE remove warn when working in textual
107
+ warn("Facet layout not implemented for this interface.")
108
+
83
109
  def submit(self, _post_submit=None):
84
110
  """ Submits the whole form.
85
111
 
@@ -8,9 +8,10 @@ from types import FunctionType, MethodType, SimpleNamespace
8
8
  from typing import (TYPE_CHECKING, Any, Callable, Optional, Type, TypeVar,
9
9
  Union, get_args, get_type_hints)
10
10
 
11
+
11
12
  from .auxiliary import get_description
12
- from .tag import Tag, TagValue
13
- from .tag_factory import tag_factory
13
+ from .tag import MissingTagValue, Tag, TagValue
14
+ from .tag_factory import tag_assure_type, tag_fetch, tag_factory
14
15
 
15
16
  if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self`
16
17
  from typing import Self
@@ -105,7 +106,8 @@ def dict_to_tagdict(data: dict, mininterface: Optional["Mininterface"] = None) -
105
106
  if not isinstance(val, Tag):
106
107
  tag = Tag(val, "", name=key, _src_dict=data, _src_key=key, **d)
107
108
  else:
108
- tag = val._fetch_from(Tag(**d))
109
+ tag = tag_fetch(val, d)
110
+ tag = tag_assure_type(tag)
109
111
  fd[key] = tag
110
112
  return fd
111
113
 
@@ -173,22 +175,8 @@ def dataclass_to_tagdict(env: EnvClass | Type[EnvClass], mininterface: Optional[
173
175
  raise ValueError(f"We got a namespace instead of class, CLI probably failed: {env}")
174
176
 
175
177
  for param, val in iterate_attributes(env):
176
- annotation = get_type_hints(env.__class__).get(param)
177
- if val is None:
178
- if type(None) in get_args(annotation):
179
- # Since tkinter_form does not handle None yet, we have help it.
180
- # We need it to be able to write a number and if empty, return None.
181
- # This would fail: `severity: int | None = None`
182
- # Here, we convert None to str(""), in normalize_types we convert it back.
183
- val = ""
184
- else:
185
- # An unknown type annotation encountered.
186
- # Since tkinter_form does not handle None yet, this will display as checkbox.
187
- # Which is not probably wanted.
188
- val = False
189
- logger.warning(f"Annotation {annotation} of `{param}` not supported by Mininterface."
190
- "None converted to False.")
191
-
178
+ if isinstance(val, MissingTagValue):
179
+ val = None # need to convert as MissingTagValue has .__dict__ too
192
180
  if hasattr(val, "__dict__") and not isinstance(val, (FunctionType, MethodType)): # nested config hierarchy
193
181
  # nested config hierarchy
194
182
  # Why checking the isinstance? See Tag._is_a_callable.
@@ -198,6 +186,7 @@ def dataclass_to_tagdict(env: EnvClass | Type[EnvClass], mininterface: Optional[
198
186
  if not isinstance(val, Tag):
199
187
  tag = tag_factory(val, _src_key=param, _src_obj=env, **d)
200
188
  else:
201
- tag = val._fetch_from(Tag(**d))
189
+ tag = tag_fetch(val, d)
190
+ tag = tag_assure_type(tag)
202
191
  (subdict if _nested else main)[param] = tag
203
192
  return subdict
@@ -0,0 +1,58 @@
1
+ # Access to interfaces via this module assures lazy loading
2
+ from importlib import import_module
3
+ import sys
4
+ from typing import Literal, Type
5
+
6
+ from .mininterface import Mininterface
7
+ from .exceptions import InterfaceNotAvailable
8
+ from .text_interface import TextInterface
9
+
10
+ # We do not use InterfaceType as a type in run because we want the documentation to show full alias.
11
+ InterfaceType = Type[Mininterface] | Literal["gui"] | Literal["tui"] | None
12
+
13
+
14
+ def __getattr__(name):
15
+ # shortcuts
16
+ if name == "GuiInterface":
17
+ return __getattr__("TkInterface")
18
+ if name == "TuiInterface":
19
+ # if textual not installed or isatty False, return TextInterface
20
+ return __getattr__("TextualInterface") or TextInterface
21
+
22
+ # real interfaces
23
+ if name == "TkInterface":
24
+ try:
25
+ globals()[name] = import_module("..tk_interface", __name__).TkInterface
26
+ return globals()[name]
27
+ except InterfaceNotAvailable:
28
+ return None
29
+
30
+ if name == "TextualInterface":
31
+ try:
32
+ globals()[name] = import_module("..textual_interface", __name__).TextualInterface
33
+ return globals()[name]
34
+ except InterfaceNotAvailable:
35
+ return None
36
+ return None # such attribute does not exist
37
+
38
+
39
+ def get_interface(title="", interface: InterfaceType = None, env=None):
40
+ args = title, env
41
+ if isinstance(interface, type) and issubclass(interface, Mininterface):
42
+ # the user gave a specific interface, let them catch InterfaceNotAvailable then
43
+ return interface(*args)
44
+ if interface == "gui" or interface is None:
45
+ try:
46
+ return __getattr__("GuiInterface")(*args)
47
+ except InterfaceNotAvailable:
48
+ pass
49
+ try:
50
+ return __getattr__("TuiInterface")(*args)
51
+ except InterfaceNotAvailable:
52
+ # Even though TUI is able to claim a non-interactive terminal,
53
+ # ex. when doing a cron job, a terminal cannot be made interactive.
54
+ pass
55
+ return Mininterface(*args)
56
+
57
+
58
+ __all__ = ["GuiInterface", "TuiInterface", "TextInterface", "TextualInterface", "TkInterface"]
@@ -83,8 +83,72 @@ class Mininterface(Generic[EnvClass]):
83
83
  self.env.run()
84
84
 
85
85
  def __enter__(self) -> "Self":
86
- """ When used in the with statement, the GUI window does not vanish between dialogs
87
- and it redirects the stdout to a text area. """
86
+ """ Usage within the with statement makes the program to attempt for the following benefits:
87
+
88
+ # Continual window
89
+
90
+ Do not vanish between dialogs (the GUI window stays the same)
91
+
92
+ # Stdout redirection
93
+
94
+ Redirects the stdout to a text area instead of a terminal.
95
+
96
+ ```python3
97
+ from mininterface import run
98
+
99
+ with run() as m:
100
+ print("This is a printed text")
101
+ m.alert("Alert text")
102
+ ```
103
+
104
+ ![With statement print redirect](asset/with-print-redirect.avif)
105
+
106
+ # Make the session interactive
107
+
108
+ If run from an interactive terminal or if a GUI is used, nothing special happens.
109
+
110
+ ```python3
111
+ # $ ./program.py
112
+ with run() as m:
113
+ m.ask_number("What number")
114
+ ```
115
+
116
+ ![Asking number](asset/ask-number.avif)
117
+
118
+ However, when run in a non-interactive session with TUI (ex. no display), [TextInterface](Interfaces.md#TextInterface)
119
+ is used which is able to turn it into an interactive one.
120
+
121
+ ```python3
122
+ piped_in = int(sys.stdin.read())
123
+
124
+ with run(interface="tui") as m:
125
+ result = m.ask_number("What number") + piped_in
126
+ print(result)
127
+ ```
128
+
129
+ ```bash
130
+ $ echo 2 | ./program.py
131
+ What number: 3
132
+ 5
133
+ ```
134
+
135
+ If the `with` statement is not used, the result is the same as if an interactive session is not available, like in a cron job.
136
+ In that case, plain Mininterface is used.
137
+
138
+ ```python3
139
+ piped_in = int(sys.stdin.read())
140
+
141
+ m = run(interface="tui")
142
+ result = m.ask_number("What number") + piped_in
143
+ print(result)
144
+ ```
145
+
146
+ ```bash
147
+ echo 2 | ./program.py
148
+ Asking: What number
149
+ 3
150
+ ```
151
+ """
88
152
  return self
89
153
 
90
154
  def __exit__(self, *_):
@@ -97,8 +161,8 @@ class Mininterface(Generic[EnvClass]):
97
161
 
98
162
  def ask(self, text: str) -> str:
99
163
  """ Prompt the user to input a text. """
100
- print("Asking", text)
101
- raise Cancelled(".. cancelled")
164
+ print("Asking:", text)
165
+ return ""
102
166
 
103
167
  def ask_number(self, text: str) -> int:
104
168
  """ Prompt the user to input a number. Empty input = 0.
@@ -117,7 +181,7 @@ class Mininterface(Generic[EnvClass]):
117
181
  Number
118
182
 
119
183
  """
120
- print("Asking number", text)
184
+ print("Asking number:", text)
121
185
  return 0
122
186
 
123
187
  def choice(self, choices: ChoicesType, title: str = "", _guesses=None,
@@ -345,7 +409,7 @@ class Mininterface(Generic[EnvClass]):
345
409
  # The form dict might be a default dict but we want output just the dict (it's shorter).
346
410
  f = dict(f)
347
411
  print(f"Asking the form {title}".strip(), f)
348
- return self._form(form, title, MinAdaptor(self))
412
+ return self._form(form, title, MinAdaptor(self), submit)
349
413
 
350
414
  def _form(self,
351
415
  form: DataClass | Type[DataClass] | FormDict | None,
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
  from typing import TYPE_CHECKING
3
+
3
4
  if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self`
4
5
  from typing import Self, Type
5
6
  else:
@@ -21,11 +22,18 @@ class RedirectText:
21
22
 
22
23
  def join(self):
23
24
  t = "".join(self.pending_buffer)
24
- self.pending_buffer.clear()
25
+ self.clear()
25
26
  return t
26
27
 
28
+ def isatty(self): # required by an interface
29
+ return False
30
+
31
+ def clear(self):
32
+ self.pending_buffer.clear()
33
+
27
34
 
28
35
  class Redirectable:
36
+ """ When enwraped in a with statement, the prints go to the UI instead of a stdout."""
29
37
  # NOTE When used in the with statement, the TUI window should not vanish between dialogues.
30
38
  # The same way the GUI does not vanish.
31
39
  # NOTE: Current implementation will show only after a dialog submit, not continuously.