mininterface 1.1.4__tar.gz → 1.2.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 (73) hide show
  1. {mininterface-1.1.4 → mininterface-1.2.0}/PKG-INFO +2 -2
  2. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/argparse_support.py +19 -6
  3. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/auxiliary.py +11 -6
  4. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/cli_flags.py +15 -4
  5. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/cli_parser.py +149 -29
  6. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/cli_utils.py +1 -1
  7. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/config_file.py +23 -21
  8. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/dataclass_creation.py +137 -57
  9. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/form_dict.py +13 -7
  10. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/future_compatibility.py +2 -2
  11. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/run.py +32 -15
  12. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/start.py +4 -0
  13. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/tyro_patches.py +16 -4
  14. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_mininterface/__init__.py +2 -5
  15. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_mininterface/adaptor.py +4 -5
  16. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_mininterface/mixin.py +1 -1
  17. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_text_interface/__init__.py +1 -1
  18. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_text_interface/timeout.py +16 -21
  19. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/widgets.py +1 -1
  20. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/select_input.py +1 -1
  21. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/timeout.py +2 -3
  22. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_web_interface/child_adaptor.py +1 -0
  23. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/cli.py +2 -1
  24. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/datetime_tag.py +1 -1
  25. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/flag.py +4 -2
  26. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/tag.py +23 -7
  27. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/tag_factory.py +5 -2
  28. {mininterface-1.1.4 → mininterface-1.2.0}/pyproject.toml +2 -2
  29. {mininterface-1.1.4 → mininterface-1.2.0}/LICENSE +0 -0
  30. {mininterface-1.1.4 → mininterface-1.2.0}/README.md +0 -0
  31. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/__init__.py +0 -0
  32. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/__main__.py +0 -0
  33. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/__init__.py +0 -0
  34. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/redirectable.py +0 -0
  35. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/shortcuts.py +0 -0
  36. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_lib/showcase.py +0 -0
  37. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_text_interface/adaptor.py +0 -0
  38. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_text_interface/facet.py +0 -0
  39. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/__init__.py +0 -0
  40. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/adaptor.py +0 -0
  41. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/button_contents.py +0 -0
  42. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/facet.py +0 -0
  43. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/file_picker_input.py +0 -0
  44. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/form_contents.py +0 -0
  45. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/secret_input.py +0 -0
  46. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/style.tcss +0 -0
  47. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/textual_app.py +0 -0
  48. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_textual_interface/timeout.py +0 -0
  49. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/__init__.py +0 -0
  50. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/adaptor.py +0 -0
  51. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/date_entry.py +0 -0
  52. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/external_fix.py +0 -0
  53. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/facet.py +0 -0
  54. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
  55. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/secret_entry.py +0 -0
  56. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_tk_interface/utils.py +0 -0
  57. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_web_interface/__init__.py +0 -0
  58. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_web_interface/app.py +0 -0
  59. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/_web_interface/parent_adaptor.py +0 -0
  60. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/exceptions.py +0 -0
  61. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/experimental.py +0 -0
  62. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/facet/__init__.py +0 -0
  63. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/interfaces.py +0 -0
  64. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/settings.py +1 -1
  65. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/__init__.py +0 -0
  66. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/alias.py +0 -0
  67. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/callback_tag.py +0 -0
  68. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/internal.py +0 -0
  69. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/path_tag.py +0 -0
  70. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/secret_tag.py +0 -0
  71. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/select_tag.py +0 -0
  72. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/tag/type_stubs.py +0 -0
  73. {mininterface-1.1.4 → mininterface-1.2.0}/mininterface/validators.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mininterface
3
- Version: 1.1.4
4
- Summary: A minimal access to GUI, TUI, CLI and config
3
+ Version: 1.2.0
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
7
7
  Author: Edvard Rejthar
@@ -1,9 +1,19 @@
1
1
  import re
2
2
  import sys
3
- from argparse import (SUPPRESS, Action, ArgumentParser, _AppendAction,
4
- _AppendConstAction, _CountAction, _HelpAction,
5
- _StoreConstAction, _StoreFalseAction, _StoreTrueAction,
6
- _SubParsersAction, _VersionAction)
3
+ from argparse import (
4
+ SUPPRESS,
5
+ Action,
6
+ ArgumentParser,
7
+ _AppendAction,
8
+ _AppendConstAction,
9
+ _CountAction,
10
+ _HelpAction,
11
+ _StoreConstAction,
12
+ _StoreFalseAction,
13
+ _StoreTrueAction,
14
+ _SubParsersAction,
15
+ _VersionAction,
16
+ )
7
17
  from collections import defaultdict
8
18
  from dataclasses import MISSING, Field, dataclass, field, make_dataclass
9
19
  from functools import cached_property
@@ -65,7 +75,9 @@ class ArgparseField:
65
75
  return self.action.dest in self.properties
66
76
 
67
77
 
68
- def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> tuple[DataClass | list[DataClass], Optional[str]]:
78
+ def parser_to_dataclass(
79
+ parser: ArgumentParser, name: str = "Args"
80
+ ) -> tuple[DataClass | list[DataClass], Optional[str]]:
69
81
  """
70
82
  Note: Ex. parser.add_argument("--time", type=time) -> does work at all in argparse, here it works.
71
83
 
@@ -197,8 +209,9 @@ def _make_dataclass_from_actions(
197
209
  if action.choices:
198
210
  # With the drop of Python 3.10, use mere:
199
211
  # arg_type = Literal[*action.choices]
200
- if sys.version_info >= (3,11):
212
+ if sys.version_info >= (3, 11):
201
213
  from .future_compatibility import literal
214
+
202
215
  arg_type = literal(action.choices)
203
216
  else:
204
217
  # we do not prefer this option as tyro does not understand it
@@ -5,8 +5,7 @@ from argparse import ArgumentParser
5
5
  from dataclasses import fields, is_dataclass
6
6
  from functools import lru_cache
7
7
  from types import UnionType
8
- from typing import (Any, Callable, Iterable, Optional, TypeVar, Union, Literal,
9
- get_args, get_origin, get_type_hints)
8
+ from typing import Any, Callable, Iterable, Optional, TypeVar, Union, Literal, get_args, get_origin, get_type_hints
10
9
 
11
10
  from annotated_types import Ge, Gt, Le, Len, Lt, MultipleOf
12
11
 
@@ -75,7 +74,9 @@ def get_descriptions(parser: ArgumentParser) -> dict:
75
74
  """Load descriptions from the parser. Strip argparse info about the default value as it will be editable in the form."""
76
75
  # clean-up tyro stuff that may have a meaning in the CLI, but not in the UI
77
76
  return {
78
- re.sub(r"\s\(positional\)$", "", action.dest).replace("-", "_"): re.sub(r"\((default|fixed to|required).*\)", "", action.help or "")
77
+ re.sub(r"\s\(positional\)$", "", action.dest).replace("-", "_"): re.sub(
78
+ r"\((default|fixed to|required).*\)", "", action.help or ""
79
+ )
79
80
  for action in parser._actions
80
81
  }
81
82
 
@@ -201,7 +202,7 @@ def matches_annotation(value, annotation) -> bool:
201
202
  if origin is list:
202
203
  return all(matches_annotation(item, subtypes[0]) for item in value)
203
204
  elif origin is tuple:
204
- if len(subtypes) == 2 and subtypes[1] is Ellipsis: # ex. tuple[int, ...]
205
+ if len(subtypes) == 2 and subtypes[1] is Ellipsis: # ex. tuple[int, ...]
205
206
  return all(matches_annotation(v, subtypes[0]) for v in value)
206
207
  if len(subtypes) != len(value):
207
208
  return False
@@ -286,8 +287,9 @@ def merge_dicts(d1: dict, d2: dict):
286
287
  d1[key] = value
287
288
  return d1
288
289
 
290
+
289
291
  def dict_diff(a: dict, b: dict) -> dict:
290
- """ Returns the B values where they differ. """
292
+ """Returns the B values where they differ."""
291
293
  result = {}
292
294
  for k in b:
293
295
  if isinstance(a.get(k), dict) and isinstance(b.get(k), dict):
@@ -298,6 +300,7 @@ def dict_diff(a: dict, b: dict) -> dict:
298
300
  result[k] = b[k]
299
301
  return result
300
302
 
303
+
301
304
  def naturalsize(value: float | str, *args) -> str:
302
305
  """For a bare interface, humanize might not be installed."""
303
306
  if naturalsize_:
@@ -346,6 +349,7 @@ def allows_none(annotation) -> bool:
346
349
  return any(arg is type(None) for arg in args)
347
350
  return False
348
351
 
352
+
349
353
  def strip_none(annotation):
350
354
  """Return the same annotation but without NoneType inside a Union/Optional."""
351
355
  origin = get_origin(annotation)
@@ -358,7 +362,8 @@ def strip_none(annotation):
358
362
 
359
363
  return annotation
360
364
 
361
- @lru_cache(maxsize=1024*10)
365
+
366
+ @lru_cache(maxsize=1024 * 10)
362
367
  def _get_origin(tp: Any):
363
368
  """
364
369
  Cached version of typing.get_origin.
@@ -15,14 +15,17 @@ class CliFlags:
15
15
  default_verbosity: int = logging.WARNING
16
16
  _verbosity_sequence: Optional[Sequence[int]] = None
17
17
 
18
+ config: bool = False
19
+
18
20
  def __init__(
19
21
  self,
20
22
  add_verbose: bool | int | Sequence[int] = False,
21
23
  add_version: Optional[str] = None,
22
24
  add_version_package: Optional[str] = None,
23
25
  add_quiet: bool = False,
26
+ add_config: bool = False,
24
27
  ):
25
- self._enabled = {"verbose": True, "version": True, "quiet": True}
28
+ self._enabled = {"verbose": True, "version": True, "quiet": True, "config": True}
26
29
  # verbosity
27
30
  match add_verbose:
28
31
  case bool():
@@ -30,7 +33,7 @@ class CliFlags:
30
33
  case int():
31
34
  self._add_verbose = True
32
35
  self.default_verbosity = add_verbose
33
- self._verbosity_sequence = list(range(add_verbose-10, -1, -10))
36
+ self._verbosity_sequence = list(range(add_verbose - 10, -1, -10))
34
37
  case list() | tuple():
35
38
  self._add_verbose = True
36
39
  self.default_verbosity = add_verbose[0]
@@ -50,13 +53,17 @@ class CliFlags:
50
53
  except PackageNotFoundError:
51
54
  self.version = f"package {add_version_package} not found"
52
55
 
56
+ # config
57
+ self.config = add_config
58
+
53
59
  def should_add(self, env_classes: list[EnvClass]) -> bool:
54
60
  # Flags are added only if neither the env_class nor any of the subcommands have the same-name flag already
55
61
  self._enabled["verbose"] = self._add_verbose and self._attr_not_present("verbose", env_classes)
56
62
  self._enabled["quiet"] = self._add_quiet and self._attr_not_present("quiet", env_classes)
57
63
  self._enabled["version"] = self.version and self._attr_not_present("version", env_classes)
64
+ self._enabled["config"] = self.config and self._attr_not_present("config", env_classes)
58
65
 
59
- return self.add_verbose or self.add_version or self.add_quiet
66
+ return self.add_verbose or self.add_version or self.add_quiet or self.add_config
60
67
 
61
68
  def _attr_not_present(self, flag, env_classes):
62
69
  return all(flag not in cl.__annotations__ for cl in env_classes)
@@ -73,6 +80,10 @@ class CliFlags:
73
80
  def add_quiet(self):
74
81
  return self._add_quiet and self._enabled["quiet"]
75
82
 
83
+ @property
84
+ def add_config(self):
85
+ return self.config and self._enabled["config"]
86
+
76
87
  def get_log_level(self, count):
77
88
  """
78
89
  Ex.
@@ -95,7 +106,7 @@ class CliFlags:
95
106
  Returns:
96
107
  int: log level
97
108
  """
98
- if count == -1: # quiet flag
109
+ if count == -1: # quiet flag
99
110
  return logging.ERROR
100
111
  if not count:
101
112
  return self.default_verbosity
@@ -3,9 +3,11 @@
3
3
  #
4
4
  from dataclasses import asdict
5
5
  from functools import reduce
6
+ from io import StringIO
7
+ from multiprocessing import Value
6
8
  import sys
7
9
  from collections import deque
8
- from contextlib import ExitStack
10
+ from contextlib import ExitStack, redirect_stderr, redirect_stdout
9
11
  from typing import Annotated, Optional, Sequence, Type, Union
10
12
  from unittest.mock import patch
11
13
 
@@ -21,10 +23,11 @@ from .auxiliary import (
21
23
  flatten,
22
24
  )
23
25
  from .dataclass_creation import (
24
- ChosenSubcommand,
25
26
  _unwrap_annotated,
26
27
  choose_subcommand,
27
28
  create_with_missing,
29
+ get_chosen,
30
+ pop_from_passage,
28
31
  to_kebab_case,
29
32
  )
30
33
  from .form_dict import EnvClass, TagDict, dataclass_to_tagdict, MissingTagValue, dict_added_main
@@ -69,13 +72,18 @@ def assure_args(args: Optional[Sequence[str]] = None):
69
72
  return args
70
73
 
71
74
 
75
+ def _subcommands_default_appliable(kwargs, _crawling):
76
+ if len(_crawling.get()):
77
+ return kwargs.get("subcommands_default")
78
+
79
+
72
80
  def parse_cli(
73
81
  env_or_list: Type[EnvClass] | list[Type[EnvClass]],
74
82
  kwargs: dict,
75
83
  m: "Mininterface",
76
84
  cf: Optional[CliFlags] = None,
77
85
  ask_for_missing: bool = True,
78
- args: Optional[Sequence[str]] = None,
86
+ args: Optional[Sequence[str]] = None, # NOTE no more Optional, change the arg order
79
87
  ask_on_empty_cli: Optional[bool] = None,
80
88
  cli_settings: Optional[CliSettings] = None,
81
89
  _crawled=None,
@@ -89,6 +97,7 @@ def parse_cli(
89
97
  """
90
98
  # Xint: The depth we crawled into. The number of subcommands in args.
91
99
  # NOTE ask_on_empty_cli might reveal all fields (in cli_parser), not just wrongs. Eg. when using a subparser `$ prog run`, reveal all subparsers.
100
+ _req_fields = _req_fields or {}
92
101
 
93
102
  if isinstance(env_or_list, list):
94
103
  # We have to convert the list of possible classes (subcommands) to union for tyro.
@@ -149,17 +158,82 @@ def parse_cli(
149
158
  warn(f"Cannot apply {annotations} on Python <= 3.11.")
150
159
  return type_form
151
160
 
161
+ #
162
+ # --- Begin to launch tyro.cli ---
163
+ # This will be divided into four sections.
164
+ # (A) First parse section
165
+ # (B) Re-parse with subcommand-config ensured section
166
+ # (C) The dialog missing section
167
+ # (D) The nothing was missing section
168
+ #
169
+ enforce_dialog = False
170
+ """ When subcommand-chooser was raised (hence the CLI input was not completely working and without mininterface it would raise an error),
171
+ we make sure we display whole CLI overview form at the end."""
172
+
152
173
  try:
153
174
  with ExitStack() as stack:
154
175
  [stack.enter_context(p) for p in patches] # apply just the chosen mocks
176
+
177
+ # --- (A) First parse section ---
178
+
179
+ # Let me explain this awful structure.
180
+ # If we have subcommanded-config file, we first need the tyro to do the parsing as it leaks the crawled path (through the subcommands).
181
+ # Then, we can fill the kwargs['default'] from the subcommanded-config and do the second parsing with some field filled up.
182
+ buffer = StringIO()
183
+ helponly = False
155
184
  try:
156
- env = cli(annot(type_form), args=args, registry=_custom_registry, **kwargs)
157
- except BaseException:
158
- # Why this exception handling? Try putting this out and test_strange_error_mitigation fails.
159
- if len(env_classes) > 1 and kwargs.get("default"):
160
- env = cli(annot(kwargs["default"].__class__), args=args[1:], registry=_custom_registry, **kwargs)
185
+ # Why redirect_stdout? Help-text shows the defaults, which also uses the subcommanded-config.
186
+ with redirect_stdout(buffer):
187
+ try:
188
+ # Standard way.
189
+ env = cli(annot(type_form), args=args, registry=_custom_registry, **kwargs)
190
+ except BaseException:
191
+ # Why this exception handling? Try putting this out and test_strange_error_mitigation fails.
192
+ if len(env_classes) > 1 and kwargs.get("default"):
193
+ env = cli(
194
+ annot(kwargs["default"].__class__), args=args[1:], registry=_custom_registry, **kwargs
195
+ )
196
+ else:
197
+ raise
198
+ except SystemExit as exception:
199
+ # This catch handling is just for the subcommanded-config.
200
+ # Not raising this exception means it worked well and we re-parse with the subcommand-config data just below.
201
+ if _crawled is None and exception.code == 0 and _subcommands_default_appliable(kwargs, _crawling):
202
+ # Help-text exception, continue here and try again with subcommands. As it raises SystemExit first,
203
+ # it will raise SystemExit in the second run too.
204
+ helponly = True
205
+ elif (
206
+ _crawled is None
207
+ and _subcommands_default_appliable(kwargs, _crawling)
208
+ and exception.code == 2
209
+ and failed_fields.get()
210
+ ):
211
+ # Some fields are missing, directly try again. If it raises again
212
+ # (some fields are really missing which cannot be filled from the subcommanded-config),
213
+ # it will immediately raise again and trigger the (C) dialog missing section.
214
+ # If it worked (and no fields are missing), we continue here without triggering the (C) dialog missing section.
215
+ _crawled = True
216
+ env, enforce_dialog = _try_with_subcommands(
217
+ kwargs, m, args, type_form, env_classes, _custom_registry, annot, _req_fields
218
+ )
161
219
  else:
220
+ # This is either a recurrent call from the (C) dialog missing section (and thus subcommand-config re-parsing was done),
221
+ # or there is no subcommand-config data and thus we continue as if this exception handling did not happen.
222
+ if content := buffer.getvalue():
223
+ print(content)
162
224
  raise
225
+
226
+ # --- (B) Re-parse with subcommand-config ensured section ---
227
+
228
+ # Re-parse with subcommand-config.
229
+ # It either raises (if it raised before and subcommand-config did not bring the missing fields) or works well if it worked well before.
230
+ if _crawled is None and _subcommands_default_appliable(kwargs, _crawling):
231
+ # Why not catching enforce_dialog here? As we are here, calling tyro.cli worked for the first time.
232
+ # For sure then, there were no choose_subcommand dialog, subcommands for sure are all written in the CLI.
233
+ env, _ = _try_with_subcommands(
234
+ kwargs, None if helponly else m, args, type_form, env_classes, _custom_registry, annot, _req_fields
235
+ )
236
+
163
237
  # Why setting m.env instead of putting into into a constructor of a new get_interface() call?
164
238
  # 1. Getting the interface is a costly operation
165
239
  # 2. There is this bug so that we need to use single interface:
@@ -170,17 +244,18 @@ def parse_cli(
170
244
  # m = get_interface("gui")
171
245
  # m.select([1,2,3])
172
246
  m.env = env
173
- except BaseException as exception:
174
- if ask_for_missing and getattr(exception, "code", None) == 2 and failed_fields.get():
247
+ except SystemExit as exception:
248
+ # --- (C) The dialog missing section ---
249
+ # Some fields are needed to be filled up.
250
+ if ask_for_missing and exception.code == 2 and failed_fields.get():
175
251
  env = _dialog_missing(
176
252
  env_classes, kwargs, m, cf, ask_for_missing, args, cli_settings, _crawled, _req_fields
177
253
  )
178
254
 
179
255
  if final_call:
180
256
  # Ask for the wrong fields
181
- # Why first_attempt? We display the wrong-fields-form only once.
257
+ # Why final_call? We display the wrong-fields-form only once in the `parse_cli` uppermost call.
182
258
  _ensure_command_init(env, m)
183
-
184
259
  try:
185
260
  m.form(env)
186
261
  except Cancelled as e:
@@ -201,6 +276,7 @@ def parse_cli(
201
276
  # Parsing wrong fields failed. The program ends with a nice tyro message.
202
277
  raise
203
278
  else:
279
+ # --- (D) The nothing was missing section ---
204
280
  dialog_raised = False
205
281
  if final_call:
206
282
  _ensure_command_init(env, m)
@@ -219,7 +295,7 @@ def parse_cli(
219
295
 
220
296
  # Empty CLI → GUI edit
221
297
  subcommand_count = len(_crawling.get())
222
- if not dialog_raised and ask_on_empty_cli and len(sys.argv) <= 1 + subcommand_count:
298
+ if not dialog_raised and (ask_on_empty_cli and len(args) <= subcommand_count) or enforce_dialog:
223
299
  # Raise a dialog if the command line is empty.
224
300
  # This still means empty because 'run' and 'message' are just subcommands: `program.py run message`
225
301
  m.form()
@@ -228,6 +304,37 @@ def parse_cli(
228
304
  return env, dialog_raised
229
305
 
230
306
 
307
+ def _try_with_subcommands(kwargs, m, args, type_form, env_classes, _custom_registry, annot, _req_fields):
308
+ """This awful method is here to re-parse the tyro.cli with the subcommand-config"""
309
+
310
+ failed_fields.set([])
311
+ old_defs = kwargs.get("default", {})
312
+ if old_defs:
313
+ old_defs = asdict(old_defs)
314
+ passage = [cl_name for _, cl_name, _ in _crawling.get()]
315
+
316
+ if len(env_classes) > 1:
317
+ if len(passage):
318
+ env, cl_name = pop_from_passage(passage, env_classes)
319
+ if not old_defs:
320
+ old_defs = kwargs["subcommands_default_union"][cl_name]
321
+ subc = kwargs["subcommands_default"].get(cl_name)
322
+ else: # we should never come here
323
+ raise ValueError("Subcommands parsing failed")
324
+ else:
325
+ env = env_classes[0]
326
+ subc = kwargs["subcommands_default"]
327
+ kwargs["default"] = create_with_missing(env, old_defs, _req_fields, m, subc=subc, subc_passage=passage)
328
+ dialog_used = False
329
+ if hasattr(m, "__subcommand_dialog_used"):
330
+ delattr(m, "__subcommand_dialog_used")
331
+ dialog_used = True
332
+
333
+ env = cli(annot(type_form), args=args, registry=_custom_registry, **kwargs)
334
+
335
+ return env, dialog_used
336
+
337
+
231
338
  def _apply_patches(cf: Optional[CliFlags], ask_for_missing, env_classes, kwargs):
232
339
  patches = []
233
340
 
@@ -272,7 +379,7 @@ def _dialog_missing(
272
379
  args: Optional[Sequence[str]],
273
380
  cli_settings,
274
381
  crawled,
275
- req_fields: Optional[TagDict],
382
+ req_fields: TagDict,
276
383
  ) -> EnvClass:
277
384
  """Some required arguments are missing. Determine which and ask for them.
278
385
 
@@ -288,10 +395,8 @@ def _dialog_missing(
288
395
  * env – Tyro's merge of CLI and kwargs["default"].
289
396
 
290
397
  """
291
- req_fields = req_fields or {}
292
-
293
398
  # There are multiple dataclasses, query which is chosen
294
- m, env_cl = _ensure_chosen_env(env_classes, args, m)
399
+ env_cl = _ensure_chosen_env(env_classes, args, m, kwargs)
295
400
 
296
401
  if crawled is None:
297
402
  # This is the first correction attempt.
@@ -302,16 +407,19 @@ def _dialog_missing(
302
407
  # So in further run, there is no need to rebuild the data. We just process new failed_fields reported by tyro.
303
408
 
304
409
  # Merge with the config file defaults.
305
- disk = d = asdict(dc) if (dc := kwargs.get("default")) else {}
306
- crawled = [None]
307
- for _, val, field_name in _crawling.get():
308
- # NOTE this might be ameliorated so that config file can define subcommands too, now we throw everything out
309
- subd = {}
310
- d[field_name] = ChosenSubcommand(val, subd)
311
- d = subd
312
- crawled.append(val)
313
-
314
- kwargs["default"] = create_with_missing(env_cl, disk, req_fields, m)
410
+ if len(env_classes) > 1:
411
+ disk = kwargs.get("subcommands_default_union", {})
412
+ else:
413
+ disk = asdict(dc) if (dc := kwargs.get("default")) else {}
414
+ crawled = True
415
+ kwargs["default"] = create_with_missing(
416
+ env_cl,
417
+ disk,
418
+ req_fields,
419
+ m,
420
+ subc=kwargs.get("subcommands_default"),
421
+ subc_passage=[cl_name for _, cl_name, _ in _crawling.get()],
422
+ )
315
423
 
316
424
  missing_req = _fetch_currently_failed(req_fields)
317
425
  """ Fields required and missing from CLI """
@@ -341,10 +449,15 @@ def _dialog_missing(
341
449
  return env
342
450
 
343
451
 
344
- def _ensure_chosen_env(env_classes, args, m):
452
+ def _ensure_chosen_env(env_classes, args, m, kwargs):
453
+ # NOTE by preference, handling subclasses union should be done
454
+ # by making an arbitrary dataclass, having single subcommands attribute.
455
+ # That way, all the mendling with the env_classes list would disappear from many places in the code as
456
+ # we already support subclasses in attribute – and this awful function would disappear.
345
457
  env = None
346
458
  if len(env_classes) == 1:
347
459
  env = env_classes[0]
460
+ return env
348
461
  elif len(args):
349
462
  env = next(
350
463
  (env for env in env_classes if to_kebab_case(env.__name__) == args[0]),
@@ -356,7 +469,14 @@ def _ensure_chosen_env(env_classes, args, m):
356
469
  env = choose_subcommand(env_classes, m)
357
470
  if not env:
358
471
  raise NotImplementedError("This case of nested dataclasses is not implemented. Raise an issue please.")
359
- return m, env
472
+
473
+ cl_name = to_kebab_case(env.__name__)
474
+ if kwargs.get("subcommands_default"):
475
+ kwargs["subcommands_default"] = kwargs["subcommands_default"].get(cl_name)
476
+ if kwargs.get("subcommands_default_union"):
477
+ kwargs["subcommands_default_union"] = kwargs["subcommands_default_union"].get(cl_name)
478
+
479
+ return env
360
480
 
361
481
 
362
482
  def _fetch_currently_failed(requireds) -> TagDict:
@@ -168,4 +168,4 @@ SubcommandPlaceholder.__name__ = "subcommand" # show just the shortcut in the C
168
168
  # val: Message | Console
169
169
  # m = run(Env) # here
170
170
  # m = run([Message, Console]) # and here too
171
- # Then, add is as a tip to Supported-types.md.
171
+ # Then, add is as a tip to Supported-types.md.
@@ -1,10 +1,12 @@
1
+ from dataclasses import asdict
1
2
  import warnings
2
3
  from pathlib import Path
3
4
  from typing import Optional, Type
4
5
 
6
+
5
7
  from ..settings import MininterfaceSettings
6
8
  from .auxiliary import dataclass_asdict_no_defaults, merge_dicts
7
- from .dataclass_creation import create_with_missing
9
+ from .dataclass_creation import create_with_missing, to_kebab_case
8
10
  from .form_dict import EnvClass
9
11
 
10
12
  try:
@@ -15,6 +17,7 @@ except ImportError:
15
17
 
16
18
  raise DependencyRequired("basic")
17
19
 
20
+
18
21
  def parse_config_file(
19
22
  env_or_list: Type[EnvClass] | list[Type[EnvClass]],
20
23
  config_file: Path | None = None,
@@ -23,7 +26,7 @@ def parse_config_file(
23
26
  """Fetches the config file into the program defaults kwargs["default"] and UI settings.
24
27
 
25
28
  Args:
26
- env_class: Class with the configuration.
29
+ env_or_list: Class(es) with the configuration.
27
30
  config_file: File to load YAML to be merged with the configuration.
28
31
  You do not have to re-define all the settings in the config file, you can choose a few.
29
32
  Kwargs:
@@ -32,30 +35,29 @@ def parse_config_file(
32
35
  Returns:
33
36
  Tuple of kwargs and dict (section 'mininterface' in the config file).
34
37
  """
35
- if isinstance(env_or_list, list):
36
- subcommands, env = env_or_list, None
37
- else:
38
- subcommands, env = None, env_or_list
39
-
40
- # Load config file
41
- if config_file and subcommands:
42
- # Reading config files when using subcommands is not implemented.
43
- # NOTE But might be now.
44
- kwargs.pop("default", None)
45
- warnings.warn(
46
- f"Config file {config_file} is ignored because subcommands are used."
47
- " It is not easy to set how this should work."
48
- " Describe the developer your usecase so that they might implement this."
49
- )
50
-
51
38
  confopt = None
52
- if "default" not in kwargs and not subcommands and config_file:
39
+ if "default" not in kwargs and config_file:
53
40
  # Undocumented feature. User put a namespace into kwargs["default"]
54
41
  # that already serves for defaults. We do not fetch defaults yet from a config file.
55
42
  disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
56
43
  try:
57
44
  confopt = disk.pop("mininterface", None)
58
- kwargs["default"] = create_with_missing(env, disk)
45
+ subc = {}
46
+
47
+ if isinstance(env_or_list, list):
48
+ kwargs["subcommands_default_union"] = {}
49
+ for cl in env_or_list:
50
+ cl_name = to_kebab_case(cl.__name__)
51
+ subc[cl_name] = {}
52
+ ooo = create_with_missing(cl, disk.get(cl_name, {}), subc=subc[cl_name])
53
+ kwargs["subcommands_default_union"][cl_name] = asdict(ooo)
54
+ # `kwargs["default"]` remains empty for now as there is no bare default that tyro would support as everything is hidden under the subcommands
55
+
56
+ else:
57
+ kwargs["default"] = create_with_missing(env_or_list, disk, subc=subc)
58
+
59
+ if subc:
60
+ kwargs["subcommands_default"] = subc
59
61
  except TypeError:
60
62
  raise SyntaxError(f"Config file parsing failed for {config_file}")
61
63
 
@@ -94,4 +96,4 @@ def ensure_settings_inheritance(
94
96
  for key, value in vars(create_with_missing(_def_fact, confopt)).items():
95
97
  if value is not MISSING_NONPROP:
96
98
  setattr(runopt, key, value)
97
- return runopt
99
+ return runopt