mininterface 1.0.4__tar.gz → 1.1.0__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-1.0.4 → mininterface-1.1.0}/PKG-INFO +3 -3
- {mininterface-1.0.4 → mininterface-1.1.0}/README.md +2 -2
- mininterface-1.1.0/mininterface/__init__.py +7 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/__main__.py +1 -1
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_lib/argparse_support.py +29 -47
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_lib/auxiliary.py +140 -8
- mininterface-1.1.0/mininterface/_lib/cli_flags.py +107 -0
- mininterface-1.1.0/mininterface/_lib/cli_parser.py +408 -0
- mininterface-1.0.4/mininterface/cli.py → mininterface-1.1.0/mininterface/_lib/cli_utils.py +4 -49
- mininterface-1.1.0/mininterface/_lib/config_file.py +101 -0
- mininterface-1.1.0/mininterface/_lib/dataclass_creation.py +282 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_lib/form_dict.py +35 -8
- mininterface-1.1.0/mininterface/_lib/future_compatibility.py +6 -0
- mininterface-1.0.4/mininterface/__init__.py → mininterface-1.1.0/mininterface/_lib/run.py +159 -75
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_lib/showcase.py +3 -1
- mininterface-1.1.0/mininterface/_lib/start.py +135 -0
- mininterface-1.1.0/mininterface/_lib/tyro_patches.py +400 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_mininterface/__init__.py +26 -19
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_mininterface/adaptor.py +5 -9
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_tk_interface/adaptor.py +10 -3
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_tk_interface/select_input.py +18 -3
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_tk_interface/utils.py +5 -1
- mininterface-1.1.0/mininterface/cli.py +46 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/interfaces.py +3 -6
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/settings.py +15 -15
- mininterface-1.1.0/mininterface/tag/flag.py +284 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/select_tag.py +82 -22
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/tag.py +58 -21
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/tag_factory.py +41 -3
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/validators.py +13 -9
- {mininterface-1.0.4 → mininterface-1.1.0}/pyproject.toml +1 -1
- mininterface-1.0.4/mininterface/_lib/cli_parser.py +0 -574
- mininterface-1.0.4/mininterface/_lib/start.py +0 -133
- mininterface-1.0.4/mininterface/tag/flag.py +0 -144
- {mininterface-1.0.4 → mininterface-1.1.0}/LICENSE +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_lib/__init__.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_lib/redirectable.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_lib/shortcuts.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_mininterface/mixin.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_text_interface/__init__.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_text_interface/adaptor.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_text_interface/facet.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/__init__.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/adaptor.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/button_contents.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/facet.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/file_picker_input.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/form_contents.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/secret_input.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/style.tcss +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/textual_app.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_textual_interface/widgets.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_tk_interface/__init__.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_tk_interface/date_entry.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_tk_interface/external_fix.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_tk_interface/facet.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_tk_interface/secret_entry.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_web_interface/__init__.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_web_interface/app.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_web_interface/child_adaptor.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/_web_interface/parent_adaptor.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/exceptions.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/experimental.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/facet/__init__.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/__init__.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/alias.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/callback_tag.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/datetime_tag.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/internal.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/path_tag.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/secret_tag.py +0 -0
- {mininterface-1.0.4 → mininterface-1.1.0}/mininterface/tag/type_stubs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A minimal access to GUI, TUI, CLI and config
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
Author: Edvard Rejthar
|
|
@@ -37,7 +37,7 @@ Project-URL: Homepage, https://github.com/CZ-NIC/mininterface
|
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
38
38
|
|
|
39
39
|
# Mininterface – access to GUI, TUI, web, CLI and config files
|
|
40
|
-
[](https://github.com/CZ-NIC/mininterface/actions)
|
|
40
|
+
[](https://github.com/CZ-NIC/mininterface/actions)
|
|
41
41
|
[](https://pepy.tech/project/mininterface)
|
|
42
42
|
|
|
43
43
|
Write the program core, do not bother with the input/output.
|
|
@@ -144,7 +144,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
144
144
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
145
145
|
|
|
146
146
|
```bash
|
|
147
|
-
pip install mininterface[all] # GPLv3 and compatible
|
|
147
|
+
pip install "mininterface[all]<2" # GPLv3 and compatible
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
## Bundles
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Mininterface – access to GUI, TUI, web, CLI and config files
|
|
2
|
-
[](https://github.com/CZ-NIC/mininterface/actions)
|
|
2
|
+
[](https://github.com/CZ-NIC/mininterface/actions)
|
|
3
3
|
[](https://pepy.tech/project/mininterface)
|
|
4
4
|
|
|
5
5
|
Write the program core, do not bother with the input/output.
|
|
@@ -106,7 +106,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
106
106
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
pip install mininterface[all] # GPLv3 and compatible
|
|
109
|
+
pip install "mininterface[all]<2" # GPLv3 and compatible
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
## Bundles
|
|
@@ -1,35 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
_StoreConstAction,
|
|
8
|
-
_StoreFalseAction,
|
|
9
|
-
_StoreTrueAction,
|
|
10
|
-
_SubParsersAction,
|
|
11
|
-
_VersionAction,
|
|
12
|
-
Action,
|
|
13
|
-
ArgumentParser,
|
|
14
|
-
)
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
from argparse import (SUPPRESS, Action, ArgumentParser, _AppendAction,
|
|
4
|
+
_AppendConstAction, _CountAction, _HelpAction,
|
|
5
|
+
_StoreConstAction, _StoreFalseAction, _StoreTrueAction,
|
|
6
|
+
_SubParsersAction, _VersionAction)
|
|
15
7
|
from collections import defaultdict
|
|
16
8
|
from dataclasses import MISSING, Field, dataclass, field, make_dataclass
|
|
17
9
|
from functools import cached_property
|
|
18
|
-
import
|
|
19
|
-
import sys
|
|
20
|
-
from typing import Annotated, Callable, Literal, Optional
|
|
10
|
+
from typing import Annotated, Callable, Optional
|
|
21
11
|
from warnings import warn
|
|
22
12
|
|
|
23
|
-
from
|
|
24
|
-
|
|
25
|
-
from .. import Options
|
|
26
|
-
|
|
13
|
+
from ..tag.alias import Options
|
|
27
14
|
from .form_dict import DataClass
|
|
28
15
|
|
|
29
|
-
|
|
30
16
|
try:
|
|
31
|
-
from tyro.
|
|
32
|
-
from tyro.conf import Positional, arg
|
|
17
|
+
from tyro.conf import DisallowNone, OmitSubcommandPrefixes, Positional
|
|
33
18
|
except ImportError:
|
|
34
19
|
from ..exceptions import DependencyRequired
|
|
35
20
|
|
|
@@ -80,11 +65,16 @@ class ArgparseField:
|
|
|
80
65
|
return self.action.dest in self.properties
|
|
81
66
|
|
|
82
67
|
|
|
83
|
-
def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> DataClass | list[DataClass]:
|
|
68
|
+
def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> tuple[DataClass | list[DataClass], Optional[str]]:
|
|
84
69
|
"""
|
|
85
70
|
Note: Ex. parser.add_argument("--time", type=time) -> does work at all in argparse, here it works.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
DataClass | list[DataClass]
|
|
74
|
+
Optional[str]: add_version flag
|
|
86
75
|
"""
|
|
87
76
|
subparsers: list[_SubParsersAction] = []
|
|
77
|
+
add_version = None
|
|
88
78
|
|
|
89
79
|
normal_actions: list[Action] = []
|
|
90
80
|
has_positionals = False
|
|
@@ -98,6 +88,9 @@ def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> DataClass
|
|
|
98
88
|
"This CLI parser have a subcommand placed after positional arguments. The order of arguments changes, see --help."
|
|
99
89
|
)
|
|
100
90
|
subparsers.append(action)
|
|
91
|
+
case _VersionAction():
|
|
92
|
+
# We do not want the version to be part of the dataclass (and appear in `m.form()`).
|
|
93
|
+
add_version = action.version
|
|
101
94
|
case _:
|
|
102
95
|
if not action.option_strings:
|
|
103
96
|
has_positionals = True
|
|
@@ -113,9 +106,9 @@ def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> DataClass
|
|
|
113
106
|
)
|
|
114
107
|
for subparser in subparsers
|
|
115
108
|
for subname, subactions, help_ in _loop_SubParsersAction(subparser)
|
|
116
|
-
]
|
|
109
|
+
], add_version
|
|
117
110
|
else:
|
|
118
|
-
return _make_dataclass_from_actions(normal_actions, name, None, parser.description)
|
|
111
|
+
return _make_dataclass_from_actions(normal_actions, name, None, parser.description), add_version
|
|
119
112
|
|
|
120
113
|
|
|
121
114
|
def _loop_SubParsersAction(subparser: _SubParsersAction):
|
|
@@ -144,22 +137,6 @@ def _make_dataclass_from_actions(
|
|
|
144
137
|
match action:
|
|
145
138
|
case _HelpAction():
|
|
146
139
|
continue
|
|
147
|
-
case _VersionAction():
|
|
148
|
-
# NOTE Should be probably implemented in tyro. Here that way:
|
|
149
|
-
# run(add_version="1.2.3")
|
|
150
|
-
# run(add_version_package="intelmq") -> get pip version
|
|
151
|
-
arg_type = Annotated[
|
|
152
|
-
None,
|
|
153
|
-
PrimitiveConstructorSpec(
|
|
154
|
-
nargs="*",
|
|
155
|
-
metavar="",
|
|
156
|
-
instance_from_str=lambda _, v=action.version: print(v) or sys.exit(0),
|
|
157
|
-
is_instance=lambda _: True,
|
|
158
|
-
# NOTE tyro might not diplay anything here,
|
|
159
|
-
# but it displays `(default: )`
|
|
160
|
-
str_from_instance=(lambda _, v=action.version: [str(v)]),
|
|
161
|
-
),
|
|
162
|
-
]
|
|
163
140
|
case _SubParsersAction():
|
|
164
141
|
# Note that there is only one _SubParsersAction in argparse
|
|
165
142
|
# but to be sure, we allow multiple of them
|
|
@@ -216,12 +193,17 @@ def _make_dataclass_from_actions(
|
|
|
216
193
|
else:
|
|
217
194
|
arg_type = str
|
|
218
195
|
|
|
219
|
-
metavar = None
|
|
220
196
|
if "default" not in opt and "default_factory" not in opt:
|
|
221
197
|
if action.choices:
|
|
222
|
-
# With the drop of Python 3.10, use:
|
|
198
|
+
# With the drop of Python 3.10, use mere:
|
|
223
199
|
# arg_type = Literal[*action.choices]
|
|
224
|
-
|
|
200
|
+
if sys.version_info >= (3,11):
|
|
201
|
+
from .future_compatibility import literal
|
|
202
|
+
arg_type = literal(action.choices)
|
|
203
|
+
else:
|
|
204
|
+
# we do not prefer this option as tyro does not understand it
|
|
205
|
+
# and won't display options in the help
|
|
206
|
+
arg_type = Annotated[arg_type, Options(*action.choices)]
|
|
225
207
|
|
|
226
208
|
if not action.option_strings and action.default is None and action.nargs != "?":
|
|
227
209
|
opt["default"] = MISSING
|
|
@@ -281,4 +263,4 @@ def _make_dataclass_from_actions(
|
|
|
281
263
|
separator = ": " if needs_colon else ("\n" if trimmed else "")
|
|
282
264
|
dc.__doc__ = trimmed + separator + (description or "")
|
|
283
265
|
|
|
284
|
-
return dc
|
|
266
|
+
return DisallowNone[dc]
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
from dataclasses import fields, is_dataclass
|
|
1
|
+
import logging
|
|
3
2
|
import os
|
|
4
3
|
import re
|
|
5
4
|
from argparse import ArgumentParser
|
|
5
|
+
from dataclasses import fields, is_dataclass
|
|
6
|
+
from functools import lru_cache
|
|
6
7
|
from types import UnionType
|
|
7
|
-
from typing import Callable, Iterable, Optional, TypeVar, Union,
|
|
8
|
+
from typing import (Any, Callable, Iterable, Optional, TypeVar, Union,
|
|
9
|
+
get_args, get_origin, get_type_hints)
|
|
10
|
+
|
|
11
|
+
from annotated_types import Ge, Gt, Le, Len, Lt, MultipleOf
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
10
14
|
|
|
11
15
|
try:
|
|
12
16
|
from tyro.extras import get_parser
|
|
@@ -71,14 +75,29 @@ def get_descriptions(parser: ArgumentParser) -> dict:
|
|
|
71
75
|
"""Load descriptions from the parser. Strip argparse info about the default value as it will be editable in the form."""
|
|
72
76
|
# clean-up tyro stuff that may have a meaning in the CLI, but not in the UI
|
|
73
77
|
return {
|
|
74
|
-
action.dest.replace("-", "_"): re.sub(r"\((default|fixed to|required).*\)", "", action.help or "")
|
|
78
|
+
re.sub(r"\s\(positional\)$", "", action.dest).replace("-", "_"): re.sub(r"\((default|fixed to|required).*\)", "", action.help or "")
|
|
75
79
|
for action in parser._actions
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
@lru_cache
|
|
84
|
+
def _get_parser(obj):
|
|
80
85
|
if get_parser:
|
|
81
|
-
return
|
|
86
|
+
return get_parser(obj)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_description(obj, param: str) -> str:
|
|
90
|
+
if p := _get_parser(obj):
|
|
91
|
+
try:
|
|
92
|
+
d = get_descriptions(p)[param].strip()
|
|
93
|
+
except KeyError:
|
|
94
|
+
logger.warning("Cannot fetch description for '%s'", param)
|
|
95
|
+
return ""
|
|
96
|
+
else:
|
|
97
|
+
if d.replace("-", "_") == param:
|
|
98
|
+
# field `bot_id` is reported as `bot-id` in tyro
|
|
99
|
+
return ""
|
|
100
|
+
return d
|
|
82
101
|
else:
|
|
83
102
|
# We are missing mininterface[basic] requirement. Tyro is missing.
|
|
84
103
|
# Without tyro, we are not able to evaluate the class: m.form(Env),
|
|
@@ -91,6 +110,78 @@ def yield_annotations(dataclass):
|
|
|
91
110
|
yield from (cl.__annotations__ for cl in dataclass.__mro__ if is_dataclass(cl))
|
|
92
111
|
|
|
93
112
|
|
|
113
|
+
def get_annotation(class_, dest: str, crawled: list):
|
|
114
|
+
"""Get the attribute annotation according to the path in `dest` (dot means a nested subclass).
|
|
115
|
+
Works for dataclass, pydantic, attrs.
|
|
116
|
+
|
|
117
|
+
Ex: get_annotation(AppConfig, "bot.bot_id"))
|
|
118
|
+
|
|
119
|
+
Ex: get_annotation(AppConfig, "app.subcommand.bot_id"), "message")
|
|
120
|
+
class AppConfig:
|
|
121
|
+
subcommand: Message|Console
|
|
122
|
+
|
|
123
|
+
class Message:
|
|
124
|
+
bot_id: int
|
|
125
|
+
"""
|
|
126
|
+
parts = dest.split(".")
|
|
127
|
+
current_cls = class_
|
|
128
|
+
for part, class_name in zip(parts, crawled):
|
|
129
|
+
if not isinstance(current_cls, type): # `(Message | Console)`
|
|
130
|
+
for cl in get_args(current_cls):
|
|
131
|
+
if cl.__name__.casefold() == class_name:
|
|
132
|
+
current_cls = cl
|
|
133
|
+
break
|
|
134
|
+
else:
|
|
135
|
+
raise KeyError(f"Field {part!r} not accessible in {current_cls}")
|
|
136
|
+
|
|
137
|
+
hints = get_type_hints(current_cls)
|
|
138
|
+
|
|
139
|
+
if part not in hints:
|
|
140
|
+
raise KeyError(f"Field {part!r} not found in {current_cls}")
|
|
141
|
+
current_cls = hints[part] # přejdi na typ dalšího levelu
|
|
142
|
+
return current_cls
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_or_create_parent_dict(d: dict, fname: str, ignore_last=False) -> dict:
|
|
146
|
+
"""
|
|
147
|
+
Return the subdict for the path in `fname`, but ignore the last part.
|
|
148
|
+
If a subdict does not exist, create it.
|
|
149
|
+
If `fname` has only one part, return `d` directly.
|
|
150
|
+
"""
|
|
151
|
+
parts = fname.split(".")
|
|
152
|
+
# if len(parts) == 1:
|
|
153
|
+
# return d
|
|
154
|
+
if ignore_last:
|
|
155
|
+
parts = parts[:-1]
|
|
156
|
+
|
|
157
|
+
current = d
|
|
158
|
+
for part in parts:
|
|
159
|
+
if part not in current or not isinstance(current[part], dict):
|
|
160
|
+
current[part] = {}
|
|
161
|
+
current = current[part]
|
|
162
|
+
return current
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# NOTE Deprecated
|
|
166
|
+
# def get_nested_class(class_: type, fname: str, ignore_last=False) -> type:
|
|
167
|
+
# """
|
|
168
|
+
# Traverse the class attributes according to the dot-separated path in `fname`
|
|
169
|
+
# and return the type of the most deeply nested attribute.
|
|
170
|
+
# Works for dataclasses, Pydantic models, and attrs classes.
|
|
171
|
+
# """
|
|
172
|
+
# parts = fname.split(".")
|
|
173
|
+
# if ignore_last:
|
|
174
|
+
# parts = parts[:-1]
|
|
175
|
+
# current = class_
|
|
176
|
+
|
|
177
|
+
# for part in parts:
|
|
178
|
+
# if not hasattr(current, part):
|
|
179
|
+
# raise AttributeError(f"{part} not found in {current}")
|
|
180
|
+
# current = getattr(current, part)
|
|
181
|
+
|
|
182
|
+
# return current
|
|
183
|
+
|
|
184
|
+
|
|
94
185
|
def matches_annotation(value, annotation) -> bool:
|
|
95
186
|
"""Check whether the value type corresponds to the annotation.
|
|
96
187
|
Because built-in isinstance is not enough, it cannot determine parametrized generics.
|
|
@@ -192,6 +283,17 @@ def merge_dicts(d1: dict, d2: dict):
|
|
|
192
283
|
d1[key] = value
|
|
193
284
|
return d1
|
|
194
285
|
|
|
286
|
+
def dict_diff(a: dict, b: dict) -> dict:
|
|
287
|
+
""" Returns the B values where they differ. """
|
|
288
|
+
result = {}
|
|
289
|
+
for k in b:
|
|
290
|
+
if isinstance(a.get(k), dict) and isinstance(b.get(k), dict):
|
|
291
|
+
nested = dict_diff(a[k], b[k])
|
|
292
|
+
if nested:
|
|
293
|
+
result[k] = nested
|
|
294
|
+
elif a.get(k) != b.get(k):
|
|
295
|
+
result[k] = b[k]
|
|
296
|
+
return result
|
|
195
297
|
|
|
196
298
|
def naturalsize(value: float | str, *args) -> str:
|
|
197
299
|
"""For a bare interface, humanize might not be installed."""
|
|
@@ -227,6 +329,7 @@ def validate_annotated_type(meta, value) -> bool:
|
|
|
227
329
|
raise NotImplementedError(f"Unknown predicated {meta}")
|
|
228
330
|
return True
|
|
229
331
|
|
|
332
|
+
|
|
230
333
|
def allows_none(annotation) -> bool:
|
|
231
334
|
"""True, if annotation allows None: `int | None`, `Optional[int]`, `Union[int,None]`."""
|
|
232
335
|
if annotation is None:
|
|
@@ -238,4 +341,33 @@ def allows_none(annotation) -> bool:
|
|
|
238
341
|
|
|
239
342
|
if origin is Union or origin is UnionType:
|
|
240
343
|
return any(arg is type(None) for arg in args)
|
|
241
|
-
return False
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
def strip_none(annotation):
|
|
347
|
+
"""Return the same annotation but without NoneType inside a Union/Optional."""
|
|
348
|
+
origin = get_origin(annotation)
|
|
349
|
+
|
|
350
|
+
if origin is Union or origin is UnionType:
|
|
351
|
+
args = tuple(arg for arg in get_args(annotation) if arg is not type(None))
|
|
352
|
+
if len(args) == 1:
|
|
353
|
+
return args[0]
|
|
354
|
+
return Union[args] # nebo origin[args], aby se zachoval typ
|
|
355
|
+
|
|
356
|
+
return annotation
|
|
357
|
+
|
|
358
|
+
@lru_cache(maxsize=1024*10)
|
|
359
|
+
def _get_origin(tp: Any):
|
|
360
|
+
"""
|
|
361
|
+
Cached version of typing.get_origin.
|
|
362
|
+
Faster when called repeatedly on the same type hints.
|
|
363
|
+
"""
|
|
364
|
+
return get_origin(tp)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def remove_empty_dicts(d: dict):
|
|
368
|
+
"""Recursively remove empty dicts from a nested dict, in place."""
|
|
369
|
+
for k in list(d):
|
|
370
|
+
if isinstance(d[k], dict):
|
|
371
|
+
remove_empty_dicts(d[k])
|
|
372
|
+
if not d[k]:
|
|
373
|
+
d.pop(k)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Sequence
|
|
5
|
+
|
|
6
|
+
from .form_dict import EnvClass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CliFlags:
|
|
10
|
+
|
|
11
|
+
_add_verbose: bool = False
|
|
12
|
+
version: bool | str = False
|
|
13
|
+
_add_quiet: bool = False
|
|
14
|
+
|
|
15
|
+
default_verbosity: int = logging.WARNING
|
|
16
|
+
_verbosity_sequence: Optional[Sequence[int]] = None
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
add_verbose: bool | int | Sequence[int] = False,
|
|
21
|
+
add_version: Optional[str] = None,
|
|
22
|
+
add_version_package: Optional[str] = None,
|
|
23
|
+
add_quiet: bool = False,
|
|
24
|
+
):
|
|
25
|
+
self._enabled = {"verbose": True, "version": True, "quiet": True}
|
|
26
|
+
# verbosity
|
|
27
|
+
match add_verbose:
|
|
28
|
+
case bool():
|
|
29
|
+
self._add_verbose = add_verbose
|
|
30
|
+
case int():
|
|
31
|
+
self._add_verbose = True
|
|
32
|
+
self.default_verbosity = add_verbose
|
|
33
|
+
self._verbosity_sequence = list(range(add_verbose-10, -1, -10))
|
|
34
|
+
case list() | tuple():
|
|
35
|
+
self._add_verbose = True
|
|
36
|
+
self.default_verbosity = add_verbose[0]
|
|
37
|
+
self._verbosity_sequence = add_verbose[1:]
|
|
38
|
+
self._add_quiet = add_quiet
|
|
39
|
+
|
|
40
|
+
# version
|
|
41
|
+
if add_version:
|
|
42
|
+
self.version = add_version
|
|
43
|
+
elif add_version_package:
|
|
44
|
+
try:
|
|
45
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
46
|
+
except ImportError:
|
|
47
|
+
self.version = f"cannot determine version"
|
|
48
|
+
try:
|
|
49
|
+
self.version = version(add_version_package)
|
|
50
|
+
except PackageNotFoundError:
|
|
51
|
+
self.version = f"package {add_version_package} not found"
|
|
52
|
+
|
|
53
|
+
def should_add(self, env_classes: list[EnvClass]) -> bool:
|
|
54
|
+
# Flags are added only if neither the env_class nor any of the subcommands have the same-name flag already
|
|
55
|
+
self._enabled["verbose"] = self._add_verbose and self._attr_not_present("verbose", env_classes)
|
|
56
|
+
self._enabled["quiet"] = self._add_quiet and self._attr_not_present("quiet", env_classes)
|
|
57
|
+
self._enabled["version"] = self.version and self._attr_not_present("version", env_classes)
|
|
58
|
+
|
|
59
|
+
return self.add_verbose or self.add_version or self.add_quiet
|
|
60
|
+
|
|
61
|
+
def _attr_not_present(self, flag, env_classes):
|
|
62
|
+
return all(flag not in cl.__annotations__ for cl in env_classes)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def add_verbose(self):
|
|
66
|
+
return self._add_verbose and self._enabled["verbose"]
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def add_version(self):
|
|
70
|
+
return self.version and self._enabled["version"]
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def add_quiet(self):
|
|
74
|
+
return self._add_quiet and self._enabled["quiet"]
|
|
75
|
+
|
|
76
|
+
def get_log_level(self, count):
|
|
77
|
+
"""
|
|
78
|
+
Ex.
|
|
79
|
+
* add_verbose = True ( default level = WARNING )
|
|
80
|
+
* -v -> logging.INFO
|
|
81
|
+
* -vv -> logging.DEBUG
|
|
82
|
+
* -vvv -> logging.NOTSET
|
|
83
|
+
* add_verbose = default level INFO
|
|
84
|
+
* -v -> logging.DEBUG
|
|
85
|
+
* -vv -> logging.NOTSET
|
|
86
|
+
* add_verbose = (40, 35, 30, 25)
|
|
87
|
+
* -v -> 35
|
|
88
|
+
* -vv -> logging.INFO
|
|
89
|
+
* -vvv -> 25
|
|
90
|
+
* -vvv -> logging.NOTSET
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
count: number of times `--verbose` flag is used. Negative count means the `--quiet` flag is used.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
int: log level
|
|
97
|
+
"""
|
|
98
|
+
if count == -1: # quiet flag
|
|
99
|
+
return logging.ERROR
|
|
100
|
+
if not count:
|
|
101
|
+
return self.default_verbosity
|
|
102
|
+
if not self._verbosity_sequence:
|
|
103
|
+
seq = logging.INFO, logging.DEBUG
|
|
104
|
+
else:
|
|
105
|
+
seq = self._verbosity_sequence
|
|
106
|
+
log_level = {i + 1: level for i, level in enumerate(seq)}.get(count, logging.NOTSET)
|
|
107
|
+
return log_level
|