mininterface 1.2.0__tar.gz → 1.2.2__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.2.0 → mininterface-1.2.2}/PKG-INFO +5 -5
  2. {mininterface-1.2.0 → mininterface-1.2.2}/README.md +4 -4
  3. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/__main__.py +0 -1
  4. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/auxiliary.py +81 -35
  5. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/cli_flags.py +119 -3
  6. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/cli_parser.py +30 -18
  7. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/dataclass_creation.py +12 -5
  8. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/run.py +2 -12
  9. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/tyro_patches.py +83 -84
  10. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/adaptor.py +21 -0
  11. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/secret_entry.py +1 -0
  12. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/select_input.py +12 -1
  13. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/exceptions.py +16 -0
  14. {mininterface-1.2.0 → mininterface-1.2.2}/pyproject.toml +1 -1
  15. {mininterface-1.2.0 → mininterface-1.2.2}/LICENSE +0 -0
  16. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/__init__.py +0 -0
  17. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/__init__.py +0 -0
  18. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/argparse_support.py +0 -0
  19. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/cli_utils.py +0 -0
  20. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/config_file.py +0 -0
  21. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/form_dict.py +0 -0
  22. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/future_compatibility.py +0 -0
  23. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/redirectable.py +0 -0
  24. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/shortcuts.py +0 -0
  25. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/showcase.py +0 -0
  26. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_lib/start.py +0 -0
  27. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_mininterface/__init__.py +0 -0
  28. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_mininterface/adaptor.py +0 -0
  29. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_mininterface/mixin.py +0 -0
  30. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_text_interface/__init__.py +0 -0
  31. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_text_interface/adaptor.py +0 -0
  32. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_text_interface/facet.py +0 -0
  33. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_text_interface/timeout.py +0 -0
  34. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/__init__.py +0 -0
  35. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/adaptor.py +0 -0
  36. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/button_contents.py +0 -0
  37. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/facet.py +0 -0
  38. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/file_picker_input.py +0 -0
  39. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/form_contents.py +0 -0
  40. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/secret_input.py +0 -0
  41. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/style.tcss +0 -0
  42. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/textual_app.py +0 -0
  43. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/timeout.py +0 -0
  44. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_textual_interface/widgets.py +0 -0
  45. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/__init__.py +0 -0
  46. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/date_entry.py +0 -0
  47. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/external_fix.py +0 -0
  48. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/facet.py +0 -0
  49. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
  50. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/timeout.py +0 -0
  51. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_tk_interface/utils.py +0 -0
  52. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_web_interface/__init__.py +0 -0
  53. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_web_interface/app.py +0 -0
  54. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_web_interface/child_adaptor.py +0 -0
  55. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/_web_interface/parent_adaptor.py +0 -0
  56. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/cli.py +0 -0
  57. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/experimental.py +0 -0
  58. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/facet/__init__.py +0 -0
  59. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/interfaces.py +0 -0
  60. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/settings.py +0 -0
  61. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/__init__.py +0 -0
  62. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/alias.py +0 -0
  63. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/callback_tag.py +0 -0
  64. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/datetime_tag.py +0 -0
  65. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/flag.py +0 -0
  66. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/internal.py +0 -0
  67. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/path_tag.py +0 -0
  68. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/secret_tag.py +0 -0
  69. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/select_tag.py +0 -0
  70. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/tag.py +0 -0
  71. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/tag_factory.py +0 -0
  72. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/tag/type_stubs.py +0 -0
  73. {mininterface-1.2.0 → mininterface-1.2.2}/mininterface/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mininterface
3
- Version: 1.2.0
3
+ Version: 1.2.2
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
@@ -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
@@ -1,4 +1,3 @@
1
- from ast import literal_eval
2
1
  from dataclasses import dataclass
3
2
  from os import environ
4
3
  from pathlib import Path
@@ -1,20 +1,36 @@
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
+ Annotated,
9
+ Callable,
10
+ Iterable,
11
+ Optional,
12
+ TypeVar,
13
+ Union,
14
+ Literal,
15
+ get_args,
16
+ get_origin,
17
+ get_type_hints,
18
+ )
9
19
 
10
20
  from annotated_types import Ge, Gt, Le, Len, Lt, MultipleOf
11
21
 
12
22
  logger = logging.getLogger(__name__)
13
23
 
14
24
  try:
15
- from tyro.extras import get_parser
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
16
30
  except ImportError:
17
- get_parser = None
31
+ tyro = None
32
+ _tyro_docstrings_available = False
33
+ _tyro_get_callable_description = None
18
34
 
19
35
  try:
20
36
  from humanize import naturalsize as naturalsize_
@@ -70,40 +86,70 @@ def get_terminal_size():
70
86
  return 0, 0
71
87
 
72
88
 
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
-
89
+ def get_class_description(obj) -> str:
90
+ if _tyro_get_callable_description:
91
+ return _tyro_get_callable_description(obj)
92
+ return ""
83
93
 
84
94
  @lru_cache
85
- def _get_parser(obj):
86
- if get_parser:
87
- return get_parser(obj)
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
88
141
 
89
142
 
90
143
  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 ""
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 ""
107
153
 
108
154
 
109
155
  def yield_annotations(dataclass):
@@ -358,7 +404,7 @@ def strip_none(annotation):
358
404
  args = tuple(arg for arg in get_args(annotation) if arg is not type(None))
359
405
  if len(args) == 1:
360
406
  return args[0]
361
- return Union[args] # nebo origin[args], aby se zachoval typ
407
+ return Union[args]
362
408
 
363
409
  return annotation
364
410
 
@@ -1,15 +1,22 @@
1
- from functools import lru_cache
1
+ from argparse import ArgumentParser
2
2
  import logging
3
- from dataclasses import dataclass
3
+ import sys
4
4
  from typing import Optional, Sequence
5
5
 
6
+ from tyro.conf import FlagConversionOff
7
+
6
8
  from .form_dict import EnvClass
7
9
 
10
+ from typing import List, Any, Optional
11
+
12
+ from tyro._fields import FieldDefinition
13
+ from tyro.conf._confstruct import _ArgConfig
14
+
8
15
 
9
16
  class CliFlags:
10
17
 
11
18
  _add_verbose: bool = False
12
- version: bool | str = False
19
+ version: str = ""
13
20
  _add_quiet: bool = False
14
21
 
15
22
  default_verbosity: int = logging.WARNING
@@ -56,6 +63,17 @@ class CliFlags:
56
63
  # config
57
64
  self.config = add_config
58
65
 
66
+ self.orig_stream = (
67
+ sys.stderr
68
+ ) # NOTE might be removed now. Might be used if we redirect_stderr while setting basicConfig.
69
+
70
+ self.field_list: list[FieldDefinition] = []
71
+ """ List of FieldDefinitions corresponding to the arguments added via this helper"""
72
+
73
+ self.arguments_prepared: list[dict[str, Any]] = []
74
+ self.setup_done = False
75
+ """ Setup might be called multiple times – ex. parsing fails and we call tyro.cli in recursion. """
76
+
59
77
  def should_add(self, env_classes: list[EnvClass]) -> bool:
60
78
  # Flags are added only if neither the env_class nor any of the subcommands have the same-name flag already
61
79
  self._enabled["verbose"] = self._add_verbose and self._attr_not_present("verbose", env_classes)
@@ -116,3 +134,101 @@ class CliFlags:
116
134
  seq = self._verbosity_sequence
117
135
  log_level = {i + 1: level for i, level in enumerate(seq)}.get(count, logging.NOTSET)
118
136
  return log_level
137
+
138
+ def add_typed_argument(
139
+ self,
140
+ prefix: str,
141
+ *aliases: str,
142
+ action: Optional[str] = None,
143
+ default: Any = False,
144
+ helptext: Optional[str] = None,
145
+ metavar: Optional[str] = None,
146
+ version: Optional[str] = None,
147
+ ) -> FieldDefinition:
148
+ # Prepare FieldDefinition
149
+ name = aliases[0]
150
+ aliases_ = tuple((prefix * (1 if len(n) == 1 else 2) + n) for n in aliases) if aliases else None
151
+ typ_ = bool if action in ("store_true", "store_false") else int if action == "count" else str
152
+
153
+ field = FieldDefinition(
154
+ intern_name=name,
155
+ extern_name=name,
156
+ type=typ_,
157
+ type_stripped=typ_,
158
+ default=default,
159
+ helptext=helptext,
160
+ markers={FlagConversionOff},
161
+ custom_constructor=False,
162
+ argconf=_ArgConfig(
163
+ name=aliases_[0],
164
+ metavar="",
165
+ help=helptext,
166
+ help_behavior_hint="",
167
+ aliases=aliases_[1:] or None,
168
+ prefix_name=False,
169
+ constructor_factory=None,
170
+ default=default,
171
+ ),
172
+ mutex_group=None,
173
+ call_argname=name,
174
+ )
175
+
176
+ self.field_list.append(field)
177
+
178
+ # prepare argparse
179
+ self.arguments_prepared.append(
180
+ {
181
+ "field": field,
182
+ "names": aliases_,
183
+ "kwargs": {
184
+ "action": action,
185
+ "default": default,
186
+ "help": helptext,
187
+ "metavar": metavar,
188
+ "version": version,
189
+ },
190
+ }
191
+ )
192
+
193
+ return field
194
+
195
+ def setup(self, parser: ArgumentParser):
196
+ if self.setup_done:
197
+ # tyro.cli might be called multiple times if some missing required fields
198
+ return
199
+ self.setup_done = True
200
+ prefix = "-" if "-" in parser.prefix_chars else parser.prefix_chars[0]
201
+ if self.add_verbose:
202
+ self.add_typed_argument(
203
+ prefix,
204
+ "verbose",
205
+ "v",
206
+ action="count",
207
+ default=0,
208
+ helptext="verbosity level, can be used multiple times to increase",
209
+ )
210
+
211
+ if self.add_version:
212
+ self.add_typed_argument(
213
+ prefix,
214
+ "version",
215
+ action="version",
216
+ version=self.version,
217
+ default="",
218
+ helptext=f"show program's version number ({self.version}) and exit",
219
+ )
220
+
221
+ if self.add_quiet:
222
+ self.add_typed_argument(
223
+ prefix, "quiet", "q", action="store_true", helptext="suppress warnings, display only errors"
224
+ )
225
+
226
+ if self.add_config:
227
+ self.add_typed_argument(
228
+ prefix, "config", helptext=f"path to config file to fetch the defaults from", metavar="PATH"
229
+ )
230
+
231
+ def apply_to_parser(self, parser):
232
+ for item in self.arguments_prepared:
233
+ kwargs = {k: v for k, v in item["kwargs"].items() if v is not None}
234
+ parser.add_argument(*item["names"], **kwargs)
@@ -11,7 +11,6 @@ from contextlib import ExitStack, redirect_stderr, redirect_stdout
11
11
  from typing import Annotated, Optional, Sequence, Type, Union
12
12
  from unittest.mock import patch
13
13
 
14
- from .cli_flags import CliFlags
15
14
 
16
15
  from ..cli import Command
17
16
  from ..settings import CliSettings
@@ -33,10 +32,20 @@ from .dataclass_creation import (
33
32
  from .form_dict import EnvClass, TagDict, dataclass_to_tagdict, MissingTagValue, dict_added_main
34
33
 
35
34
  try:
35
+ from .cli_flags import CliFlags
36
36
  from tyro import cli
37
- from tyro._argparse import _SubParsersAction, ArgumentParser
38
- from tyro._argparse_formatter import TyroArgumentParser
39
- from tyro._singleton import MISSING_NONPROP
37
+
38
+ try: # tyro >= 0.10
39
+ from tyro import _experimental_options
40
+
41
+ _experimental_options["backend"] = "argparse"
42
+ from tyro._backends._argparse import _SubParsersAction, ArgumentParser
43
+ from tyro._backends._argparse_formatter import TyroArgumentParser
44
+ except ImportError:
45
+ from tyro._argparse import _SubParsersAction, ArgumentParser
46
+ from tyro._argparse_formatter import TyroArgumentParser
47
+ from tyro._parsers import ParserSpecification
48
+
40
49
  from tyro.conf import OmitArgPrefixes, OmitSubcommandPrefixes, DisallowNone, FlagCreatePairsOff
41
50
 
42
51
  from .tyro_patches import (
@@ -45,7 +54,8 @@ try:
45
54
  custom_init,
46
55
  custom_parse_known_args,
47
56
  failed_fields,
48
- patched_parse_known_args,
57
+ patched__parse_known_args,
58
+ patched__format_help,
49
59
  subparser_call,
50
60
  argparse_init,
51
61
  )
@@ -183,6 +193,7 @@ def parse_cli(
183
193
  helponly = False
184
194
  try:
185
195
  # Why redirect_stdout? Help-text shows the defaults, which also uses the subcommanded-config.
196
+ # TODO maybe new tyro 0.10 will not output to stdout, get rid of the buffer
186
197
  with redirect_stdout(buffer):
187
198
  try:
188
199
  # Standard way.
@@ -234,16 +245,8 @@ def parse_cli(
234
245
  kwargs, None if helponly else m, args, type_form, env_classes, _custom_registry, annot, _req_fields
235
246
  )
236
247
 
237
- # Why setting m.env instead of putting into into a constructor of a new get_interface() call?
238
- # 1. Getting the interface is a costly operation
239
- # 2. There is this bug so that we need to use single interface:
240
- # TODO
241
- # As this works badly, lets make sure we use single interface now
242
- # and will not need the second one.
243
- # get_interface("gui")
244
- # m = get_interface("gui")
245
- # m.select([1,2,3])
246
- m.env = env
248
+ # Make the interface ready for the user
249
+ m.env = env
247
250
  except SystemExit as exception:
248
251
  # --- (C) The dialog missing section ---
249
252
  # Some fields are needed to be filled up.
@@ -339,8 +342,7 @@ def _apply_patches(cf: Optional[CliFlags], ask_for_missing, env_classes, kwargs)
339
342
  patches = []
340
343
 
341
344
  patches.append(patch.object(_SubParsersAction, "__call__", subparser_call))
342
- patches.append(patch.object(TyroArgumentParser, "_parse_known_args", patched_parse_known_args))
343
-
345
+ patches.append(patch.object(TyroArgumentParser, "_parse_known_args", patched__parse_known_args))
344
346
  kw = {
345
347
  k: v for k, v in kwargs.items() if k != "default"
346
348
  } # NOTE I might separate kwargs['default'] and do not do this filtering
@@ -359,6 +361,11 @@ def _apply_patches(cf: Optional[CliFlags], ask_for_missing, env_classes, kwargs)
359
361
  "__init__",
360
362
  custom_init(cf),
361
363
  ),
364
+ patch.object(
365
+ TyroArgumentParser,
366
+ "format_help",
367
+ patched__format_help(cf),
368
+ ),
362
369
  patch.object(
363
370
  TyroArgumentParser,
364
371
  "parse_known_args",
@@ -486,7 +493,12 @@ def _fetch_currently_failed(requireds) -> TagDict:
486
493
  missing_req = {}
487
494
  for field in failed_fields.get():
488
495
  # ex: `_subcommands._nested_subcommands (positional)`
489
- fname = field.dest.replace(" (positional)", "").replace("-", "_") # `_subcommands._nested_subcommands`
496
+ fname = (
497
+ field.dest.replace(" (positional)", "")
498
+ .replace("-", "_")
499
+ .replace("__tyro_dummy_inner__.", "")
500
+ .replace("__tyro_dummy_inner__", "")
501
+ ) # `_subcommands._nested_subcommands`
490
502
  fname_raw = fname.rsplit(".", 1)[-1] # `_nested_subcommands`
491
503
 
492
504
  if isinstance(field, _SubParsersAction):
@@ -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,7 @@ 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, get_class_description, get_description
23
21
  from .form_dict import DataClass, EnvClass, MissingTagValue
24
22
 
25
23
  # Pydantic is not a project dependency, that is just an optional integration
@@ -49,6 +47,15 @@ def coerce_type_to_annotation(value, annotation):
49
47
  annotation = _unwrap_annotated(annotation) # NOTE might be superfluous, called before
50
48
  origin = get_origin(annotation)
51
49
 
50
+ # Handle Union (e.g. int | None)
51
+ if origin in (Union, UnionType):
52
+ for arg in get_args(annotation):
53
+ try:
54
+ return coerce_type_to_annotation(value, arg)
55
+ except Exception:
56
+ pass
57
+ return value
58
+
52
59
  # Handle tuple[...] conversion
53
60
  if origin is tuple and isinstance(value, list):
54
61
  args = get_args(annotation)
@@ -353,7 +360,7 @@ def choose_subcommand(env_classes: list[Type[DataClass]], m: "Mininterface[EnvCl
353
360
  # NOTE make select display buttons if there is a little amount of options.
354
361
  env = m.select(
355
362
  {
356
- (to_kebab_case(cl.__name__).replace("-", " ").capitalize(), _get_parser(cl).description): cl
363
+ (to_kebab_case(cl.__name__).replace("-", " ").capitalize(), get_class_description(cl)): cl
357
364
  for cl in env_classes
358
365
  if cl is not SubcommandPlaceholder
359
366
  }
@@ -7,7 +7,7 @@ from typing import Literal, Optional, Sequence, Type
7
7
 
8
8
 
9
9
  from .._mininterface import Mininterface
10
- from ..exceptions import DependencyRequired, ValidationFail
10
+ from ..exceptions import DependencyRequired, ValidationFail, _debug_wanted
11
11
  from ..interfaces import get_interface
12
12
  from ..settings import CliSettings, MininterfaceSettings, UiSettings
13
13
  from .form_dict import EnvClass
@@ -332,17 +332,7 @@ def run(
332
332
  try:
333
333
  parse_cli(env_or_list, kwargs, m, cf, ask_for_missing, args, ask_on_empty_cli, cliset)
334
334
  except Exception as e:
335
- # Undocumented MININTERFACE_DEBUG flag. Note ipdb package requirement.
336
- from ast import literal_eval
337
-
338
- if literal_eval(environ.get("MININTERFACE_DEBUG", "0")):
339
- import traceback
340
-
341
- import ipdb
342
-
343
- traceback.print_exception(e)
344
- ipdb.post_mortem()
345
- else:
335
+ if not _debug_wanted(e):
346
336
  raise
347
337
 
348
338
  # Command run
@@ -5,14 +5,22 @@ from collections import deque
5
5
  from contextvars import ContextVar
6
6
  from gettext import gettext as _
7
7
  import sys
8
- from typing import Optional, Callable
9
8
 
10
- from tyro import _argparse as argparse
11
- from tyro._argparse import Action, _SubParsersAction, ArgumentParser
12
- from tyro._argparse_formatter import TyroArgumentParser
13
9
 
14
10
  from .cli_flags import CliFlags
15
11
 
12
+ try:
13
+ # tyro >= 0.10
14
+ from tyro._backends._argparse import _get_action_name, SUPPRESS, ArgumentError
15
+ from tyro._backends._argparse import Action, _SubParsersAction, ArgumentParser
16
+ from tyro._backends._argparse_formatter import TyroArgumentParser
17
+ except ImportError:
18
+ from tyro._argparse import _get_action_name, SUPPRESS, ArgumentError
19
+ from tyro._argparse import Action, _SubParsersAction, ArgumentParser
20
+ from tyro._argparse_formatter import TyroArgumentParser
21
+ from tyro import _arguments
22
+
23
+
16
24
  failed_fields: ContextVar[list[Action]] = ContextVar("failed_fields", default=[])
17
25
  _orig_call = _SubParsersAction.__call__
18
26
  _crawling = ContextVar("_crawling", default=deque())
@@ -25,18 +33,7 @@ _orig_init = ArgumentParser.__init__
25
33
  #
26
34
  # The only line changed: failed_fields
27
35
  #
28
- def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # pragma: no cover
29
- """We override _parse_known_args() to improve error messages in the presence of
30
- subcommands. Difference is marked with <new>...</new> below."""
31
-
32
- # <new>
33
- # Reset the unused argument list in the root parser.
34
- # Subparsers will have spaces in self.prog.
35
- if " " not in self.prog:
36
- global global_unrecognized_arg_and_prog
37
- global_unrecognized_arg_and_prog = []
38
- # </new>
39
-
36
+ def patched__parse_known_args(self, arg_strings, namespace):
40
37
  # replace arg strings that are file references
41
38
  if self.fromfile_prefix_chars is not None:
42
39
  arg_strings = self._read_args_from_files(arg_strings)
@@ -58,6 +55,7 @@ def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # p
58
55
  arg_string_pattern_parts = []
59
56
  arg_strings_iter = iter(arg_strings)
60
57
  for i, arg_string in enumerate(arg_strings_iter):
58
+
61
59
  # all args after -- are non-options
62
60
  if arg_string == "--":
63
61
  arg_string_pattern_parts.append("-")
@@ -94,16 +92,17 @@ def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # p
94
92
  for conflict_action in action_conflicts.get(action, []):
95
93
  if conflict_action in seen_non_default_actions:
96
94
  msg = _("not allowed with argument %s")
97
- action_name = argparse._get_action_name(conflict_action)
98
- raise argparse.ArgumentError(action, msg % action_name)
95
+ action_name = _get_action_name(conflict_action)
96
+ raise ArgumentError(action, msg % action_name)
99
97
 
100
98
  # take the action if we didn't receive a SUPPRESS value
101
99
  # (e.g. from a default)
102
- if argument_values is not argparse.SUPPRESS:
100
+ if argument_values is not SUPPRESS:
103
101
  action(self, namespace, argument_values, option_string)
104
102
 
105
103
  # function to convert arg_strings into an optional action
106
104
  def consume_optional(start_index):
105
+
107
106
  # get the optional identified at this index
108
107
  option_tuple = option_string_indices[start_index]
109
108
  action, option_string, sep, explicit_arg = option_tuple
@@ -113,14 +112,9 @@ def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # p
113
112
  match_argument = self._match_argument
114
113
  action_tuples = []
115
114
  while True:
115
+
116
116
  # if we found no optional action, skip it
117
117
  if action is None:
118
- # <new>
119
- # Manually track unused arguments to assist with error messages
120
- # later.
121
- if not self._parsing_known_args:
122
- global_unrecognized_arg_and_prog.append((option_string, self.prog))
123
- # </new>
124
118
  extras.append(arg_strings[start_index])
125
119
  return start_index + 1
126
120
 
@@ -136,7 +130,7 @@ def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # p
136
130
  if arg_count == 0 and option_string[1] not in chars and explicit_arg != "":
137
131
  if sep or explicit_arg[0] in chars:
138
132
  msg = _("ignored explicit argument %r")
139
- raise argparse.ArgumentError(action, msg % explicit_arg)
133
+ raise ArgumentError(action, msg % explicit_arg)
140
134
  action_tuples.append((action, [], option_string))
141
135
  char = option_string[0]
142
136
  option_string = char + explicit_arg[0]
@@ -167,7 +161,7 @@ def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # p
167
161
  # explicit argument
168
162
  else:
169
163
  msg = _("ignored explicit argument %r")
170
- raise argparse.ArgumentError(action, msg % explicit_arg)
164
+ raise ArgumentError(action, msg % explicit_arg)
171
165
 
172
166
  # if there is no explicit argument, try to match the
173
167
  # optional's string arguments with the following strings
@@ -220,6 +214,7 @@ def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # p
220
214
  else:
221
215
  max_option_string_index = -1
222
216
  while start_index <= max_option_string_index:
217
+
223
218
  # consume any Positionals preceding the next option
224
219
  next_option_string_index = min([index for index in option_string_indices if index >= start_index])
225
220
  if start_index != next_option_string_index:
@@ -256,7 +251,7 @@ def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # p
256
251
  if action not in seen_actions:
257
252
  if action.required:
258
253
  failed_fields.get().append(action) # WE ADDED THIS LINE
259
- required_actions.append(argparse._get_action_name(action))
254
+ required_actions.append(_get_action_name(action))
260
255
  else:
261
256
  # Convert action default now instead of doing it before
262
257
  # parsing arguments to avoid calling convert functions
@@ -268,11 +263,7 @@ def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # p
268
263
  and hasattr(namespace, action.dest)
269
264
  and action.default is getattr(namespace, action.dest)
270
265
  ):
271
- setattr(
272
- namespace,
273
- action.dest,
274
- self._get_value(action, action.default),
275
- )
266
+ setattr(namespace, action.dest, self._get_value(action, action.default))
276
267
 
277
268
  if required_actions:
278
269
  self.error(_("the following arguments are required: %s") % ", ".join(required_actions))
@@ -286,13 +277,9 @@ def patched_parse_known_args(self, arg_strings, namespace): # type: ignore # p
286
277
 
287
278
  # if no actions were used, report the error
288
279
  else:
289
- names = [
290
- argparse._get_action_name(action)
291
- for action in group._group_actions
292
- if action.help is not argparse.SUPPRESS
293
- ]
280
+ names = [_get_action_name(action) for action in group._group_actions if action.help is not SUPPRESS]
294
281
  msg = _("one of the arguments %s is required")
295
- self.error(msg % " ".join(names)) # type: ignore
282
+ self.error(msg % " ".join(names))
296
283
 
297
284
  # return the updated namespace and the extra arguments
298
285
  return namespace, extras
@@ -314,65 +301,69 @@ def custom_error(self: TyroArgumentParser, message: str):
314
301
 
315
302
 
316
303
  def custom_init(cf: CliFlags):
304
+ orig_init = TyroArgumentParser.__init__
305
+
317
306
  def _(self: TyroArgumentParser, *args, **kwargs):
318
- super(TyroArgumentParser, self).__init__(*args, **kwargs)
319
- default_prefix = "-" if "-" in self.prefix_chars else self.prefix_chars[0]
320
- if cf.add_verbose:
321
- self.add_argument(
322
- default_prefix + "v",
323
- default_prefix * 2 + "verbose",
324
- action="count",
325
- default=0,
326
- help="verbosity level, can be used multiple times to increase",
327
- )
307
+ orig_init(self, *args, **kwargs)
328
308
 
329
- if cf.add_version:
330
- self.add_argument(
331
- default_prefix * 2 + "version",
332
- # NOTE We use the native version, but it inserts a blank line
333
- action="version",
334
- version=cf.version,
335
- # Our custom version works bad with subcommands, we have to first resolve subcommands,
336
- # than it comes to the version
337
- # action="store_const",
338
- # const=cf.version,
339
- help=f"show program's version number ({cf.version}) and exit",
340
- )
309
+ cf.setup(self)
310
+ cf.apply_to_parser(self)
311
+
312
+ return _
341
313
 
342
- if cf.add_quiet:
343
- self.add_argument(
344
- default_prefix + "q",
345
- default_prefix * 2 + "quiet",
346
- action="store_true",
347
- help="suppress warnings, display only errors",
348
- )
349
314
 
350
- if cf.add_config:
351
- self.add_argument(
352
- default_prefix * 2 + "config", help=f"path to config file to fetch the defaults from", metavar="PATH"
315
+ def patched__format_help(cf: CliFlags):
316
+ orig = TyroArgumentParser.format_help
317
+
318
+ def _(self, *args, **kwargs):
319
+ parser_spec = self._parser_specification
320
+
321
+ for field in reversed(cf.field_list):
322
+ field_out = _arguments.ArgumentDefinition(
323
+ intern_prefix=field.intern_name,
324
+ extern_prefix=field.extern_name,
325
+ subcommand_prefix="",
326
+ field=field,
353
327
  )
354
328
 
329
+ parser_spec.args.insert(0, field_out)
330
+
331
+ return orig(self, *args, **kwargs)
332
+
355
333
  return _
356
334
 
357
335
 
358
336
  def custom_parse_known_args(cf: CliFlags):
337
+ orig = TyroArgumentParser.parse_known_args
338
+
359
339
  def _(self: TyroArgumentParser, args=None, namespace=None):
360
- namespace, args = super(TyroArgumentParser, self).parse_known_args(args, namespace)
340
+ namespace, args = orig(self, args, namespace)
361
341
  # NOTE We may check that the Env does not have its own `verbose``
362
- # NOTE I do not like much tests need force=True here as they are run in paralel.
363
342
  if cf.add_verbose and hasattr(namespace, "verbose"):
364
- if namespace.verbose > 0:
365
- logging.basicConfig(level=cf.get_log_level(namespace.verbose), format="%(message)s", force=True)
366
- else:
367
- logging.basicConfig(level=cf.default_verbosity, format="%(message)s", force=True)
343
+ root = logging.getLogger()
344
+ if not root.handlers:
345
+ level = (
346
+ cf.get_log_level(namespace.verbose)
347
+ if namespace.verbose > 0
348
+ else cf.default_verbosity
349
+ )
350
+ logging.basicConfig(
351
+ level=level, format="%(message)s", stream=cf.orig_stream
352
+ )
353
+ elif namespace.verbose > 0:
354
+ level = cf.get_log_level(namespace.verbose)
355
+ root.setLevel(level)
356
+ for handler in root.handlers:
357
+ if handler.level > level: # increase verbosity for strict handlers
358
+ handler.setLevel(level)
368
359
  delattr(namespace, "verbose")
369
360
 
370
- # This code is now not used, see `custom_init`
371
- # if cf.add_verbose and hasattr(namespace, "version"):
372
- # if namespace.version:
373
- # print(namespace.version)
374
- # raise SystemExit(0)
375
- # delattr(namespace, "version")
361
+ if cf.add_version and hasattr(namespace, "version"):
362
+ # This code is now not used, see `custom_init`
363
+ # if namespace.version:
364
+ # print(namespace.version)
365
+ # raise SystemExit(0)
366
+ delattr(namespace, "version")
376
367
 
377
368
  # Note that we do not parse --config here as it is parsed at `run.py`, before CLI parsing.
378
369
  # Since config file serves as default fo CLI parsing.
@@ -381,7 +372,15 @@ def custom_parse_known_args(cf: CliFlags):
381
372
 
382
373
  if cf.add_quiet and hasattr(namespace, "quiet"):
383
374
  if namespace.quiet:
384
- logging.basicConfig(level=cf.get_log_level(-1), format="%(message)s", force=True)
375
+ new_level = cf.get_log_level(-1)
376
+ root = logging.getLogger()
377
+ if not root.handlers:
378
+ logging.basicConfig(level=new_level, format="%(message)s", stream=cf.orig_stream)
379
+ else:
380
+ root.setLevel(new_level)
381
+ for handler in root.handlers:
382
+ if handler.level < new_level: # edit just benevolent handlers
383
+ handler.setLevel(new_level)
385
384
  delattr(namespace, "quiet")
386
385
  return namespace, args
387
386
 
@@ -32,7 +32,27 @@ class TkAdaptor(Tk, RichUiAdaptor, BackendAdaptor):
32
32
  facet: TkFacet
33
33
  settings: GuiSettings
34
34
 
35
+ _instance = None
36
+ """ singleton """
37
+
38
+ def __new__(cls, *args, **kwargs):
39
+ # Singleton.
40
+ # Why enforcing singleton?
41
+ # Invoking second tk would mean a strange second window
42
+ # and non-responding tkinter variables in the second invocation.
43
+ # get_interface("gui")
44
+ # m = get_interface("gui")
45
+ # m.select([1,2,3]) # cannot choose the value
46
+ if cls._instance is None:
47
+ return Tk.__new__(cls)
48
+ return cls._instance
49
+
35
50
  def __init__(self, *args):
51
+ if self._instance:
52
+ return
53
+ else:
54
+ self.__class__._instance = self
55
+
36
56
  BackendAdaptor.__init__(self, *args)
37
57
 
38
58
  try:
@@ -40,6 +60,7 @@ class TkAdaptor(Tk, RichUiAdaptor, BackendAdaptor):
40
60
  except TclError:
41
61
  # even when installed the libraries are installed, display might not be available, hence tkinter fails
42
62
  raise InterfaceNotAvailable
63
+ self._initialized = True
43
64
 
44
65
  self.params = None
45
66
  self._result = None
@@ -25,6 +25,7 @@ class SecretEntryWrapper:
25
25
  """Handle toggle key event"""
26
26
  self.toggle_show()
27
27
  return "break" # Prevent event propagation
28
+
28
29
 
29
30
  def toggle_show(self):
30
31
  if self.tag.toggle_visibility():
@@ -193,7 +193,8 @@ class SelectInputWrapper:
193
193
  widget = AutoCombobox(self.frame, textvariable=self.variable)
194
194
  widget["values"] = [k for k, *_ in options]
195
195
  widget.pack()
196
- widget.bind("<Return>", lambda _: "break") # override default enter that submits the form
196
+ #widget.bind("<Return>", lambda _: "break") # override default enter that submits the form
197
+ widget.bind("<Return>", lambda _: self._enter_handler())
197
198
 
198
199
  self.set_default_label()
199
200
  self.taking_focus = widget
@@ -212,3 +213,13 @@ class SelectInputWrapper:
212
213
  # We never want to select the radiobutton in the initial phase
213
214
  # as this might trigger on_change action (not caused by the user)
214
215
  var.set(val)
216
+
217
+ def _enter_handler(self, event=None):
218
+ current_value = self.variable.get()
219
+
220
+ if not current_value:
221
+ return "break" # Let it perform the default behavior and open the dropdown
222
+
223
+ # If it has a value, submit it
224
+ self.adaptor._ok()
225
+ return "break"
@@ -1,5 +1,7 @@
1
1
  """Exceptions that might make sense to be used outside the library."""
2
2
 
3
+ from os import environ as _environ
4
+
3
5
 
4
6
  class Cancelled(SystemExit):
5
7
  """User has cancelled.
@@ -47,4 +49,18 @@ class DependencyRequired(InterfaceNotAvailable):
47
49
 
48
50
  def exit(self):
49
51
  """Wrap the exception in a SystemExit so that the program exits without a traceback."""
52
+ _debug_wanted(self)
50
53
  raise SystemExit(self)
54
+
55
+
56
+ def _debug_wanted(e: Exception):
57
+ # Undocumented MININTERFACE_DEBUG flag. Note ipdb package requirement.
58
+ from ast import literal_eval
59
+
60
+ if literal_eval(_environ.get("MININTERFACE_DEBUG", "0")):
61
+ import traceback
62
+ import ipdb
63
+
64
+ traceback.print_exception(e)
65
+ ipdb.post_mortem(e)
66
+ return True
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "mininterface"
7
- version = "1.2.0"
7
+ version = "1.2.2"
8
8
  description = "CLI & dialog toolkit – a minimal interface to Python application (GUI, TUI, CLI + config files, web)"
9
9
  authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
10
10
  license = "LGPL-3.0-or-later"
File without changes