mininterface 1.1.1__tar.gz → 1.1.3__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 (74) hide show
  1. {mininterface-1.1.1 → mininterface-1.1.3}/PKG-INFO +3 -4
  2. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/__main__.py +1 -0
  3. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/cli_parser.py +41 -8
  4. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/cli_utils.py +19 -0
  5. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/config_file.py +6 -10
  6. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/dataclass_creation.py +26 -10
  7. mininterface-1.1.3/mininterface/_lib/future_compatibility.py +9 -0
  8. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/run.py +26 -14
  9. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/start.py +11 -11
  10. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_mininterface/__init__.py +19 -3
  11. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_mininterface/adaptor.py +16 -0
  12. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_mininterface/mixin.py +6 -6
  13. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_text_interface/__init__.py +10 -6
  14. mininterface-1.1.3/mininterface/_text_interface/timeout.py +129 -0
  15. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/adaptor.py +6 -5
  16. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/button_contents.py +3 -3
  17. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/textual_app.py +13 -10
  18. mininterface-1.1.3/mininterface/_textual_interface/timeout.py +39 -0
  19. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/widgets.py +8 -0
  20. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/adaptor.py +15 -6
  21. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/select_input.py +1 -1
  22. mininterface-1.1.3/mininterface/_tk_interface/timeout.py +41 -0
  23. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/utils.py +4 -3
  24. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_web_interface/child_adaptor.py +6 -1
  25. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/interfaces.py +5 -0
  26. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/settings.py +124 -2
  27. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/flag.py +1 -1
  28. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/select_tag.py +11 -1
  29. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/tag.py +23 -3
  30. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/tag_factory.py +9 -1
  31. {mininterface-1.1.1 → mininterface-1.1.3}/pyproject.toml +4 -3
  32. mininterface-1.1.1/mininterface/_lib/future_compatibility.py +0 -6
  33. {mininterface-1.1.1 → mininterface-1.1.3}/LICENSE +0 -0
  34. {mininterface-1.1.1 → mininterface-1.1.3}/README.md +0 -0
  35. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/__init__.py +0 -0
  36. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/__init__.py +0 -0
  37. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/argparse_support.py +0 -0
  38. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/auxiliary.py +0 -0
  39. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/cli_flags.py +0 -0
  40. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/form_dict.py +0 -0
  41. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/redirectable.py +0 -0
  42. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/shortcuts.py +0 -0
  43. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/showcase.py +0 -0
  44. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_lib/tyro_patches.py +0 -0
  45. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_text_interface/adaptor.py +0 -0
  46. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_text_interface/facet.py +0 -0
  47. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/__init__.py +0 -0
  48. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/facet.py +0 -0
  49. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/file_picker_input.py +0 -0
  50. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/form_contents.py +0 -0
  51. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/secret_input.py +0 -0
  52. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_textual_interface/style.tcss +0 -0
  53. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/__init__.py +0 -0
  54. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/date_entry.py +0 -0
  55. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/external_fix.py +0 -0
  56. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/facet.py +0 -0
  57. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
  58. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_tk_interface/secret_entry.py +0 -0
  59. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_web_interface/__init__.py +0 -0
  60. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_web_interface/app.py +0 -0
  61. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/_web_interface/parent_adaptor.py +0 -0
  62. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/cli.py +0 -0
  63. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/exceptions.py +0 -0
  64. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/experimental.py +0 -0
  65. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/facet/__init__.py +0 -0
  66. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/__init__.py +0 -0
  67. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/alias.py +0 -0
  68. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/callback_tag.py +0 -0
  69. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/datetime_tag.py +0 -0
  70. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/internal.py +0 -0
  71. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/path_tag.py +0 -0
  72. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/secret_tag.py +0 -0
  73. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/tag/type_stubs.py +0 -0
  74. {mininterface-1.1.1 → mininterface-1.1.3}/mininterface/validators.py +0 -0
@@ -1,19 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mininterface
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary: A minimal access to GUI, TUI, CLI and config
5
5
  License: LGPL-3.0-or-later
6
6
  License-File: LICENSE
7
7
  Author: Edvard Rejthar
8
8
  Author-email: edvard.rejthar@nic.cz
9
- Requires-Python: >=3.10,<4.0
9
+ Requires-Python: >=3.10,<3.14
10
10
  Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Programming Language :: Python :: 3.14
17
16
  Provides-Extra: all
18
17
  Provides-Extra: basic
19
18
  Provides-Extra: gui
@@ -34,7 +33,7 @@ Requires-Dist: tkcalendar ; extra == "gui" or extra == "ui" or extra == "all"
34
33
  Requires-Dist: tkinter-tooltip ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
35
34
  Requires-Dist: tkinter_form (==0.2.1) ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
36
35
  Requires-Dist: tkscrollableframe ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
37
- Requires-Dist: tyro (>=0.9,<0.10) ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
36
+ Requires-Dist: tyro (>=0.9.31,<0.10.0) ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
38
37
  Project-URL: Homepage, https://github.com/CZ-NIC/mininterface
39
38
  Description-Content-Type: text/markdown
40
39
 
@@ -33,6 +33,7 @@ Showcase_Type = Literal[1, 2]
33
33
  # @dataclass
34
34
  # class SharedLabel(Command):
35
35
  # text: Positional[str]
36
+ # NOTE implement `timeout`
36
37
 
37
38
 
38
39
  @dataclass
@@ -2,16 +2,17 @@
2
2
  # CLI and config file parsing.
3
3
  #
4
4
  from dataclasses import asdict
5
+ from functools import reduce
5
6
  import sys
6
7
  from collections import deque
7
8
  from contextlib import ExitStack
8
- from typing import Optional, Sequence, Type, Union
9
+ from typing import Annotated, Optional, Sequence, Type, Union
9
10
  from unittest.mock import patch
10
11
 
11
-
12
12
  from .cli_flags import CliFlags
13
13
 
14
14
  from ..cli import Command
15
+ from ..settings import CliSettings
15
16
 
16
17
  from ..exceptions import Cancelled
17
18
  from .auxiliary import (
@@ -33,6 +34,7 @@ try:
33
34
  from tyro._argparse import _SubParsersAction, ArgumentParser
34
35
  from tyro._argparse_formatter import TyroArgumentParser
35
36
  from tyro._singleton import MISSING_NONPROP
37
+ from tyro.conf import OmitArgPrefixes, OmitSubcommandPrefixes, DisallowNone, FlagCreatePairsOff
36
38
 
37
39
  from .tyro_patches import (
38
40
  _crawling,
@@ -75,6 +77,7 @@ def parse_cli(
75
77
  ask_for_missing: bool = True,
76
78
  args: Optional[Sequence[str]] = None,
77
79
  ask_on_empty_cli: Optional[bool] = None,
80
+ cli_settings: Optional[CliSettings] = None,
78
81
  _crawled=None,
79
82
  _req_fields=None,
80
83
  ) -> tuple[EnvClass, bool]:
@@ -118,18 +121,43 @@ def parse_cli(
118
121
  # Special CLI parsing
119
122
  if sys.modules.get("mininterface.tag.flag") is not None:
120
123
  from ..tag.flag import _custom_registry
121
- else: # run only if the user imported the flags. (Untested) performance reasons.
124
+ else: # run only if the user imported the flags. (Untested) performance reasons.
122
125
  _custom_registry = None
123
126
 
127
+ annotations = None
128
+ if cli_settings:
129
+ annotations = [
130
+ cls
131
+ for cond, cls in (
132
+ (cli_settings.omit_arg_prefixes, OmitArgPrefixes),
133
+ (cli_settings.omit_subcommand_prefixes, OmitSubcommandPrefixes),
134
+ (cli_settings.disallow_none, DisallowNone),
135
+ (cli_settings.flag_create_pairs_off, FlagCreatePairsOff),
136
+ )
137
+ if cond
138
+ ]
139
+
140
+ def annot(type_form):
141
+ if annotations:
142
+ if sys.version_info >= (3, 11):
143
+ from .future_compatibility import spread_annotated
144
+
145
+ return spread_annotated(type_form, annotations)
146
+ else:
147
+ from warnings import warn
148
+
149
+ warn(f"Cannot apply {annotations} on Python <= 3.11.")
150
+ return type_form
151
+
124
152
  try:
125
153
  with ExitStack() as stack:
126
154
  [stack.enter_context(p) for p in patches] # apply just the chosen mocks
127
155
  try:
128
- env = cli(type_form, args=args, registry=_custom_registry, **kwargs)
156
+ env = cli(annot(type_form), args=args, registry=_custom_registry, **kwargs)
129
157
  except BaseException:
130
158
  # Why this exception handling? Try putting this out and test_strange_error_mitigation fails.
131
159
  if len(env_classes) > 1 and kwargs.get("default"):
132
- env = cli(kwargs["default"].__class__, args=args[1:], registry=_custom_registry, **kwargs)
160
+ env = cli(annot(kwargs["default"].__class__), args=args[1:], registry=_custom_registry, **kwargs)
133
161
  else:
134
162
  raise
135
163
  # Why setting m.env instead of putting into into a constructor of a new get_interface() call?
@@ -144,7 +172,9 @@ def parse_cli(
144
172
  m.env = env
145
173
  except BaseException as exception:
146
174
  if ask_for_missing and getattr(exception, "code", None) == 2 and failed_fields.get():
147
- env = _dialog_missing(env_classes, kwargs, m, cf, ask_for_missing, args, _crawled, _req_fields)
175
+ env = _dialog_missing(
176
+ env_classes, kwargs, m, cf, ask_for_missing, args, cli_settings, _crawled, _req_fields
177
+ )
148
178
 
149
179
  if final_call:
150
180
  # Ask for the wrong fields
@@ -240,6 +270,7 @@ def _dialog_missing(
240
270
  cf: Optional[CliFlags],
241
271
  ask_for_missing: bool,
242
272
  args: Optional[Sequence[str]],
273
+ cli_settings,
243
274
  crawled,
244
275
  req_fields: Optional[TagDict],
245
276
  ) -> EnvClass:
@@ -301,7 +332,7 @@ def _dialog_missing(
301
332
  # We have just put a default values for missing fields so that tyro will not fail.
302
333
  # If we succeeded (no exotic case), this will pass through.
303
334
  # Then, we impose the user to fill the missing values.
304
- env, _ = parse_cli(env_classes, kwargs, m, cf, ask_for_missing, args, None, crawled, req_fields)
335
+ env, _ = parse_cli(env_classes, kwargs, m, cf, ask_for_missing, args, None, cli_settings, crawled, req_fields)
305
336
  td = dataclass_to_tagdict(env, m)
306
337
  # Remove teporary defaults to be correctly displayed in the dialog form
307
338
  # so that user must fill them.
@@ -345,7 +376,9 @@ def _fetch_currently_failed(requireds) -> TagDict:
345
376
  # Here, we pick the field unknown to the CLI parser too.
346
377
  # As whole subparser was unknown here, we safely consider all its fields wrong fields.
347
378
  if fname:
348
- get_or_create_parent_dict(missing_req, fname, True)[fname_raw] = get_or_create_parent_dict(requireds, fname)
379
+ get_or_create_parent_dict(missing_req, fname, True)[fname_raw] = get_or_create_parent_dict(
380
+ requireds, fname
381
+ )
349
382
  else:
350
383
  # This is the default subparser, without a field name:
351
384
  # ex. `run([List, Run])`
@@ -150,3 +150,22 @@ class SubcommandPlaceholder(Command):
150
150
 
151
151
 
152
152
  SubcommandPlaceholder.__name__ = "subcommand" # show just the shortcut in the CLI
153
+
154
+ # NOTE I'd like the method run to actually run here
155
+ # @dataclass
156
+ # class Console(Command):
157
+ # foo: str = "bar"
158
+ # def run(self):
159
+ # raise ValueError("DVA")
160
+ # self.interface.alert("Console!")
161
+ # @dataclass
162
+ # class Message(Command):
163
+ # text: str
164
+ # def run(self):
165
+ # raise ValueError("RAZ")
166
+ # @dataclass
167
+ # class Env:
168
+ # val: Message | Console
169
+ # m = run(Env) # here
170
+ # m = run([Message, Console]) # and here too
171
+ # Then, add is as a tip to Supported-types.md.
@@ -18,21 +18,19 @@ except ImportError:
18
18
  def parse_config_file(
19
19
  env_or_list: Type[EnvClass] | list[Type[EnvClass]],
20
20
  config_file: Path | None = None,
21
- settings: Optional[MininterfaceSettings] = None,
22
21
  **kwargs,
23
- ) -> tuple[dict, MininterfaceSettings | None]:
22
+ ) -> tuple[dict, dict | None]:
24
23
  """Fetches the config file into the program defaults kwargs["default"] and UI settings.
25
24
 
26
25
  Args:
27
26
  env_class: Class with the configuration.
28
27
  config_file: File to load YAML to be merged with the configuration.
29
28
  You do not have to re-define all the settings in the config file, you can choose a few.
30
- settings: Used to complement the 'mininterface' config file section-
31
29
  Kwargs:
32
30
  The same as for argparse.ArgumentParser.
33
31
 
34
32
  Returns:
35
- Tuple of kwargs and settings.
33
+ Tuple of kwargs and dict (section 'mininterface' in the config file).
36
34
  """
37
35
  if isinstance(env_or_list, list):
38
36
  subcommands, env = env_or_list, None
@@ -50,23 +48,21 @@ def parse_config_file(
50
48
  " Describe the developer your usecase so that they might implement this."
51
49
  )
52
50
 
51
+ confopt = None
53
52
  if "default" not in kwargs and not subcommands and config_file:
54
53
  # Undocumented feature. User put a namespace into kwargs["default"]
55
54
  # that already serves for defaults. We do not fetch defaults yet from a config file.
56
55
  disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
57
56
  try:
58
- if confopt := disk.pop("mininterface", None):
59
- # Section 'mininterface' in the config file.
60
- settings = _merge_settings(settings, confopt)
61
-
57
+ confopt = disk.pop("mininterface", None)
62
58
  kwargs["default"] = create_with_missing(env, disk)
63
59
  except TypeError:
64
60
  raise SyntaxError(f"Config file parsing failed for {config_file}")
65
61
 
66
- return kwargs, settings
62
+ return kwargs, confopt
67
63
 
68
64
 
69
- def _merge_settings(
65
+ def ensure_settings_inheritance(
70
66
  runopt: MininterfaceSettings | None, confopt: dict, _def_fact=MininterfaceSettings
71
67
  ) -> MininterfaceSettings:
72
68
  # Settings inheritance:
@@ -108,7 +108,7 @@ def _unwrap_annotated(tp):
108
108
  return tp
109
109
 
110
110
 
111
- def create_with_missing(env: T, disk: dict, wf: Optional[dict] = None, mint: Optional["Mininterface"] = None)->T:
111
+ def create_with_missing(env: T, disk: dict, wf: Optional[dict] = None, m: Optional["Mininterface"] = None)->T:
112
112
  """
113
113
  Create a default instance of an Env object. This is due to provent tyro to spawn warnings about missing fields.
114
114
  Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
@@ -116,6 +116,8 @@ def create_with_missing(env: T, disk: dict, wf: Optional[dict] = None, mint: Opt
116
116
  The result contains MISSING_NONPROP on the places the original Env object must have a value.
117
117
 
118
118
  And such fields are put into the wf (wrong_fields) dict.
119
+
120
+ Having the `m` defined means we build the dataclass. None means we are still in the config file parsing context.
119
121
  """
120
122
  # NOTE a test is missing
121
123
  # @dataclass
@@ -146,7 +148,7 @@ def create_with_missing(env: T, disk: dict, wf: Optional[dict] = None, mint: Opt
146
148
  # as the default value takes the precedence over the hard coded one, even if missing.
147
149
  out = {}
148
150
  missings: list[Tag] = []
149
- for name, v in proc_method(env, disk, wf, mint):
151
+ for name, v in proc_method(env, disk, wf, m):
150
152
  out[name] = v
151
153
  if v == MISSING_NONPROP and wf is not None:
152
154
  # For building config file, the MISSING_NONPROP is alright as we expect tyro to fail
@@ -174,7 +176,7 @@ def _process_pydantic(env, disk, wf: Optional[dict], m: Optional["Mininterface"]
174
176
  elif f.default is not None:
175
177
  v = f.default
176
178
  else:
177
- v = _process_field(name, f.annotation, MISSING, wf, m, default_value=MISSING)
179
+ v = _process_field(name, f.annotation, MISSING_NONPROP, wf, m, default_value=MISSING)
178
180
  yield name, v
179
181
 
180
182
 
@@ -187,7 +189,7 @@ def _process_attr(env, disk, wf: Optional[dict], m: Optional["Mininterface"] = N
187
189
  elif has_default:
188
190
  v = f.default
189
191
  else:
190
- v = _process_field(f.name, f.type, MISSING, wf, m, default_val)
192
+ v = _process_field(f.name, f.type, MISSING_NONPROP, wf, m, default_val)
191
193
  yield f.name, v
192
194
 
193
195
 
@@ -235,24 +237,38 @@ def _process_field(fname, ftype, disk_value, wf, m, default_value=MISSING):
235
237
 
236
238
  # the subcommand has been already chosen by CLI parser
237
239
  if isinstance(disk_value, ChosenSubcommand): # `(class Message | class Console)`
240
+ # there might be a subcommand prefix, ex. 'val:message' -> 'message'
241
+ name = disk_value.name.partition(":")[2] or disk_value.name
242
+
238
243
  for _subcomm in get_args(_unwrap_annotated(ftype)):
239
- if disk_value.name == to_kebab_case(_subcomm.__name__):
244
+ if name == to_kebab_case(_subcomm.__name__):
240
245
  ftype = _subcomm # `class Message` only
241
246
  disk_value = disk_value.subdict
242
247
  break
243
248
  else:
244
249
  raise ValueError(f"Type {disk_value} not found in {ftype}")
245
250
 
246
- if disk_value is not MISSING:
251
+ if disk_value is not MISSING_NONPROP:
247
252
  if _is_struct_type(ftype):
248
253
  return _init_struct_value(ftype, disk_value, wf, fname, m)
254
+ # NOTE for handling subcommands in config file, I'll need something like this,
255
+ # combined with a structure like ChosenSubcommand, possibly inheriting from MISSING_NONPROP
256
+ # to be ignored by tyro, yet fetchable by cli_parser
257
+ # elif _is_subcommands:
258
+ # return {cl.__name__: _init_struct_value(cl, {}, wf, fname, m) for cl in get_args(ftype)}
249
259
  return coerce_type_to_annotation(disk_value, ftype)
250
260
 
261
+ if _is_struct_type(ftype):
262
+ # Ex. `foo: Subcommand`
263
+ return _init_struct_value(ftype, {}, wf, fname, m)
264
+
251
265
  # We must handle the case when there are multiple subcommands possible.
252
266
  # The user decides now which way to go (choose a subcommand).
253
- if (origin is Union or origin is UnionType) and all(_is_struct_type(cl) for cl in get_args(ftype)):
254
- if not m: # we should not come here
255
- raise RuntimeError("Missing interface, report the issue please.")
267
+ # Ex. `foo: Subcommand1 | Subcommand2`
268
+ _is_subcommands = (origin is Union or origin is UnionType) and all(_is_struct_type(cl) for cl in get_args(ftype))
269
+ if _is_subcommands:
270
+ if not m: # we are parsing the config file. The fields are not defined in the config file
271
+ return MISSING_NONPROP
256
272
  ftype = choose_subcommand(get_args(ftype), m)
257
273
  return _init_struct_value(ftype, {}, wf, fname, m)
258
274
 
@@ -271,7 +287,7 @@ def _process_dataclass(env, disk, wf: Optional[dict], m: Optional["Mininterface"
271
287
  elif f.default is not MISSING:
272
288
  v = f.default
273
289
  else:
274
- v = _process_field(f.name, f.type, MISSING, wf, m)
290
+ v = _process_field(f.name, f.type, MISSING_NONPROP, wf, m)
275
291
  yield f.name, v
276
292
 
277
293
 
@@ -0,0 +1,9 @@
1
+
2
+ from typing import Annotated, Literal
3
+
4
+
5
+ def literal(c):
6
+ return Literal[*c]
7
+
8
+ def spread_annotated(obj, annotations):
9
+ return Annotated[obj, *annotations]
@@ -5,10 +5,11 @@ from os import environ
5
5
  from pathlib import Path
6
6
  from typing import Literal, Optional, Sequence, Type
7
7
 
8
+
8
9
  from .._mininterface import Mininterface
9
10
  from ..exceptions import DependencyRequired, ValidationFail
10
11
  from ..interfaces import get_interface
11
- from ..settings import MininterfaceSettings
12
+ from ..settings import CliSettings, MininterfaceSettings, UiSettings
12
13
  from .form_dict import EnvClass
13
14
 
14
15
  try:
@@ -16,11 +17,11 @@ try:
16
17
  from .argparse_support import parser_to_dataclass
17
18
  from .cli_flags import CliFlags as _CliFlags
18
19
  from .cli_parser import assure_args, parse_cli
19
- from .config_file import parse_config_file
20
+ from .config_file import parse_config_file, ensure_settings_inheritance
20
21
  from .dataclass_creation import choose_subcommand, to_kebab_case
21
22
  from .start import Start
22
23
  except DependencyRequired as e:
23
- assure_args, parse_cli, parse_config_file, parser_to_dataclass = (e,) * 4
24
+ assure_args, parse_cli, parse_config_file, ensure_settings_inheritance, parser_to_dataclass = (e,) * 5
24
25
  Start, SubcommandPlaceholder = (e,) * 2
25
26
  to_kebab_case, choose_subcommand, _CliFlags = (e,) * 3
26
27
 
@@ -40,7 +41,7 @@ def run(
40
41
  # We do not use InterfaceType as a type here because we want the documentation to show full alias:
41
42
  interface: Type[Mininterface] | Literal["gui"] | Literal["tui"] | Literal["text"] | Literal["web"] | None = None,
42
43
  args: Optional[Sequence[str]] = None,
43
- settings: Optional[MininterfaceSettings] = None,
44
+ settings: Optional[MininterfaceSettings | UiSettings | CliSettings] = None,
44
45
  **kwargs,
45
46
  ) -> Mininterface[EnvClass]:
46
47
  """The main access, start here.
@@ -192,7 +193,7 @@ def run(
192
193
  see the full [list](Interfaces.md) of possible interfaces.
193
194
  If not set, we look also for an environment variable [`MININTERFACE_INTERFACE`](Interfaces.md#environment-variable-mininterface_interface) and in the config file.
194
195
  args: Parse arguments from a sequence instead of the command line.
195
- settings: Default settings. These might be further modified by the 'mininterface' section in the config file.
196
+ settings: Default settings. These might be further modified by the 'mininterface' section in the config file. See the [Settings](Settings.md) section.
196
197
  Kwargs:
197
198
  The same as for [argparse.ArgumentParser](https://docs.python.org/3/library/argparse.html).
198
199
 
@@ -275,8 +276,22 @@ def run(
275
276
  if isinstance(env_or_list, ArgumentParser):
276
277
  env_or_list, add_version = parser_to_dataclass(env_or_list)
277
278
 
278
- # Parse CLI arguments, possibly merged from a config file.
279
- # A) Superform overview of the subcommands
279
+ # Parse config file
280
+ kwargs, settings_conf = parse_config_file(env_or_list or _Empty, config_file, **kwargs)
281
+
282
+ # Ensure settings inheritance
283
+ if isinstance(settings, CliSettings):
284
+ settings = MininterfaceSettings(cli=settings)
285
+ elif isinstance(settings, UiSettings):
286
+ # Ex. `settings=GuiSettings(...)` -> `settings=MininterfaceSettings(gui=GuiSettings(...)`
287
+ attr_name = settings.__class__.__name__.removesuffix("Settings").lower()
288
+ settings = MininterfaceSettings(**{attr_name: settings})
289
+ if settings or settings_conf:
290
+ # previous settings are used to complement the 'mininterface' config file section
291
+ settings = ensure_settings_inheritance(settings, settings_conf or {})
292
+ cliset = settings.cli if settings else CliSettings()
293
+
294
+ # Choose an interface
280
295
  m = get_interface(interface, title, settings)
281
296
 
282
297
  # Resolve SubcommandPlaceholder
@@ -290,17 +305,14 @@ def run(
290
305
  ):
291
306
  args[0] = to_kebab_case(choose_subcommand(env_or_list, m).__name__)
292
307
 
293
- # B) A single Env object, or a list of such objects (with one is being selected via args)
294
- # C) No Env object
295
-
296
- kwargs, settings = parse_config_file(env_or_list or _Empty, config_file, settings, **kwargs)
308
+ # Parse CLI arguments, possibly merged from a config file.
297
309
  cf = _CliFlags(add_verbose, add_version, add_version_package, add_quiet)
298
310
  if env_or_list:
299
- # B) single Env object
311
+ # A single Env object, or a list of such objects (with one is not/being selected via args)
300
312
  # Load configuration from CLI and a config file
301
313
  try:
302
314
  parse_cli(
303
- env_or_list, kwargs, m, cf, ask_for_missing, args, ask_on_empty_cli
315
+ env_or_list, kwargs, m, cf, ask_for_missing, args, ask_on_empty_cli, cliset
304
316
  )
305
317
  except Exception as e:
306
318
  # Undocumented MININTERFACE_DEBUG flag. Note ipdb package requirement.
@@ -321,7 +333,7 @@ def run(
321
333
  else:
322
334
  # C) No Env object
323
335
  # even though there is no configuration, yet we need to parse CLI for meta-commands like --help or --verbose
324
- parse_cli(_Empty, {}, m, cf, ask_for_missing, args)
336
+ parse_cli(_Empty, {}, m, cf, ask_for_missing, args, None, cliset)
325
337
 
326
338
  return m
327
339
 
@@ -1,23 +1,23 @@
1
1
  # Starting and maintaining a program, using mininterface, in the system.
2
2
  import sys
3
- from collections import defaultdict
4
- from dataclasses import is_dataclass
3
+ # from collections import defaultdict
4
+ # from dataclasses import is_dataclass
5
5
  from pathlib import Path
6
6
  from subprocess import run
7
7
  from typing import Optional, Type
8
- from warnings import warn
8
+ # from warnings import warn
9
9
 
10
10
  from .._mininterface import Mininterface
11
- from ..cli import Command, SubcommandPlaceholder
12
- from ..exceptions import DependencyRequired
11
+ # from ..cli import Command, SubcommandPlaceholder
12
+ # from ..exceptions import DependencyRequired
13
13
  from ..interfaces import get_interface
14
- from ..tag import Tag
15
- from .form_dict import DataClass, EnvClass, TagDict, dataclass_to_tagdict
14
+ # from ..tag import Tag
15
+ # from .form_dict import DataClass, EnvClass, TagDict, dataclass_to_tagdict
16
16
 
17
- try:
18
- from .cli_parser import parse_cli
19
- except DependencyRequired as e:
20
- parse_cli = e
17
+ # try:
18
+ # from .cli_parser import parse_cli
19
+ # except DependencyRequired as e:
20
+ # parse_cli = e
21
21
 
22
22
 
23
23
  class Start:
@@ -90,6 +90,7 @@ class Mininterface(Generic[EnvClass]):
90
90
  # NOTE docs that
91
91
  # m = run([Env, Env2]) -> .env will be the chosen one.
92
92
 
93
+ # NOTE In Python3.14, type(self).__annotations__ will work.
93
94
  self._adaptor = self.__annotations__["_adaptor"](self, settings)
94
95
 
95
96
  def __enter__(self) -> "Self":
@@ -164,8 +165,13 @@ class Mininterface(Generic[EnvClass]):
164
165
  def __exit__(self, *_):
165
166
  pass
166
167
 
167
- def alert(self, text: str) -> None:
168
- """Prompt the user to confirm the text."""
168
+ def alert(self, text: str, *, timeout: int = 0) -> None:
169
+ """Prompt the user to confirm the text.
170
+
171
+ Args:
172
+ text: Displayed text
173
+ timeout: Auto-confirm after N seconds (0 = disabled).
174
+ """
169
175
  print("Alert text", text)
170
176
  return
171
177
 
@@ -255,7 +261,7 @@ class Mininterface(Generic[EnvClass]):
255
261
  # Output:
256
262
  return assure_tag(annotation, validation)._make_default_value()
257
263
 
258
- def confirm(self, text: str, default: bool = True) -> bool:
264
+ def confirm(self, text: str, default: bool = True, *, timeout: int = 0) -> bool:
259
265
  """Display confirm box and returns bool.
260
266
 
261
267
  ```python
@@ -265,9 +271,19 @@ class Mininterface(Generic[EnvClass]):
265
271
 
266
272
  ![Is yes window](asset/is_yes.avif "A prompted dialog")
267
273
 
274
+ Automatically submit after 10 seconds with the `timeout` parameter.
275
+
276
+ ```python
277
+ m = run()
278
+ print(m.confirm("Is that alright?"), timeout=10) # True/False
279
+ ```
280
+
281
+ ![Is yes window](asset/confirm_countdown.avif "A prompted dialog with timeout")
282
+
268
283
  Args:
269
284
  text: Displayed text.
270
285
  default: Focus the button with this value.
286
+ timeout: Auto-confirm after N seconds (0 = disabled).
271
287
 
272
288
  Returns:
273
289
  bool: Whether the user has chosen the Yes button.
@@ -26,6 +26,7 @@ class BackendAdaptor(ABC):
26
26
 
27
27
  def __init__(self, interface: "Mininterface", settings: UiSettings | None):
28
28
  self.interface = interface
29
+ # NOTE self.__annotations__ will pose problem at Python3.14
29
30
  self.facet = interface.facet = self.__annotations__["facet"](self, interface.env)
30
31
  self.settings = settings or self.__annotations__["settings"]()
31
32
 
@@ -140,3 +141,18 @@ class MinAdaptor(BackendAdaptor):
140
141
  raise SystemExit(validation_fails)
141
142
 
142
143
  return form
144
+
145
+ class Timeout(ABC):
146
+ """ Auto-submit dialogs """
147
+
148
+ def __init__(self, timeout: int, adaptor: "BackendAdaptor"):
149
+ self.timeout = timeout
150
+ self.adaptor = adaptor
151
+
152
+ @abstractmethod
153
+ def countdown(self):
154
+ ...
155
+
156
+ @abstractmethod
157
+ def cancel(self):
158
+ ...
@@ -13,12 +13,12 @@ from . import Mininterface
13
13
  class RichUiMixin(Mininterface):
14
14
  _adaptor: "RichUiAdaptor"
15
15
 
16
- def alert(self, text: str) -> None:
16
+ def alert(self, text: str, *, timeout: int = 0) -> None:
17
17
  """Display the OK dialog with text."""
18
- self._adaptor.buttons(text, [("Ok", None)])
18
+ self._adaptor.buttons(text, [("Ok", None)], timeout=timeout)
19
19
 
20
- def confirm(self, text, default: bool = True) -> bool:
21
- return self._adaptor.yes_no(text, not default)
20
+ def confirm(self, text, default: bool = True, *, timeout: int = 0) -> bool:
21
+ return self._adaptor.yes_no(text, not default, timeout=timeout)
22
22
 
23
23
  def ask(
24
24
  self,
@@ -36,7 +36,7 @@ class RichUiMixin(Mininterface):
36
36
 
37
37
  class RichUiAdaptor(ABC):
38
38
  @abstractmethod
39
- def yes_no(self, text: str, focus_no=True): ...
39
+ def yes_no(self, text: str, focus_no=True, *, timeout: int = 0)->bool: ...
40
40
 
41
41
  @abstractmethod
42
- def buttons(self, text: str, buttons: list[tuple[str, Any]], focused: int = 1): ...
42
+ def buttons(self, text: str, buttons: list[tuple[str, Any]], focused: int = 1, *, timeout: int = 0): ...
@@ -2,12 +2,14 @@ import sys
2
2
  from pprint import pprint
3
3
  from typing import TYPE_CHECKING, Iterable, Type, TypeVar
4
4
 
5
+
5
6
  from ..tag.tag_factory import assure_tag
6
7
 
7
8
  from ..exceptions import Cancelled, InterfaceNotAvailable
8
9
  from .._lib.form_dict import DataClass, EnvClass, FormDict, tag_assure_type
9
10
  from .._mininterface import Mininterface
10
11
  from ..tag.tag import Tag, TagValue, ValidationCallback
12
+ from .timeout import input_timeout
11
13
  from .adaptor import TextAdaptor
12
14
 
13
15
  if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self`
@@ -75,23 +77,25 @@ class TextInterface(AssureInteractiveTerminal, Mininterface):
75
77
 
76
78
  _adaptor: TextAdaptor
77
79
 
78
- def alert(self, text: str):
80
+ def alert(self, text: str, *, timeout: int = 0):
79
81
  """Display text and let the user hit any key."""
80
82
  with StdinTTYWrapper():
81
- input(text + " Hit any key.")
83
+ input_timeout(text + " Hit any key.", timeout, True)
82
84
 
83
85
  def ask(
84
86
  self,
85
87
  text: str,
86
88
  annotation: Type[TagValue] | Tag = str,
87
89
  validation: Iterable[ValidationCallback] | ValidationCallback | None = None,
90
+ *,
91
+ _timeout: int = 0
88
92
  ) -> TagValue:
89
93
  with StdinTTYWrapper():
90
94
  if not self.interactive:
91
95
  return super().ask(text, annotation=annotation, validation=validation)
92
96
  while True:
93
97
  try:
94
- txt = input(text + ": ") if text else input()
98
+ txt = input_timeout(text + ":" if text else "", _timeout)
95
99
  except EOFError:
96
100
  raise Cancelled(".. cancelled")
97
101
  t = assure_tag(annotation, validation)
@@ -133,20 +137,20 @@ class TextInterface(AssureInteractiveTerminal, Mininterface):
133
137
  pdb.set_trace()
134
138
  return form
135
139
 
136
- def confirm(self, text: str, default: bool = True):
140
+ def confirm(self, text: str, default: bool = True, *, timeout: int = 0):
137
141
  with StdinTTYWrapper():
138
142
  if default:
139
143
  t = text + " [y]/n"
140
144
  else:
141
145
  t = text + " y/[n]"
142
- val = self.ask(text=t).lower()
146
+ val = self.ask(text=t, _timeout=timeout).lower()
143
147
  if not val:
144
148
  return bool(default)
145
149
  if val in ("y", "yes", ""):
146
150
  return True
147
151
  if val in ("n", "no", ""):
148
152
  return False
149
- return self.confirm(text, default)
153
+ return self.confirm(text, default, timeout=timeout)
150
154
 
151
155
 
152
156
  class ReplInterface(TextInterface):