mininterface 1.2.2__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.2 → mininterface-1.3.0}/PKG-INFO +2 -2
  2. mininterface-1.3.0/mininterface/__init__.py +29 -0
  3. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/auxiliary.py +6 -114
  4. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/cli_parser.py +1 -18
  5. mininterface-1.3.0/mininterface/_lib/config_file.py +97 -0
  6. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/dataclass_creation.py +2 -1
  7. mininterface-1.3.0/mininterface/_lib/dict_utils.py +17 -0
  8. mininterface-1.3.0/mininterface/_lib/docstrings.py +101 -0
  9. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/form_dict.py +34 -50
  10. mininterface-1.3.0/mininterface/_lib/form_types.py +7 -0
  11. mininterface-1.3.0/mininterface/_lib/ipc_command.py +20 -0
  12. mininterface-1.3.0/mininterface/_lib/redirectable.py +105 -0
  13. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/run.py +67 -42
  14. mininterface-1.3.0/mininterface/_lib/subprocess_base.py +569 -0
  15. mininterface-1.3.0/mininterface/_lib/subprocess_child_base.py +248 -0
  16. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_mininterface/__init__.py +12 -9
  17. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_mininterface/adaptor.py +13 -7
  18. mininterface-1.3.0/mininterface/_textual_interface/__init__.py +0 -0
  19. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/adaptor.py +38 -31
  20. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/button_contents.py +6 -5
  21. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/facet.py +7 -12
  22. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/file_picker_input.py +2 -2
  23. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/form_contents.py +9 -9
  24. mininterface-1.2.2/mininterface/_textual_interface/__init__.py → mininterface-1.3.0/mininterface/_textual_interface/interface.py +14 -13
  25. mininterface-1.3.0/mininterface/_textual_interface/style.tcss +71 -0
  26. mininterface-1.3.0/mininterface/_textual_interface/subprocess_adaptor.py +39 -0
  27. mininterface-1.3.0/mininterface/_textual_interface/subprocess_child.py +381 -0
  28. mininterface-1.3.0/mininterface/_tk_interface/__init__.py +0 -0
  29. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/adaptor.py +18 -9
  30. mininterface-1.2.2/mininterface/_tk_interface/__init__.py → mininterface-1.3.0/mininterface/_tk_interface/interface.py +17 -20
  31. mininterface-1.3.0/mininterface/_tk_interface/subprocess_adaptor.py +89 -0
  32. mininterface-1.3.0/mininterface/_tk_interface/subprocess_child.py +349 -0
  33. mininterface-1.3.0/mininterface/_web_interface/__init__.py +90 -0
  34. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/exceptions.py +14 -0
  35. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/facet/__init__.py +4 -4
  36. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/interfaces.py +2 -2
  37. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/select_tag.py +11 -0
  38. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/tag.py +12 -19
  39. {mininterface-1.2.2 → mininterface-1.3.0}/pyproject.toml +2 -2
  40. mininterface-1.2.2/mininterface/__init__.py +0 -7
  41. mininterface-1.2.2/mininterface/_lib/config_file.py +0 -99
  42. mininterface-1.2.2/mininterface/_lib/redirectable.py +0 -62
  43. mininterface-1.2.2/mininterface/_textual_interface/style.tcss +0 -50
  44. mininterface-1.2.2/mininterface/_web_interface/__init__.py +0 -92
  45. mininterface-1.2.2/mininterface/_web_interface/app.py +0 -43
  46. mininterface-1.2.2/mininterface/_web_interface/child_adaptor.py +0 -90
  47. mininterface-1.2.2/mininterface/_web_interface/parent_adaptor.py +0 -83
  48. {mininterface-1.2.2 → mininterface-1.3.0}/LICENSE +0 -0
  49. {mininterface-1.2.2 → mininterface-1.3.0}/README.md +0 -0
  50. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/__main__.py +0 -0
  51. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/__init__.py +0 -0
  52. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/argparse_support.py +0 -0
  53. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/cli_flags.py +0 -0
  54. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/cli_utils.py +0 -0
  55. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/future_compatibility.py +0 -0
  56. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/shortcuts.py +0 -0
  57. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/showcase.py +0 -0
  58. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/start.py +0 -0
  59. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_lib/tyro_patches.py +0 -0
  60. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_mininterface/mixin.py +0 -0
  61. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_text_interface/__init__.py +0 -0
  62. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_text_interface/adaptor.py +0 -0
  63. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_text_interface/facet.py +0 -0
  64. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_text_interface/timeout.py +0 -0
  65. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/secret_input.py +0 -0
  66. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/textual_app.py +0 -0
  67. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/timeout.py +0 -0
  68. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_textual_interface/widgets.py +0 -0
  69. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/date_entry.py +0 -0
  70. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/external_fix.py +0 -0
  71. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/facet.py +0 -0
  72. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
  73. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/secret_entry.py +0 -0
  74. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/select_input.py +0 -0
  75. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/timeout.py +0 -0
  76. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/_tk_interface/utils.py +0 -0
  77. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/cli.py +0 -0
  78. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/experimental.py +0 -0
  79. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/settings.py +0 -0
  80. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/__init__.py +0 -0
  81. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/alias.py +0 -0
  82. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/callback_tag.py +0 -0
  83. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/datetime_tag.py +0 -0
  84. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/flag.py +0 -0
  85. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/internal.py +0 -0
  86. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/path_tag.py +0 -0
  87. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/secret_tag.py +0 -0
  88. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/tag_factory.py +0 -0
  89. {mininterface-1.2.2 → mininterface-1.3.0}/mininterface/tag/type_stubs.py +0 -0
  90. {mininterface-1.2.2 → 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.2
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"
@@ -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}")
@@ -5,11 +5,7 @@ from functools import lru_cache
5
5
  from types import UnionType
6
6
  from typing import (
7
7
  Any,
8
- Annotated,
9
- Callable,
10
8
  Iterable,
11
- Optional,
12
- TypeVar,
13
9
  Union,
14
10
  Literal,
15
11
  get_args,
@@ -18,50 +14,10 @@ from typing import (
18
14
  )
19
15
 
20
16
  from annotated_types import Ge, Gt, Le, Len, Lt, MultipleOf
17
+ from .dict_utils import T, KT, common_iterables, flatten
21
18
 
22
19
  logger = logging.getLogger(__name__)
23
20
 
24
- try:
25
- import tyro
26
- from tyro._docstrings import get_field_docstring as _tyro_get_field_docstring
27
- from tyro._docstrings import get_callable_description as _tyro_get_callable_description
28
-
29
- _tyro_docstrings_available = True
30
- except ImportError:
31
- tyro = None
32
- _tyro_docstrings_available = False
33
- _tyro_get_callable_description = None
34
-
35
- try:
36
- from humanize import naturalsize as naturalsize_
37
- except ImportError:
38
- naturalsize_ = None
39
-
40
- T = TypeVar("T")
41
- KT = str
42
- common_iterables = list, tuple, set
43
- """ collections, and not a str """
44
-
45
-
46
- def flatten(d: dict[str, T | dict], include_keys: Optional[Callable[[str], list]] = None) -> Iterable[T]:
47
- """Recursively traverse whole dict"""
48
- for k, v in d.items():
49
- if isinstance(v, dict):
50
- if include_keys:
51
- yield from include_keys(k)
52
- yield from flatten(v)
53
- else:
54
- yield v
55
-
56
-
57
- # NOTE: Not used.
58
- def flatten_keys(d: dict[KT, T | dict]) -> Iterable[tuple[KT, T]]:
59
- """Recursively traverse whole dict"""
60
- for k, v in d.items():
61
- if isinstance(v, dict):
62
- yield from flatten_keys(v)
63
- else:
64
- yield k, v
65
21
 
66
22
 
67
23
  def guess_type(val: T) -> type[T]:
@@ -86,72 +42,6 @@ def get_terminal_size():
86
42
  return 0, 0
87
43
 
88
44
 
89
- def get_class_description(obj) -> str:
90
- if _tyro_get_callable_description:
91
- return _tyro_get_callable_description(obj)
92
- return ""
93
-
94
- @lru_cache
95
- def _get_descriptions_from_docstring(obj) -> dict[str, str]:
96
- """Extract field descriptions for all fields of a class.
97
-
98
- Uses tyro's internal helptext extraction (tyro._docstrings.get_field_docstring),
99
- which supports the same sources and precedence as tyro's own CLI generation:
100
- 1. tyro.conf.arg(help=...)
101
- 2. PEP 727 Doc
102
- 3. Docstrings (attribute docstrings or class docstring params)
103
- 4. Comments (inline or preceding)
104
-
105
- We used to rely on tyro.extras.get_parser(), but that was marked deprecated,
106
- so we call tyro's internal API directly instead.
107
- """
108
- if not _tyro_docstrings_available:
109
- return {}
110
-
111
- result = {}
112
-
113
- # Highest priority: tyro.conf.arg(help=...) in Annotated metadata.
114
- try:
115
- hints = get_type_hints(obj, include_extras=True)
116
- ArgConfig = tyro.conf._confstruct._ArgConfig
117
- for field_name, hint in hints.items():
118
- if get_origin(hint) is Annotated:
119
- for meta in hint.__metadata__:
120
- if isinstance(meta, ArgConfig) and meta.help:
121
- result[field_name] = meta.help
122
- except Exception:
123
- hints = {}
124
-
125
- # Mid priority: docstrings and comments via tyro's own extraction.
126
- for field_name in hints:
127
- doc = _tyro_get_field_docstring(obj, field_name, ())
128
- if doc:
129
- result.setdefault(field_name, doc)
130
-
131
- # Lowest priority: field.metadata["help"] from dynamically generated
132
- # dataclasses (e.g. built from ArgumentParser via make_dataclass).
133
- try:
134
- for f in fields(obj): # type: ignore
135
- if help_text := f.metadata.get("help"):
136
- result.setdefault(f.name, help_text)
137
- except TypeError:
138
- pass
139
-
140
- return result
141
-
142
-
143
- def get_description(obj, param: str) -> str:
144
- desc = _get_descriptions_from_docstring(obj).get(param, "")
145
- if desc and desc.replace("-", "_") != param:
146
- return desc
147
-
148
- # We are missing mininterface[basic] requirement. Tyro is missing.
149
- # Without tyro, we are not able to evaluate the class: m.form(Env),
150
- # we can still evaluate its instance: m.form(Env()).
151
- # However, without descriptions.
152
- return ""
153
-
154
-
155
45
  def yield_annotations(dataclass):
156
46
  yield from (cl.__annotations__ for cl in dataclass.__mro__ if is_dataclass(cl))
157
47
 
@@ -349,9 +239,11 @@ def dict_diff(a: dict, b: dict) -> dict:
349
239
 
350
240
  def naturalsize(value: float | str, *args) -> str:
351
241
  """For a bare interface, humanize might not be installed."""
352
- if naturalsize_:
353
- return naturalsize_(value, *args)
354
- return str(value)
242
+ try:
243
+ from humanize import naturalsize as _naturalsize
244
+ return _naturalsize(value, *args)
245
+ except ImportError:
246
+ return str(value)
355
247
 
356
248
 
357
249
  def validate_annotated_type(meta, value) -> bool:
@@ -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")
@@ -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
+
@@ -17,7 +17,8 @@ except ImportError:
17
17
  from ..tag import Tag
18
18
  from ..tag.tag_factory import tag_factory
19
19
  from ..validators import not_empty
20
- from .auxiliary import _get_origin, get_class_description, get_description
20
+ from .auxiliary import _get_origin
21
+ from .docstrings import get_class_description, get_description
21
22
  from .form_dict import DataClass, EnvClass, MissingTagValue
22
23
 
23
24
  # Pydantic is not a project dependency, that is just an optional integration
@@ -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():
@@ -0,0 +1,7 @@
1
+ from typing import TypeVar
2
+
3
+ DataClass = TypeVar("DataClass")
4
+ """ Any dataclass. Or a pydantic model or attrs. """
5
+
6
+ EnvClass = TypeVar("EnvClass", bound=DataClass)
7
+ """ Any dataclass. Its instance will be available through [Mininterface.env][mininterface.Mininterface.env] after CLI parsing. """
@@ -0,0 +1,20 @@
1
+ from enum import Enum
2
+
3
+
4
+ class IpcCommand(Enum):
5
+ """Message kinds exchanged over the parent⇄child pipe of every subprocess
6
+ UI backend — both the Tk GUI and the Textual TUI use the same protocol."""
7
+
8
+ FORM = "form"
9
+ BUTTONS = "buttons"
10
+ SHUTDOWN = "shutdown"
11
+ RESULT = "result"
12
+ CANCEL = "cancel"
13
+ QUIT = "quit" # child → parent: window closed (X) → exit the program
14
+ ERROR = "error" # child → parent: dialog build crashed (exception + traceback text)
15
+ CALLBACK = "callback" # child → parent: callback fired
16
+ FORM_UPDATE = "form_update" # parent → child: updated tag values after callback
17
+ VALIDATE_RESULT = "validate_result" # parent → child: result of a live validation round-trip
18
+ OUTPUT = "output" # parent → child: live print() text to stream
19
+ CLEAR_OUTPUT = "clear_output" # parent → child: clear the streamed-output widget
20
+ SETTINGS = "settings" # parent → child: the UI settings (sent once after spawn)