mininterface 1.1.2__tar.gz → 1.1.4__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.2 → mininterface-1.1.4}/PKG-INFO +4 -1
  2. {mininterface-1.1.2 → mininterface-1.1.4}/README.md +3 -1
  3. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/cli_parser.py +41 -8
  4. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/config_file.py +6 -10
  5. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/dataclass_creation.py +4 -1
  6. mininterface-1.1.4/mininterface/_lib/future_compatibility.py +9 -0
  7. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/run.py +26 -14
  8. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/start.py +11 -11
  9. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_mininterface/__init__.py +2 -1
  10. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_mininterface/adaptor.py +10 -2
  11. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_text_interface/timeout.py +6 -3
  12. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/timeout.py +5 -1
  13. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/timeout.py +4 -1
  14. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/utils.py +4 -3
  15. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/interfaces.py +5 -0
  16. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/settings.py +124 -2
  17. {mininterface-1.1.2 → mininterface-1.1.4}/pyproject.toml +1 -1
  18. mininterface-1.1.2/mininterface/_lib/future_compatibility.py +0 -6
  19. {mininterface-1.1.2 → mininterface-1.1.4}/LICENSE +0 -0
  20. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/__init__.py +0 -0
  21. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/__main__.py +0 -0
  22. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/__init__.py +0 -0
  23. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/argparse_support.py +0 -0
  24. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/auxiliary.py +0 -0
  25. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/cli_flags.py +0 -0
  26. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/cli_utils.py +0 -0
  27. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/form_dict.py +0 -0
  28. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/redirectable.py +0 -0
  29. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/shortcuts.py +0 -0
  30. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/showcase.py +0 -0
  31. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_lib/tyro_patches.py +0 -0
  32. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_mininterface/mixin.py +0 -0
  33. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_text_interface/__init__.py +0 -0
  34. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_text_interface/adaptor.py +0 -0
  35. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_text_interface/facet.py +0 -0
  36. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/__init__.py +0 -0
  37. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/adaptor.py +0 -0
  38. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/button_contents.py +0 -0
  39. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/facet.py +0 -0
  40. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/file_picker_input.py +0 -0
  41. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/form_contents.py +0 -0
  42. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/secret_input.py +0 -0
  43. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/style.tcss +0 -0
  44. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/textual_app.py +0 -0
  45. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_textual_interface/widgets.py +0 -0
  46. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/__init__.py +0 -0
  47. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/adaptor.py +0 -0
  48. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/date_entry.py +0 -0
  49. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/external_fix.py +0 -0
  50. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/facet.py +0 -0
  51. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
  52. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/secret_entry.py +0 -0
  53. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_tk_interface/select_input.py +0 -0
  54. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_web_interface/__init__.py +0 -0
  55. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_web_interface/app.py +0 -0
  56. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_web_interface/child_adaptor.py +0 -0
  57. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/_web_interface/parent_adaptor.py +0 -0
  58. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/cli.py +0 -0
  59. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/exceptions.py +0 -0
  60. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/experimental.py +0 -0
  61. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/facet/__init__.py +0 -0
  62. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/__init__.py +0 -0
  63. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/alias.py +0 -0
  64. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/callback_tag.py +0 -0
  65. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/datetime_tag.py +0 -0
  66. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/flag.py +0 -0
  67. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/internal.py +0 -0
  68. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/path_tag.py +0 -0
  69. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/secret_tag.py +0 -0
  70. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/select_tag.py +0 -0
  71. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/tag.py +0 -0
  72. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/tag_factory.py +0 -0
  73. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/tag/type_stubs.py +0 -0
  74. {mininterface-1.1.2 → mininterface-1.1.4}/mininterface/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mininterface
3
- Version: 1.1.2
3
+ Version: 1.1.4
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
@@ -131,6 +131,8 @@ with run(Env) as m:
131
131
 
132
132
  Wrapper between various libraries that provide a user interface.
133
133
 
134
+ It allows you to focus on writing the program's core logic without worrying about input/output. It generates CLI arguments, parses config file, and shows a UI. Depending on the endpoint, the dialogs automatically use either a GUI, a mouse-clickable TUI, a lightweight terminal text interface, or be available via HTTP — all while maintaining exactly the same functionality.
135
+
134
136
  Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. *Mininterface* is here to help.
135
137
 
136
138
  The config variables needed by your program are kept in cozy dataclasses. Write less! The syntax of [tyro](https://github.com/brentyi/tyro) does not require any overhead (as its `argparse` alternatives do). You just annotate a class attribute, append a simple docstring and get a fully functional application:
@@ -338,3 +340,4 @@ If you're sure enough to start using *Mininterface*, convert the argparse into a
338
340
 
339
341
  !!! warning
340
342
  The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
343
+
@@ -91,6 +91,8 @@ with run(Env) as m:
91
91
 
92
92
  Wrapper between various libraries that provide a user interface.
93
93
 
94
+ It allows you to focus on writing the program's core logic without worrying about input/output. It generates CLI arguments, parses config file, and shows a UI. Depending on the endpoint, the dialogs automatically use either a GUI, a mouse-clickable TUI, a lightweight terminal text interface, or be available via HTTP — all while maintaining exactly the same functionality.
95
+
94
96
  Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. *Mininterface* is here to help.
95
97
 
96
98
  The config variables needed by your program are kept in cozy dataclasses. Write less! The syntax of [tyro](https://github.com/brentyi/tyro) does not require any overhead (as its `argparse` alternatives do). You just annotate a class attribute, append a simple docstring and get a fully functional application:
@@ -297,4 +299,4 @@ print(m.env.time) # -> 14:21
297
299
  If you're sure enough to start using *Mininterface*, convert the argparse into a dataclass. Then, the IDE will auto-complete the hints as you type.
298
300
 
299
301
  !!! warning
300
- The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
302
+ The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
@@ -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])`
@@ -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:
@@ -237,8 +237,11 @@ def _process_field(fname, ftype, disk_value, wf, m, default_value=MISSING):
237
237
 
238
238
  # the subcommand has been already chosen by CLI parser
239
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
+
240
243
  for _subcomm in get_args(_unwrap_annotated(ftype)):
241
- if disk_value.name == to_kebab_case(_subcomm.__name__):
244
+ if name == to_kebab_case(_subcomm.__name__):
242
245
  ftype = _subcomm # `class Message` only
243
246
  disk_value = disk_value.subdict
244
247
  break
@@ -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,7 +90,8 @@ class Mininterface(Generic[EnvClass]):
90
90
  # NOTE docs that
91
91
  # m = run([Env, Env2]) -> .env will be the chosen one.
92
92
 
93
- self._adaptor = self.__annotations__["_adaptor"](self, settings)
93
+ # NOTE In Python3.14, type(self).__annotations__ will work.
94
+ self._adaptor = type(self).__annotations__["_adaptor"](self, settings)
94
95
 
95
96
  def __enter__(self) -> "Self":
96
97
  """Usage within the with statement makes the program to attempt for the following benefits:
@@ -26,8 +26,16 @@ class BackendAdaptor(ABC):
26
26
 
27
27
  def __init__(self, interface: "Mininterface", settings: UiSettings | None):
28
28
  self.interface = interface
29
- self.facet = interface.facet = self.__annotations__["facet"](self, interface.env)
30
- self.settings = settings or self.__annotations__["settings"]()
29
+
30
+ # Why looping mro? Since 3.14, ex. MockAdaptor does not inherit .facet annotation from MinAdaptor.
31
+ for cl in type(self).__mro__:
32
+ try:
33
+ self.facet = interface.facet = cl.__annotations__["facet"](self, interface.env)
34
+ self.settings = settings or cl.__annotations__["settings"]()
35
+ except KeyError:
36
+ pass
37
+ else:
38
+ break
31
39
 
32
40
  @abstractmethod
33
41
  def widgetize(self, tag: Tag):
@@ -60,6 +60,8 @@ def input_timeout(prompt: str, timeout: int = 0, exit_on_keypress: bool = False)
60
60
  return "".join(inp)
61
61
  elif char == '\x03': # Ctrl+C
62
62
  raise Cancelled
63
+ elif char == '\x1b': # Escape
64
+ raise Cancelled
63
65
  elif char == '\x08': # Backspace
64
66
  if inp:
65
67
  inp.pop()
@@ -74,7 +76,6 @@ def input_timeout(prompt: str, timeout: int = 0, exit_on_keypress: bool = False)
74
76
  print() # newline immediately
75
77
  return "".join(inp)
76
78
 
77
- # Stop if timeout exceeded and input not started
78
79
  if timeout_running and (time.time() - start_time >= timeout) and not input_started.is_set():
79
80
  input_started.set()
80
81
  print()
@@ -93,7 +94,9 @@ def input_timeout(prompt: str, timeout: int = 0, exit_on_keypress: bool = False)
93
94
  if char == '\n':
94
95
  print() # newline at Enter
95
96
  return "".join(inp)
96
- elif char == '\x03':
97
+ elif char == '\x03': # Ctrl+C
98
+ raise Cancelled
99
+ elif char == '\x1b': # Escape
97
100
  raise Cancelled
98
101
  elif char == '\x7f': # Backspace
99
102
  if inp:
@@ -109,13 +112,13 @@ def input_timeout(prompt: str, timeout: int = 0, exit_on_keypress: bool = False)
109
112
  print() # newline immediately
110
113
  return "".join(inp)
111
114
 
112
- # Stop if timeout exceeded and input not started
113
115
  if timeout_running and (time.time() - start_time >= timeout) and not input_started.is_set():
114
116
  input_started.set()
115
117
  print()
116
118
  return ""
117
119
  finally:
118
120
  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
121
+
119
122
  except Exception:
120
123
  # Fallback to class input.
121
124
  # 'Press any key' will not work.
@@ -16,7 +16,11 @@ class TextualTimeout(Timeout):
16
16
  self.button = button
17
17
  self.orig = self.button.label
18
18
  self._task = asyncio.create_task(self.countdown(timeout))
19
- self.button.set_blur_callback(lambda event=None: self.cancel())
19
+
20
+ # Cancel countdown on focusing out
21
+ # Why checking .focused? If we jump to another app with Alt+Tab, we do not want the countdown to stop
22
+ # (in such cases, .focused is empty).
23
+ self.button.set_blur_callback(lambda event=None: self.cancel() if adaptor.app.focused else None)
20
24
 
21
25
  async def countdown(self, count: int):
22
26
  self.button.label = f"{self.orig} ({count})"
@@ -18,7 +18,10 @@ class TkTimeout(Timeout):
18
18
 
19
19
  self.countdown(timeout)
20
20
 
21
- self.button.bind("<FocusOut>", self.cancel)
21
+ # Cancel countdown on FocusOut
22
+ # Why checking .focus_get()? If we jump to another app with Alt+Tab, we do not want the countdown to stop
23
+ # (in such cases, .focus_get() is empty).
24
+ self.button.bind("<FocusOut>", lambda e: self.cancel() if e.widget.focus_get() else None)
22
25
 
23
26
  def countdown(self, count):
24
27
  try:
@@ -18,7 +18,7 @@ from ..tag.internal import CallbackButtonWidget, FacetButtonWidget, SubmitButton
18
18
 
19
19
 
20
20
  from .._lib.auxiliary import flatten
21
- from .._lib.form_dict import TagDict
21
+ from .._lib.form_dict import TagDict, dict_removed_main
22
22
 
23
23
  from ..tag import Tag
24
24
  from ..tag.secret_tag import SecretTag
@@ -165,7 +165,8 @@ def replace_widgets(adaptor: "TkAdaptor", nested_widgets, form: TagDict):
165
165
  # Only prevent form submission, don't affect Tab navigation
166
166
  return None # Allow event propagation for other handlers
167
167
 
168
- for tag, field_form in zip(flatten(form), flatten(nested_widgets)):
168
+ fform = list(flatten(form))
169
+ for tag, field_form in zip(fform, flatten(nested_widgets)):
169
170
  tag: Tag
170
171
  field_form: FieldForm
171
172
  label1: Widget = field_form.label
@@ -193,7 +194,7 @@ def replace_widgets(adaptor: "TkAdaptor", nested_widgets, form: TagDict):
193
194
  match tag:
194
195
  case SelectTag():
195
196
  grid_info = widget.grid_info()
196
- wrapper = SelectInputWrapper(master, tag, grid_info, widget, adaptor, len(form) == 1)
197
+ wrapper = SelectInputWrapper(master, tag, grid_info, widget, adaptor, len(fform) == 1)
197
198
  select_tag = True
198
199
  variable = wrapper.variable_wrapper
199
200
  # since tkinter variables do not allow objects,
@@ -65,6 +65,11 @@ def _choose_settings(type_: Mininterface, settings: Optional[MininterfaceSetting
65
65
  opt = settings.text
66
66
  case "WebInterface":
67
67
  opt = settings.web
68
+ case "Mininterface":
69
+ # Even though this has not any sense to have some settings for Mininterface,
70
+ # they are not used, we test it in TestRun.test_settings_run.
71
+ # An according to MinAdaptor, it should have UiSettings.
72
+ opt = settings.ui
68
73
  return opt
69
74
 
70
75
 
@@ -5,6 +5,117 @@ from typing import Literal, Optional
5
5
  # We do not use InterfaceType as a type in run because we want the documentation to show full alias.
6
6
  InterfaceName = Literal["gui"] | Literal["tui"] | Literal["textual"] | Literal["text"]
7
7
 
8
+ @_dataclass
9
+ class CliSettings:
10
+ omit_arg_prefixes: bool = False
11
+ """ Simplify argument names by removing parent field prefixes from flags.
12
+
13
+ ```bash
14
+ $ ./program.py --help
15
+ # omit_arg_prefixes = False
16
+ usage: program.py [-h] [-v] --arg.subarg.text STR
17
+ # omit_arg_prefixes = True
18
+ usage: program.py [-h] [-v] --text STR
19
+ ```
20
+
21
+ ??? Code
22
+ ```python
23
+ @dataclass
24
+ class SubMessage:
25
+ text: str
26
+
27
+ @dataclass
28
+ class Message:
29
+ subarg: SubMessage
30
+
31
+ @dataclass
32
+ class Env:
33
+ arg: Message
34
+
35
+ run(Env, settings=CliSettings(omit_arg_prefixes=True/False))
36
+ ```
37
+
38
+ See: [https://brentyi.github.io/tyro/api/tyro/conf/#tyro.conf.OmitArgPrefixes]()
39
+ """
40
+
41
+ omit_subcommand_prefixes: bool = False
42
+ """
43
+ Simplify subcommand names by removing parent field prefixes from subcommands.
44
+
45
+ ```bash
46
+ $ ./program.py --help
47
+ # omit_subcommand_prefixes = False
48
+ usage: program.py [-h] [-v] {subcommand:message,subcommand:console}
49
+ # omit_subcommand_prefixes = True
50
+ usage: program.py [-h] [-v] {message,console}
51
+ ```
52
+
53
+ ??? Code
54
+ ```python
55
+ @dataclass
56
+ class SubMessage:
57
+ text: str
58
+
59
+ @dataclass
60
+ class Message:
61
+ subarg: SubMessage
62
+
63
+ @dataclass
64
+ class Env:
65
+ subcommand: Message | Console
66
+
67
+ run(Env, settings=CliSettings(omit_subcommand_prefixes=True/False))
68
+ ```
69
+
70
+ See: [https://brentyi.github.io/tyro/api/tyro/conf/#tyro.conf.OmitSubcommandPrefixes]()
71
+ """
72
+
73
+ disallow_none: bool = False
74
+ """ Disallow passing None in via the command-line interface for union types containing None.
75
+
76
+ ```bash
77
+ $ ./program.py --help
78
+ # disallow_none = False
79
+ usage: program.py [-h] [-v] [--field {None}|INT]
80
+ # disallow_none = True
81
+ usage: program.py [-h] [-v] [--field INT]
82
+ ```
83
+
84
+ ??? Code
85
+ ```python
86
+ @dataclass
87
+ class Env:
88
+ field: int | None = None
89
+ run(Env, settings=CliSettings(disallow_none=True/False))
90
+ ```
91
+
92
+ See: [https://brentyi.github.io/tyro/api/tyro/conf/#tyro.conf.DisallowNone]()
93
+ """
94
+
95
+ flag_create_pairs_off: bool = False
96
+ """ Disable creation of matching flag pairs for boolean types.
97
+
98
+ ```bash
99
+ $ ./program.py --help
100
+ # flag_create_pairs_off = False
101
+ usage: program.py [-h] [-v] [--foo | --no-foo]
102
+ # flag_create_pairs_off = True
103
+ usage: program.py [-h] [-v] [--foo]
104
+ ```
105
+
106
+ ??? Code
107
+ ```python
108
+ @dataclass
109
+ class Env:
110
+ foo: bool = False
111
+ run(Env, settings=CliSettings(flag_create_pairs_off=True/False))
112
+ ```
113
+
114
+ See: [https://brentyi.github.io/tyro/api/tyro/conf/#tyro.conf.FlagCreatePairsOff]()
115
+ """
116
+
117
+
118
+
8
119
  @_dataclass
9
120
  class UiSettings:
10
121
  toggle_widget: str = "f4"
@@ -37,9 +148,19 @@ class UiSettings:
37
148
  class GuiSettings(UiSettings):
38
149
  # If multiple Gui interfaces exist, this had to be TkSettings instead.
39
150
 
40
-
41
151
  combobox_since: int = 10
42
- """ The threshold to switch from radio buttons to a combobox. """
152
+ """ The threshold to switch from radio buttons to a combobox.
153
+
154
+ Without combobox:
155
+
156
+ ![Default](asset/configuration-not-used.avif)
157
+
158
+ With combobox:
159
+
160
+ ![Combobox](asset/configuration-used.avif)
161
+
162
+ (Note, there must be multiple fields for combobox to appear.)
163
+ """
43
164
 
44
165
  radio_select_on_focus: bool = False
45
166
  """ Select the radio button on focus. Ex. when navigating by arrows. """
@@ -122,5 +243,6 @@ class MininterfaceSettings:
122
243
  textual: TextualSettings = _field(default_factory=TextualSettings)
123
244
  text: TextSettings = _field(default_factory=TextSettings)
124
245
  web: WebSettings = _field(default_factory=WebSettings)
246
+ cli: CliSettings = _field(default_factory=CliSettings)
125
247
  interface: Optional[InterfaceName] = None
126
248
  """ Enforce an interface. By default, we choose automatically. """
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "mininterface"
7
- version = "1.1.2"
7
+ version = "1.1.4"
8
8
  description = "A minimal access to GUI, TUI, CLI and config"
9
9
  authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
10
10
  license = "LGPL-3.0-or-later"
@@ -1,6 +0,0 @@
1
-
2
- from typing import Literal
3
-
4
-
5
- def literal(c):
6
- return Literal[*c]
File without changes