mininterface 1.2.1__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.1 → mininterface-1.2.2}/PKG-INFO +5 -5
  2. {mininterface-1.2.1 → mininterface-1.2.2}/README.md +4 -4
  3. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/auxiliary.py +81 -35
  4. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/cli_parser.py +1 -9
  5. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/dataclass_creation.py +12 -5
  6. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/tyro_patches.py +22 -9
  7. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/adaptor.py +21 -0
  8. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/secret_entry.py +1 -0
  9. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/select_input.py +12 -1
  10. {mininterface-1.2.1 → mininterface-1.2.2}/pyproject.toml +1 -1
  11. {mininterface-1.2.1 → mininterface-1.2.2}/LICENSE +0 -0
  12. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/__init__.py +0 -0
  13. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/__main__.py +0 -0
  14. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/__init__.py +0 -0
  15. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/argparse_support.py +0 -0
  16. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/cli_flags.py +0 -0
  17. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/cli_utils.py +0 -0
  18. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/config_file.py +0 -0
  19. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/form_dict.py +0 -0
  20. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/future_compatibility.py +0 -0
  21. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/redirectable.py +0 -0
  22. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/run.py +0 -0
  23. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/shortcuts.py +0 -0
  24. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/showcase.py +0 -0
  25. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_lib/start.py +0 -0
  26. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_mininterface/__init__.py +0 -0
  27. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_mininterface/adaptor.py +0 -0
  28. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_mininterface/mixin.py +0 -0
  29. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_text_interface/__init__.py +0 -0
  30. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_text_interface/adaptor.py +0 -0
  31. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_text_interface/facet.py +0 -0
  32. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_text_interface/timeout.py +0 -0
  33. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/__init__.py +0 -0
  34. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/adaptor.py +0 -0
  35. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/button_contents.py +0 -0
  36. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/facet.py +0 -0
  37. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/file_picker_input.py +0 -0
  38. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/form_contents.py +0 -0
  39. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/secret_input.py +0 -0
  40. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/style.tcss +0 -0
  41. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/textual_app.py +0 -0
  42. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/timeout.py +0 -0
  43. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_textual_interface/widgets.py +0 -0
  44. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/__init__.py +0 -0
  45. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/date_entry.py +0 -0
  46. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/external_fix.py +0 -0
  47. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/facet.py +0 -0
  48. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/redirect_text_tkinter.py +0 -0
  49. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/timeout.py +0 -0
  50. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_tk_interface/utils.py +0 -0
  51. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_web_interface/__init__.py +0 -0
  52. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_web_interface/app.py +0 -0
  53. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_web_interface/child_adaptor.py +0 -0
  54. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/_web_interface/parent_adaptor.py +0 -0
  55. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/cli.py +0 -0
  56. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/exceptions.py +0 -0
  57. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/experimental.py +0 -0
  58. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/facet/__init__.py +0 -0
  59. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/interfaces.py +0 -0
  60. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/settings.py +0 -0
  61. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/__init__.py +0 -0
  62. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/alias.py +0 -0
  63. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/callback_tag.py +0 -0
  64. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/datetime_tag.py +0 -0
  65. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/flag.py +0 -0
  66. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/internal.py +0 -0
  67. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/path_tag.py +0 -0
  68. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/secret_tag.py +0 -0
  69. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/select_tag.py +0 -0
  70. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/tag.py +0 -0
  71. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/tag_factory.py +0 -0
  72. {mininterface-1.2.1 → mininterface-1.2.2}/mininterface/tag/type_stubs.py +0 -0
  73. {mininterface-1.2.1 → 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.1
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,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
 
@@ -245,15 +245,7 @@ def parse_cli(
245
245
  kwargs, None if helponly else m, args, type_form, env_classes, _custom_registry, annot, _req_fields
246
246
  )
247
247
 
248
- # Why setting m.env instead of putting into into a constructor of a new get_interface() call?
249
- # 1. Getting the interface is a costly operation
250
- # 2. There is this bug so that we need to use single interface:
251
- # TODO
252
- # As this works badly, lets make sure we use single interface now
253
- # and will not need the second one.
254
- # get_interface("gui")
255
- # m = get_interface("gui")
256
- # m.select([1,2,3])
248
+ # Make the interface ready for the user
257
249
  m.env = env
258
250
  except SystemExit as exception:
259
251
  # --- (C) The dialog missing section ---
@@ -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
  }
@@ -339,16 +339,23 @@ def custom_parse_known_args(cf: CliFlags):
339
339
  def _(self: TyroArgumentParser, args=None, namespace=None):
340
340
  namespace, args = orig(self, args, namespace)
341
341
  # NOTE We may check that the Env does not have its own `verbose``
342
- # NOTE I do not like much tests need force=True here as they are run in paralel.
343
342
  if cf.add_verbose and hasattr(namespace, "verbose"):
344
- if namespace.verbose > 0:
345
- logging.basicConfig(
346
- level=cf.get_log_level(namespace.verbose), format="%(message)s", force=True, stream=cf.orig_stream
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
347
349
  )
348
- else:
349
350
  logging.basicConfig(
350
- level=cf.default_verbosity, format="%(message)s", force=True, stream=cf.orig_stream
351
+ level=level, format="%(message)s", stream=cf.orig_stream
351
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)
352
359
  delattr(namespace, "verbose")
353
360
 
354
361
  if cf.add_version and hasattr(namespace, "version"):
@@ -365,9 +372,15 @@ def custom_parse_known_args(cf: CliFlags):
365
372
 
366
373
  if cf.add_quiet and hasattr(namespace, "quiet"):
367
374
  if namespace.quiet:
368
- logging.basicConfig(
369
- level=cf.get_log_level(-1), format="%(message)s", force=True, stream=cf.orig_stream
370
- )
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)
371
384
  delattr(namespace, "quiet")
372
385
  return namespace, args
373
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"
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "mininterface"
7
- version = "1.2.1"
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