mininterface 1.2.1__tar.gz → 1.3.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.
Files changed (90) hide show
  1. {mininterface-1.2.1 → mininterface-1.3.0}/PKG-INFO +6 -6
  2. {mininterface-1.2.1 → mininterface-1.3.0}/README.md +4 -4
  3. mininterface-1.3.0/mininterface/__init__.py +29 -0
  4. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/auxiliary.py +16 -78
  5. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/cli_parser.py +2 -27
  6. mininterface-1.3.0/mininterface/_lib/config_file.py +97 -0
  7. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/dataclass_creation.py +13 -5
  8. mininterface-1.3.0/mininterface/_lib/dict_utils.py +17 -0
  9. mininterface-1.3.0/mininterface/_lib/docstrings.py +101 -0
  10. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/form_dict.py +34 -50
  11. mininterface-1.3.0/mininterface/_lib/form_types.py +7 -0
  12. mininterface-1.3.0/mininterface/_lib/ipc_command.py +20 -0
  13. mininterface-1.3.0/mininterface/_lib/redirectable.py +105 -0
  14. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/run.py +67 -42
  15. mininterface-1.3.0/mininterface/_lib/subprocess_base.py +569 -0
  16. mininterface-1.3.0/mininterface/_lib/subprocess_child_base.py +248 -0
  17. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/tyro_patches.py +22 -9
  18. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_mininterface/__init__.py +12 -9
  19. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_mininterface/adaptor.py +13 -7
  20. mininterface-1.3.0/mininterface/_textual_interface/__init__.py +0 -0
  21. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/adaptor.py +38 -31
  22. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/button_contents.py +6 -5
  23. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/facet.py +7 -12
  24. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/file_picker_input.py +2 -2
  25. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/form_contents.py +9 -9
  26. mininterface-1.2.1/mininterface/_textual_interface/__init__.py → mininterface-1.3.0/mininterface/_textual_interface/interface.py +14 -13
  27. mininterface-1.3.0/mininterface/_textual_interface/style.tcss +71 -0
  28. mininterface-1.3.0/mininterface/_textual_interface/subprocess_adaptor.py +39 -0
  29. mininterface-1.3.0/mininterface/_textual_interface/subprocess_child.py +381 -0
  30. mininterface-1.3.0/mininterface/_tk_interface/__init__.py +0 -0
  31. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/adaptor.py +39 -9
  32. mininterface-1.2.1/mininterface/_tk_interface/__init__.py → mininterface-1.3.0/mininterface/_tk_interface/interface.py +17 -20
  33. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/secret_entry.py +1 -0
  34. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/select_input.py +12 -1
  35. mininterface-1.3.0/mininterface/_tk_interface/subprocess_adaptor.py +89 -0
  36. mininterface-1.3.0/mininterface/_tk_interface/subprocess_child.py +349 -0
  37. mininterface-1.3.0/mininterface/_web_interface/__init__.py +90 -0
  38. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/exceptions.py +14 -0
  39. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/facet/__init__.py +4 -4
  40. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/interfaces.py +2 -2
  41. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/select_tag.py +11 -0
  42. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/tag.py +12 -19
  43. {mininterface-1.2.1 → mininterface-1.3.0}/pyproject.toml +2 -2
  44. mininterface-1.2.1/mininterface/__init__.py +0 -7
  45. mininterface-1.2.1/mininterface/_lib/config_file.py +0 -99
  46. mininterface-1.2.1/mininterface/_lib/redirectable.py +0 -62
  47. mininterface-1.2.1/mininterface/_textual_interface/style.tcss +0 -50
  48. mininterface-1.2.1/mininterface/_web_interface/__init__.py +0 -92
  49. mininterface-1.2.1/mininterface/_web_interface/app.py +0 -43
  50. mininterface-1.2.1/mininterface/_web_interface/child_adaptor.py +0 -90
  51. mininterface-1.2.1/mininterface/_web_interface/parent_adaptor.py +0 -83
  52. {mininterface-1.2.1 → mininterface-1.3.0}/LICENSE +0 -0
  53. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/__main__.py +0 -0
  54. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/__init__.py +0 -0
  55. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/argparse_support.py +0 -0
  56. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/cli_flags.py +0 -0
  57. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/cli_utils.py +0 -0
  58. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/future_compatibility.py +0 -0
  59. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/shortcuts.py +0 -0
  60. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/showcase.py +0 -0
  61. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_lib/start.py +0 -0
  62. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_mininterface/mixin.py +0 -0
  63. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_text_interface/__init__.py +0 -0
  64. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_text_interface/adaptor.py +0 -0
  65. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_text_interface/facet.py +0 -0
  66. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_text_interface/timeout.py +0 -0
  67. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/secret_input.py +0 -0
  68. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/textual_app.py +0 -0
  69. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/timeout.py +0 -0
  70. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_textual_interface/widgets.py +0 -0
  71. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/date_entry.py +0 -0
  72. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/external_fix.py +0 -0
  73. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/facet.py +0 -0
  74. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
  75. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/timeout.py +0 -0
  76. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/_tk_interface/utils.py +0 -0
  77. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/cli.py +0 -0
  78. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/experimental.py +0 -0
  79. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/settings.py +0 -0
  80. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/__init__.py +0 -0
  81. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/alias.py +0 -0
  82. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/callback_tag.py +0 -0
  83. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/datetime_tag.py +0 -0
  84. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/flag.py +0 -0
  85. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/internal.py +0 -0
  86. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/path_tag.py +0 -0
  87. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/secret_tag.py +0 -0
  88. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/tag_factory.py +0 -0
  89. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/tag/type_stubs.py +0 -0
  90. {mininterface-1.2.1 → mininterface-1.3.0}/mininterface/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mininterface
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: CLI & dialog toolkit – a minimal interface to Python application (GUI, TUI, CLI + config files, web)
5
5
  License: LGPL-3.0-or-later
6
6
  License-File: LICENSE
@@ -27,7 +27,7 @@ Requires-Dist: humanize ; extra == "basic" or extra == "img" or extra == "tui" o
27
27
  Requires-Dist: pillow ; extra == "img" or extra == "gui" or extra == "ui" or extra == "all"
28
28
  Requires-Dist: pyyaml ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
29
29
  Requires-Dist: simple_term_menu
30
- Requires-Dist: textual (<2.0.0) ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
30
+ Requires-Dist: textual (>=2.0.0,<9.0.0) ; extra == "basic" or extra == "img" or extra == "tui" or extra == "gui" or extra == "web" or extra == "ui" or extra == "all"
31
31
  Requires-Dist: textual-serve ; extra == "web" or extra == "ui" or extra == "all"
32
32
  Requires-Dist: textual_imageview ; extra == "img" or extra == "tui" or extra == "ui" or extra == "all"
33
33
  Requires-Dist: tkcalendar ; extra == "gui" or extra == "ui" or extra == "all"
@@ -191,8 +191,8 @@ These projects have the code base reduced thanks to the mininterface:
191
191
  Take a look at the following example.
192
192
 
193
193
  1. We define any Env class.
194
- 2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be prompted for
195
- 3. Then, we use various dialog methods, like [`confirm`][mininterface.Mininterface.confirm], [`select`][mininterface.Mininterface.select] or [`form`][mininterface.Mininterface.form].
194
+ 2. Then, we initialize mininterface with [`run(Env)`](https://cz-nic.github.io/mininterface/run/) – the missing fields will be prompted for
195
+ 3. Then, we use various dialog methods, like [`confirm`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.confirm), [`select`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.select) or [`form`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.form).
196
196
 
197
197
  Below, you find the screenshots how the program looks in various environments ([graphic](Interfaces.md#guiinterface-or-tkinterface-or-gui) interface, [web](Interfaces.md#webinterface-or-web) interface...).
198
198
 
@@ -273,7 +273,7 @@ usage: program.py [-h] [OPTIONS]
273
273
 
274
274
  You want to try out the Mininterface with your current [`ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)?
275
275
 
276
- You're using positional arguments, subparsers, types in the ArgumentParser... Mininterface will give you immediate benefit. Just wrap it inside the [`run`][mininterface.run] method.
276
+ You're using positional arguments, subparsers, types in the ArgumentParser... Mininterface will give you immediate benefit. Just wrap it inside the [`run`](https://cz-nic.github.io/mininterface/run/) method.
277
277
 
278
278
  ```python
279
279
  #!/usr/bin/env python3
@@ -330,7 +330,7 @@ Then, a `.form()` call will create a dialog with all the fields.
330
330
 
331
331
  ![Whole form](https://github.com/CZ-NIC/mininterface/blob/main/asset/argparse_form.avif?raw=True)
332
332
 
333
- You will access the arguments through [`m.env`][mininterface.Mininterface.env]
333
+ You will access the arguments through [`m.env`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.env)
334
334
 
335
335
  ```python
336
336
  print(m.env.time) # -> 14:21
@@ -151,8 +151,8 @@ These projects have the code base reduced thanks to the mininterface:
151
151
  Take a look at the following example.
152
152
 
153
153
  1. We define any Env class.
154
- 2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be prompted for
155
- 3. Then, we use various dialog methods, like [`confirm`][mininterface.Mininterface.confirm], [`select`][mininterface.Mininterface.select] or [`form`][mininterface.Mininterface.form].
154
+ 2. Then, we initialize mininterface with [`run(Env)`](https://cz-nic.github.io/mininterface/run/) – the missing fields will be prompted for
155
+ 3. Then, we use various dialog methods, like [`confirm`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.confirm), [`select`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.select) or [`form`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.form).
156
156
 
157
157
  Below, you find the screenshots how the program looks in various environments ([graphic](Interfaces.md#guiinterface-or-tkinterface-or-gui) interface, [web](Interfaces.md#webinterface-or-web) interface...).
158
158
 
@@ -233,7 +233,7 @@ usage: program.py [-h] [OPTIONS]
233
233
 
234
234
  You want to try out the Mininterface with your current [`ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)?
235
235
 
236
- You're using positional arguments, subparsers, types in the ArgumentParser... Mininterface will give you immediate benefit. Just wrap it inside the [`run`][mininterface.run] method.
236
+ You're using positional arguments, subparsers, types in the ArgumentParser... Mininterface will give you immediate benefit. Just wrap it inside the [`run`](https://cz-nic.github.io/mininterface/run/) method.
237
237
 
238
238
  ```python
239
239
  #!/usr/bin/env python3
@@ -290,7 +290,7 @@ Then, a `.form()` call will create a dialog with all the fields.
290
290
 
291
291
  ![Whole form](https://github.com/CZ-NIC/mininterface/blob/main/asset/argparse_form.avif?raw=True)
292
292
 
293
- You will access the arguments through [`m.env`][mininterface.Mininterface.env]
293
+ You will access the arguments through [`m.env`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.env)
294
294
 
295
295
  ```python
296
296
  print(m.env.time) # -> 14:21
@@ -0,0 +1,29 @@
1
+ __all__ = ["run", "Mininterface", "Tag", "Cancelled", "Validation", "Options"]
2
+
3
+
4
+ def __getattr__(name: str):
5
+ if name == "run":
6
+ from ._lib.run import run
7
+ globals()["run"] = run
8
+ return run
9
+ if name == "Mininterface":
10
+ from ._mininterface import Mininterface
11
+ globals()["Mininterface"] = Mininterface
12
+ return Mininterface
13
+ if name == "Cancelled":
14
+ from .exceptions import Cancelled
15
+ globals()["Cancelled"] = Cancelled
16
+ return Cancelled
17
+ if name == "Tag":
18
+ from .tag import Tag
19
+ globals()["Tag"] = Tag
20
+ return Tag
21
+ if name == "Options":
22
+ from .tag.alias import Options
23
+ globals()["Options"] = Options
24
+ return Options
25
+ if name == "Validation":
26
+ from .tag.alias import Validation
27
+ globals()["Validation"] = Validation
28
+ return Validation
29
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -1,51 +1,23 @@
1
1
  import logging
2
2
  import os
3
- import re
4
- from argparse import ArgumentParser
5
3
  from dataclasses import fields, is_dataclass
6
4
  from functools import lru_cache
7
5
  from types import UnionType
8
- from typing import Any, Callable, Iterable, Optional, TypeVar, Union, Literal, get_args, get_origin, get_type_hints
6
+ from typing import (
7
+ Any,
8
+ Iterable,
9
+ Union,
10
+ Literal,
11
+ get_args,
12
+ get_origin,
13
+ get_type_hints,
14
+ )
9
15
 
10
16
  from annotated_types import Ge, Gt, Le, Len, Lt, MultipleOf
17
+ from .dict_utils import T, KT, common_iterables, flatten
11
18
 
12
19
  logger = logging.getLogger(__name__)
13
20
 
14
- try:
15
- from tyro.extras import get_parser
16
- except ImportError:
17
- get_parser = None
18
-
19
- try:
20
- from humanize import naturalsize as naturalsize_
21
- except ImportError:
22
- naturalsize_ = None
23
-
24
- T = TypeVar("T")
25
- KT = str
26
- common_iterables = list, tuple, set
27
- """ collections, and not a str """
28
-
29
-
30
- def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]] = None) -> Iterable[T]:
31
- """Recursively traverse whole dict"""
32
- for k, v in d.items():
33
- if isinstance(v, dict):
34
- if include_keys:
35
- yield from include_keys(k)
36
- yield from flatten(v)
37
- else:
38
- yield v
39
-
40
-
41
- # NOTE: Not used.
42
- def flatten_keys(d: dict[KT, T | dict]) -> Iterable[tuple[KT, T]]:
43
- """Recursively traverse whole dict"""
44
- for k, v in d.items():
45
- if isinstance(v, dict):
46
- yield from flatten_keys(v)
47
- else:
48
- yield k, v
49
21
 
50
22
 
51
23
  def guess_type(val: T) -> type[T]:
@@ -70,42 +42,6 @@ def get_terminal_size():
70
42
  return 0, 0
71
43
 
72
44
 
73
- def get_descriptions(parser: ArgumentParser) -> dict:
74
- """Load descriptions from the parser. Strip argparse info about the default value as it will be editable in the form."""
75
- # clean-up tyro stuff that may have a meaning in the CLI, but not in the UI
76
- return {
77
- re.sub(r"\s\(positional\)$", "", action.dest).replace("-", "_"): re.sub(
78
- r"\((default|fixed to|required).*\)", "", action.help or ""
79
- )
80
- for action in parser._actions
81
- }
82
-
83
-
84
- @lru_cache
85
- def _get_parser(obj):
86
- if get_parser:
87
- return get_parser(obj)
88
-
89
-
90
- def get_description(obj, param: str) -> str:
91
- if p := _get_parser(obj):
92
- try:
93
- d = get_descriptions(p)[param].strip()
94
- except KeyError: # either fetching failed or user added no description
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
101
- else:
102
- # We are missing mininterface[basic] requirement. Tyro is missing.
103
- # Without tyro, we are not able to evaluate the class: m.form(Env),
104
- # we can still evaluate its instance: m.form(Env()).
105
- # However, without descriptions.
106
- return ""
107
-
108
-
109
45
  def yield_annotations(dataclass):
110
46
  yield from (cl.__annotations__ for cl in dataclass.__mro__ if is_dataclass(cl))
111
47
 
@@ -303,9 +239,11 @@ def dict_diff(a: dict, b: dict) -> dict:
303
239
 
304
240
  def naturalsize(value: float | str, *args) -> str:
305
241
  """For a bare interface, humanize might not be installed."""
306
- if naturalsize_:
307
- return naturalsize_(value, *args)
308
- return str(value)
242
+ try:
243
+ from humanize import naturalsize as _naturalsize
244
+ return _naturalsize(value, *args)
245
+ except ImportError:
246
+ return str(value)
309
247
 
310
248
 
311
249
  def validate_annotated_type(meta, value) -> bool:
@@ -358,7 +296,7 @@ def strip_none(annotation):
358
296
  args = tuple(arg for arg in get_args(annotation) if arg is not type(None))
359
297
  if len(args) == 1:
360
298
  return args[0]
361
- return Union[args] # nebo origin[args], aby se zachoval typ
299
+ return Union[args]
362
300
 
363
301
  return annotation
364
302
 
@@ -16,10 +16,10 @@ from ..cli import Command
16
16
  from ..settings import CliSettings
17
17
 
18
18
  from ..exceptions import Cancelled
19
+ from .auxiliary import flatten
19
20
  from .auxiliary import (
20
21
  get_or_create_parent_dict,
21
22
  remove_empty_dicts,
22
- flatten,
23
23
  )
24
24
  from .dataclass_creation import (
25
25
  _unwrap_annotated,
@@ -65,23 +65,6 @@ except ImportError:
65
65
  raise DependencyRequired("basic")
66
66
 
67
67
 
68
- def assure_args(args: Optional[Sequence[str]] = None):
69
- if args is None:
70
- # Set env to determine whether to use sys.argv.
71
- # Why settings env? Prevent tyro using sys.argv if we are in an interactive shell like Jupyter,
72
- # as sys.argv is non-related there.
73
- try:
74
- # Note wherease `"get_ipython" in globals()` returns True in Jupyter, it is still False
75
- # in a script a Jupyter cell runs. Hence we must put here this lengthty statement.
76
- global get_ipython
77
- get_ipython()
78
- except:
79
- args = sys.argv[1:] # Fetch from the CLI
80
- else:
81
- args = []
82
- return args
83
-
84
-
85
68
  def _subcommands_default_appliable(kwargs, _crawling):
86
69
  if len(_crawling.get()):
87
70
  return kwargs.get("subcommands_default")
@@ -245,15 +228,7 @@ def parse_cli(
245
228
  kwargs, None if helponly else m, args, type_form, env_classes, _custom_registry, annot, _req_fields
246
229
  )
247
230
 
248
- # Why setting m.env instead of putting into into a constructor of a new get_interface() call?
249
- # 1. Getting the interface is a costly operation
250
- # 2. There is this bug so that we need to use single interface:
251
- # TODO
252
- # As this works badly, lets make sure we use single interface now
253
- # and will not need the second one.
254
- # get_interface("gui")
255
- # m = get_interface("gui")
256
- # m.select([1,2,3])
231
+ # Make the interface ready for the user
257
232
  m.env = env
258
233
  except SystemExit as exception:
259
234
  # --- (C) The dialog missing section ---
@@ -0,0 +1,97 @@
1
+ from dataclasses import asdict
2
+ import dataclasses
3
+ import warnings
4
+ from pathlib import Path
5
+ from typing import Optional, Type
6
+
7
+
8
+ from ..settings import MininterfaceSettings
9
+ from .auxiliary import dataclass_asdict_no_defaults, merge_dicts
10
+ from .form_dict import EnvClass
11
+
12
+
13
+ def load_settings_from_config(config_file: Path) -> tuple[dict, dict | None]:
14
+ """Load yaml config without tyro. Returns (raw_cli_dict, mininterface_settings_dict).
15
+ raw_cli_dict has the 'mininterface' key already removed."""
16
+ import yaml
17
+ raw = yaml.safe_load(config_file.read_text()) or {}
18
+ return raw, raw.pop("mininterface", None)
19
+
20
+
21
+ def ensure_settings_inheritance(
22
+ base: Optional[MininterfaceSettings], conf: dict, _def_fact=MininterfaceSettings
23
+ ) -> MininterfaceSettings:
24
+ """Merge a config-file settings dict into MininterfaceSettings.
25
+ Handles direct fields and one level of nested dataclass fields.
26
+ Applies UI inheritance (ui→gui, ui→tui, etc.)."""
27
+ if base:
28
+ conf = merge_dicts(dataclass_asdict_no_defaults(base), conf)
29
+ for sources in [
30
+ ("ui", "gui"),
31
+ ("ui", "tui"),
32
+ ("ui", "tui", "textual"),
33
+ ("ui", "tui", "text"),
34
+ ("ui", "tui", "textual", "web"),
35
+ ]:
36
+ target = sources[-1]
37
+ merged: dict = {}
38
+ for s in sources:
39
+ merged.update(conf.get(s, {}))
40
+ merged.update(conf.get(target, {}))
41
+ if merged:
42
+ conf[target] = merged
43
+ result = base or _def_fact()
44
+ for key, value in conf.items():
45
+ if not hasattr(result, key):
46
+ continue
47
+ attr = getattr(result, key)
48
+ if dataclasses.is_dataclass(attr) and isinstance(value, dict):
49
+ for k2, v2 in value.items():
50
+ if hasattr(attr, k2):
51
+ setattr(attr, k2, v2)
52
+ else:
53
+ setattr(result, key, value)
54
+ return result
55
+
56
+
57
+ def parse_config_file(
58
+ env_or_list: Type[EnvClass] | list[Type[EnvClass]],
59
+ raw_config: "dict | None" = None,
60
+ config_file: "Path | None" = None,
61
+ **kwargs,
62
+ ) -> dict:
63
+ """Fill kwargs["default"] from a pre-loaded config dict. Needs tyro.
64
+
65
+ Args:
66
+ env_or_list: Class(es) with the configuration.
67
+ raw_config: Pre-loaded yaml dict from load_settings_from_config (mininterface key already removed).
68
+ Pass None when there is no config file.
69
+ config_file: Original path, used only for error messages.
70
+ Kwargs:
71
+ The same as for argparse.ArgumentParser.
72
+ """
73
+ if raw_config is not None and "default" not in kwargs:
74
+ from .dataclass_creation import create_with_missing, to_kebab_case
75
+ try:
76
+ subc = {}
77
+
78
+ if isinstance(env_or_list, list):
79
+ kwargs["subcommands_default_union"] = {}
80
+ for cl in env_or_list:
81
+ cl_name = to_kebab_case(cl.__name__)
82
+ subc[cl_name] = {}
83
+ ooo = create_with_missing(cl, raw_config.get(cl_name, {}), subc=subc[cl_name])
84
+ kwargs["subcommands_default_union"][cl_name] = asdict(ooo)
85
+ # `kwargs["default"]` remains empty for now as there is no bare default that tyro would support as everything is hidden under the subcommands
86
+
87
+ else:
88
+ kwargs["default"] = create_with_missing(env_or_list, raw_config, subc=subc)
89
+
90
+ if subc:
91
+ kwargs["subcommands_default"] = subc
92
+ except TypeError:
93
+ raise SyntaxError(f"Config file parsing failed for {config_file}")
94
+
95
+ return kwargs
96
+
97
+
@@ -1,13 +1,11 @@
1
1
  import re
2
2
  import warnings
3
- from dataclasses import MISSING, asdict, dataclass, fields, is_dataclass
3
+ from dataclasses import MISSING, fields, is_dataclass
4
4
  from types import UnionType
5
5
  from typing import Annotated, Optional, Sequence, Type, Union, get_args, get_origin, TypeVar
6
6
 
7
-
8
7
  try:
9
8
  from tyro._singleton import MISSING_NONPROP
10
- from tyro.extras import subcommand_type_from_defaults
11
9
 
12
10
  from ..cli import SubcommandPlaceholder
13
11
  except ImportError:
@@ -19,7 +17,8 @@ except ImportError:
19
17
  from ..tag import Tag
20
18
  from ..tag.tag_factory import tag_factory
21
19
  from ..validators import not_empty
22
- from .auxiliary import _get_origin, _get_parser, get_description
20
+ from .auxiliary import _get_origin
21
+ from .docstrings import get_class_description, get_description
23
22
  from .form_dict import DataClass, EnvClass, MissingTagValue
24
23
 
25
24
  # Pydantic is not a project dependency, that is just an optional integration
@@ -49,6 +48,15 @@ def coerce_type_to_annotation(value, annotation):
49
48
  annotation = _unwrap_annotated(annotation) # NOTE might be superfluous, called before
50
49
  origin = get_origin(annotation)
51
50
 
51
+ # Handle Union (e.g. int | None)
52
+ if origin in (Union, UnionType):
53
+ for arg in get_args(annotation):
54
+ try:
55
+ return coerce_type_to_annotation(value, arg)
56
+ except Exception:
57
+ pass
58
+ return value
59
+
52
60
  # Handle tuple[...] conversion
53
61
  if origin is tuple and isinstance(value, list):
54
62
  args = get_args(annotation)
@@ -353,7 +361,7 @@ def choose_subcommand(env_classes: list[Type[DataClass]], m: "Mininterface[EnvCl
353
361
  # NOTE make select display buttons if there is a little amount of options.
354
362
  env = m.select(
355
363
  {
356
- (to_kebab_case(cl.__name__).replace("-", " ").capitalize(), _get_parser(cl).description): cl
364
+ (to_kebab_case(cl.__name__).replace("-", " ").capitalize(), get_class_description(cl)): cl
357
365
  for cl in env_classes
358
366
  if cl is not SubcommandPlaceholder
359
367
  }
@@ -0,0 +1,17 @@
1
+ from typing import Callable, Iterable, Optional, TypeVar
2
+
3
+ T = TypeVar("T")
4
+ KT = str
5
+ common_iterables = list, tuple, set
6
+ """ collections, and not a str """
7
+
8
+
9
+ def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]] = None) -> Iterable[T]:
10
+ """Recursively traverse whole dict"""
11
+ for k, v in d.items():
12
+ if isinstance(v, dict):
13
+ if include_keys:
14
+ yield from include_keys(k)
15
+ yield from flatten(v)
16
+ else:
17
+ yield v
@@ -0,0 +1,101 @@
1
+ """Tyro-dependent docstring/description helpers, split out from auxiliary.py.
2
+
3
+ Keeping these separate lets child processes import auxiliary.flatten without
4
+ paying the ~20 ms cost of loading tyro. Tyro itself is imported lazily inside
5
+ the functions that need it, so importing this module is also cheap.
6
+ """
7
+ from functools import lru_cache
8
+ from typing import Annotated, get_args, get_origin, get_type_hints
9
+ from dataclasses import fields
10
+
11
+ _tyro_loaded = False
12
+ _tyro_docstrings_available = False
13
+ _tyro_get_field_docstring = None
14
+ _tyro_get_callable_description = None
15
+ tyro = None
16
+
17
+
18
+ def _ensure_tyro():
19
+ global _tyro_loaded, _tyro_docstrings_available, _tyro_get_field_docstring, _tyro_get_callable_description, tyro
20
+ if _tyro_loaded:
21
+ return
22
+ _tyro_loaded = True
23
+ try:
24
+ import tyro as _tyro
25
+ from tyro._docstrings import get_field_docstring as _gfd
26
+ from tyro._docstrings import get_callable_description as _gcd
27
+ tyro = _tyro
28
+ _tyro_get_field_docstring = _gfd
29
+ _tyro_get_callable_description = _gcd
30
+ _tyro_docstrings_available = True
31
+ except ImportError:
32
+ pass
33
+
34
+
35
+ def get_class_description(obj) -> str:
36
+ _ensure_tyro()
37
+ if _tyro_get_callable_description:
38
+ return _tyro_get_callable_description(obj)
39
+ return ""
40
+
41
+
42
+ @lru_cache
43
+ def _get_descriptions_from_docstring(obj) -> dict[str, str]:
44
+ """Extract field descriptions for all fields of a class.
45
+
46
+ Uses tyro's internal helptext extraction (tyro._docstrings.get_field_docstring),
47
+ which supports the same sources and precedence as tyro's own CLI generation:
48
+ 1. tyro.conf.arg(help=...)
49
+ 2. PEP 727 Doc
50
+ 3. Docstrings (attribute docstrings or class docstring params)
51
+ 4. Comments (inline or preceding)
52
+
53
+ We used to rely on tyro.extras.get_parser(), but that was marked deprecated,
54
+ so we call tyro's internal API directly instead.
55
+ """
56
+ _ensure_tyro()
57
+ if not _tyro_docstrings_available:
58
+ return {}
59
+
60
+ result = {}
61
+
62
+ # Highest priority: tyro.conf.arg(help=...) in Annotated metadata.
63
+ try:
64
+ hints = get_type_hints(obj, include_extras=True)
65
+ ArgConfig = tyro.conf._confstruct._ArgConfig
66
+ for field_name, hint in hints.items():
67
+ if get_origin(hint) is Annotated:
68
+ for meta in hint.__metadata__:
69
+ if isinstance(meta, ArgConfig) and meta.help:
70
+ result[field_name] = meta.help
71
+ except Exception:
72
+ hints = {}
73
+
74
+ # Mid priority: docstrings and comments via tyro's own extraction.
75
+ for field_name in hints:
76
+ doc = _tyro_get_field_docstring(obj, field_name, ())
77
+ if doc:
78
+ result.setdefault(field_name, doc)
79
+
80
+ # Lowest priority: field.metadata["help"] from dynamically generated
81
+ # dataclasses (e.g. built from ArgumentParser via make_dataclass).
82
+ try:
83
+ for f in fields(obj): # type: ignore
84
+ if help_text := f.metadata.get("help"):
85
+ result.setdefault(f.name, help_text)
86
+ except TypeError:
87
+ pass
88
+
89
+ return result
90
+
91
+
92
+ def get_description(obj, param: str) -> str:
93
+ desc = _get_descriptions_from_docstring(obj).get(param, "")
94
+ if desc and desc.replace("-", "_") != param:
95
+ return desc
96
+
97
+ # We are missing mininterface[basic] requirement. Tyro is missing.
98
+ # Without tyro, we are not able to evaluate the class: m.form(Env),
99
+ # we can still evaluate its instance: m.form(Env()).
100
+ # However, without descriptions.
101
+ return ""
@@ -9,8 +9,10 @@ from dataclasses import fields, is_dataclass
9
9
  from types import FunctionType, MethodType, SimpleNamespace
10
10
  from typing import TYPE_CHECKING, Any, Callable, Hashable, Optional, Type, TypeVar, Union, get_args, get_type_hints
11
11
 
12
+ from .form_types import DataClass, EnvClass
12
13
 
13
- from .auxiliary import get_description
14
+
15
+ from .docstrings import get_description
14
16
  from ..tag.tag import MissingTagValue, Tag, TagValue
15
17
  from ..tag.tag_factory import tag_assure_type, tag_factory
16
18
 
@@ -19,53 +21,35 @@ if TYPE_CHECKING: # remove the line as of Python3.11 and make `"Self" -> Self`
19
21
 
20
22
  from .. import Mininterface
21
23
 
22
- try:
23
- import attr
24
- except ImportError:
25
- attr = None
26
- try:
27
- from pydantic import BaseModel
28
- except ImportError:
29
- BaseModel = None
24
+ # attr and pydantic are optional integrations — loaded only on first use so that
25
+ # users relying purely on dataclasses never imports the lib.
26
+ _attr = None
27
+
28
+ def _get_attr():
29
+ global _attr
30
+ if _attr is None:
31
+ try:
32
+ import attr as _a
33
+ _attr = _a
34
+ except ImportError:
35
+ _attr = False
36
+ return _attr or None
37
+
38
+ _BaseModel = None
39
+
40
+ def _get_BaseModel():
41
+ global _BaseModel
42
+ if _BaseModel is None:
43
+ try:
44
+ from pydantic import BaseModel as _BM
45
+ _BaseModel = _BM
46
+ except ImportError:
47
+ _BaseModel = False
48
+ return _BaseModel or None
30
49
 
31
50
 
32
51
  logger = logging.getLogger(__name__)
33
52
 
34
- DataClass = TypeVar("DataClass")
35
- """ Any dataclass. Or a pydantic model or attrs. """
36
- EnvClass = TypeVar("EnvClass", bound=DataClass)
37
- """ Any dataclass. Its instance will be available through [Mininterface.env][mininterface.Mininterface.env] after CLI parsing. Its fields or whole class might be annotated with [tyro conf flags](https://brentyi.github.io/tyro/api/tyro/conf/).
38
-
39
- The following example turns down boolean flag conversion.
40
-
41
- ```python
42
- from dataclasses import dataclass
43
- from mininterface import run
44
- from tyro.conf import FlagConversionOff
45
-
46
- @dataclass
47
- class Env:
48
- my_bool: bool = False
49
-
50
- m = run(FlagConversionOff[Env])
51
- ```
52
-
53
- ```bash
54
- $ program.py --help
55
- # --my-bool {True,False} (default: False)
56
- ```
57
-
58
- Whereas by default, both flags are generated:
59
-
60
- ```python
61
- m = run(Env)
62
- ```
63
-
64
- ```bash
65
- $ program.py --help
66
- # --my-bool, --no-my-bool (default: False)
67
- ```
68
- """
69
53
  FormDict = dict[Hashable, TypeVar("FormDictRecursiveValue", TagValue, Tag, "Self")]
70
54
  """ Nested dict that can have descriptions (through Tag) instead of plain values."""
71
55
  # Attention to programmers. Should we to change FormDict type, check these IDE suggestions are still the same.
@@ -192,14 +176,14 @@ def iterate_attributes(env: DataClass):
192
176
  # Why using fields instead of vars(env)? There might be some helper parameters in the dataclasses that should not be form editable.
193
177
  for f in fields(env):
194
178
  yield f.name, getattr(env, f.name)
195
- elif BaseModel and isinstance(env, BaseModel):
179
+ elif (_bm := _get_BaseModel()) and isinstance(env, _bm):
196
180
  for param, val in vars(env).items():
197
181
  yield param, val
198
182
  # NOTE private pydantic attributes might be printed to forms, because this makes test fail for nested models
199
183
  # for param, val in env.model_dump().items():
200
184
  # yield param, val
201
- elif attr and attr.has(env):
202
- for f in attr.fields(env.__class__):
185
+ elif (_at := _get_attr()) and _at.has(env):
186
+ for f in _at.fields(env.__class__):
203
187
  yield f.name, getattr(env, f.name)
204
188
  else: # might be a normal class; which is unsupported but mostly might work
205
189
  for param, val in vars(env).items():
@@ -212,14 +196,14 @@ def iterate_attributes_keys(env: DataClass):
212
196
  # Why using fields instead of vars(env)? There might be some helper parameters in the dataclasses that should not be form editable.
213
197
  for f in fields(env):
214
198
  yield f.name
215
- elif BaseModel and isinstance(env, BaseModel):
199
+ elif (_bm := _get_BaseModel()) and isinstance(env, _bm):
216
200
  for param, val in vars(env).items():
217
201
  yield param
218
202
  # NOTE private pydantic attributes might be printed to forms, because this makes test fail for nested models
219
203
  # for param, val in env.model_dump().items():
220
204
  # yield param, val
221
- elif attr and attr.has(env):
222
- for f in attr.fields(env.__class__):
205
+ elif (_at := _get_attr()) and _at.has(env):
206
+ for f in _at.fields(env.__class__):
223
207
  yield f.name
224
208
  else: # might be a normal class; which is unsupported but mostly might work
225
209
  for param, val in vars(env).items():