mininterface 0.7.0__tar.gz → 0.7.2__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.0 → mininterface-0.7.2}/PKG-INFO +15 -6
- {mininterface-0.7.0 → mininterface-0.7.2}/README.md +9 -3
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/__init__.py +2 -4
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/__main__.py +8 -4
- mininterface-0.7.2/mininterface/auxiliary.py +151 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/cli_parser.py +14 -9
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/facet.py +27 -1
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/form_dict.py +9 -20
- mininterface-0.7.2/mininterface/interfaces.py +58 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/mininterface.py +70 -6
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/redirectable.py +9 -1
- mininterface-0.7.2/mininterface/showcase.py +82 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/tag.py +94 -36
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/tag_factory.py +41 -8
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/text_interface.py +55 -7
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/textual_interface/__init__.py +8 -2
- mininterface-0.7.2/mininterface/tk_interface/date_entry.py +291 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/tk_interface/redirect_text_tkinter.py +9 -0
- mininterface-0.7.2/mininterface/tk_interface/tk_facet.py +55 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/tk_interface/tk_window.py +38 -14
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/tk_interface/utils.py +30 -20
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/types.py +86 -8
- {mininterface-0.7.0 → mininterface-0.7.2}/pyproject.toml +7 -4
- mininterface-0.7.0/mininterface/auxiliary.py +0 -68
- mininterface-0.7.0/mininterface/interfaces.py +0 -46
- mininterface-0.7.0/mininterface/showcase.py +0 -42
- mininterface-0.7.0/mininterface/tk_interface/tk_facet.py +0 -21
- {mininterface-0.7.0 → mininterface-0.7.2}/LICENSE +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/ValidationFail.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/exceptions.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/experimental.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/start.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/subcommands.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/textual_interface/textual_adaptor.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/textual_interface/textual_app.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/textual_interface/textual_button_app.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/textual_interface/textual_facet.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/textual_interface/widgets.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/tk_interface/__init__.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/type_stubs.py +0 -0
- {mininterface-0.7.0 → mininterface-0.7.2}/mininterface/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
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,17 @@ 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: gui
|
|
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)
|
|
21
23
|
Requires-Dist: tkinter-tooltip
|
|
22
|
-
Requires-Dist: tkinter_form (==0.1
|
|
24
|
+
Requires-Dist: tkinter_form (==0.2.1)
|
|
25
|
+
Requires-Dist: tkscrollableframe
|
|
23
26
|
Requires-Dist: typing_extensions
|
|
24
|
-
Requires-Dist: tyro
|
|
27
|
+
Requires-Dist: tyro (==0.8.14)
|
|
25
28
|
Description-Content-Type: text/markdown
|
|
26
29
|
|
|
27
30
|
# Mininterface – access to GUI, TUI, CLI and config files
|
|
@@ -72,7 +75,7 @@ It was all the code you need. No lengthy blocks of code imposed by an external d
|
|
|
72
75
|
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
|
-
$ ./
|
|
78
|
+
$ ./program.py --help
|
|
76
79
|
usage: My application [-h] [-v] [--my-flag | --no-my-flag] [--my-number INT]
|
|
77
80
|
|
|
78
81
|
This calculates something.
|
|
@@ -93,6 +96,12 @@ Loading config file is a piece of cake. Alongside `program.py`, put `program.yam
|
|
|
93
96
|
my_number: 555
|
|
94
97
|
```
|
|
95
98
|
|
|
99
|
+
```bash
|
|
100
|
+
$ program.py --help
|
|
101
|
+
...
|
|
102
|
+
│ --my-number INT This number is very important (default: 555) │
|
|
103
|
+
```
|
|
104
|
+
|
|
96
105
|
## You got dialogues
|
|
97
106
|
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
107
|
|
|
@@ -122,7 +131,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
122
131
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
123
132
|
|
|
124
133
|
```bash
|
|
125
|
-
pip install mininterface
|
|
134
|
+
pip install mininterface[all] # GPLv3 and compatible
|
|
126
135
|
```
|
|
127
136
|
|
|
128
137
|
## Minimal installation
|
|
@@ -139,7 +148,7 @@ See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic
|
|
|
139
148
|
|
|
140
149
|
# Examples
|
|
141
150
|
|
|
142
|
-
A powerful [`m.form`]
|
|
151
|
+
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
152
|
|
|
144
153
|
## A complex dataclass.
|
|
145
154
|
|
|
@@ -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
|
-
$ ./
|
|
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
|
|
|
@@ -96,7 +102,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
96
102
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
97
103
|
|
|
98
104
|
```bash
|
|
99
|
-
pip install mininterface
|
|
105
|
+
pip install mininterface[all] # GPLv3 and compatible
|
|
100
106
|
```
|
|
101
107
|
|
|
102
108
|
## Minimal installation
|
|
@@ -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`]
|
|
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
|
|
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:
|
|
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
|
|
193
|
-
# However, the UI then is not able to use the number filtering capabilities.
|
|
194
|
-
|
|
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 `
|
|
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.
|
|
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:
|
|
262
|
-
for
|
|
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
|
|
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
|
|
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
|
-
|
|
177
|
-
|
|
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
|
|
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
|
-
"""
|
|
87
|
-
|
|
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
|
+

|
|
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
|
+

|
|
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
|
-
|
|
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.
|
|
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.
|