mininterface 1.0.3__tar.gz → 1.1.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 (74) hide show
  1. {mininterface-1.0.3 → mininterface-1.1.0}/PKG-INFO +7 -7
  2. {mininterface-1.0.3 → mininterface-1.1.0}/README.md +6 -6
  3. mininterface-1.1.0/mininterface/__init__.py +7 -0
  4. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/__main__.py +21 -9
  5. mininterface-1.1.0/mininterface/_lib/argparse_support.py +266 -0
  6. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/auxiliary.py +165 -18
  7. mininterface-1.1.0/mininterface/_lib/cli_flags.py +107 -0
  8. mininterface-1.1.0/mininterface/_lib/cli_parser.py +408 -0
  9. mininterface-1.0.3/mininterface/cli.py → mininterface-1.1.0/mininterface/_lib/cli_utils.py +10 -53
  10. mininterface-1.1.0/mininterface/_lib/config_file.py +101 -0
  11. mininterface-1.1.0/mininterface/_lib/dataclass_creation.py +282 -0
  12. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/form_dict.py +43 -16
  13. mininterface-1.1.0/mininterface/_lib/future_compatibility.py +6 -0
  14. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/redirectable.py +3 -2
  15. mininterface-1.0.3/mininterface/__init__.py → mininterface-1.1.0/mininterface/_lib/run.py +161 -86
  16. mininterface-1.1.0/mininterface/_lib/shortcuts.py +59 -0
  17. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/showcase.py +4 -2
  18. mininterface-1.1.0/mininterface/_lib/start.py +135 -0
  19. mininterface-1.1.0/mininterface/_lib/tyro_patches.py +400 -0
  20. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_mininterface/__init__.py +107 -91
  21. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_mininterface/adaptor.py +12 -14
  22. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_mininterface/mixin.py +12 -12
  23. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_text_interface/__init__.py +21 -16
  24. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_text_interface/adaptor.py +19 -10
  25. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/__init__.py +3 -1
  26. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/adaptor.py +26 -14
  27. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/button_contents.py +3 -3
  28. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/facet.py +2 -0
  29. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/file_picker_input.py +2 -5
  30. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/form_contents.py +11 -7
  31. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/widgets.py +3 -2
  32. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/__init__.py +8 -2
  33. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/adaptor.py +33 -28
  34. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/date_entry.py +41 -36
  35. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/external_fix.py +2 -6
  36. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/facet.py +1 -0
  37. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/redirect_text_tkinter.py +4 -4
  38. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/secret_entry.py +12 -13
  39. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/select_input.py +47 -32
  40. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_tk_interface/utils.py +30 -21
  41. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_web_interface/__init__.py +16 -10
  42. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_web_interface/app.py +1 -0
  43. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_web_interface/child_adaptor.py +1 -2
  44. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_web_interface/parent_adaptor.py +2 -7
  45. mininterface-1.1.0/mininterface/cli.py +46 -0
  46. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/exceptions.py +10 -8
  47. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/experimental.py +6 -4
  48. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/facet/__init__.py +8 -7
  49. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/interfaces.py +13 -10
  50. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/settings.py +27 -33
  51. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/alias.py +2 -2
  52. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/callback_tag.py +2 -1
  53. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/datetime_tag.py +53 -8
  54. mininterface-1.1.0/mininterface/tag/flag.py +284 -0
  55. mininterface-1.1.0/mininterface/tag/internal.py +10 -0
  56. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/path_tag.py +12 -4
  57. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/select_tag.py +98 -26
  58. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/tag.py +142 -90
  59. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/tag_factory.py +54 -10
  60. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/type_stubs.py +4 -2
  61. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/validators.py +39 -20
  62. {mininterface-1.0.3 → mininterface-1.1.0}/pyproject.toml +4 -1
  63. mininterface-1.0.3/mininterface/_lib/cli_parser.py +0 -657
  64. mininterface-1.0.3/mininterface/_lib/start.py +0 -132
  65. mininterface-1.0.3/mininterface/tag/flag.py +0 -140
  66. mininterface-1.0.3/mininterface/tag/internal.py +0 -10
  67. {mininterface-1.0.3 → mininterface-1.1.0}/LICENSE +0 -0
  68. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_lib/__init__.py +0 -0
  69. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_text_interface/facet.py +0 -0
  70. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/secret_input.py +0 -0
  71. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/style.tcss +0 -0
  72. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/_textual_interface/textual_app.py +0 -0
  73. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/__init__.py +0 -0
  74. {mininterface-1.0.3 → mininterface-1.1.0}/mininterface/tag/secret_tag.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mininterface
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: A minimal access to GUI, TUI, CLI and config
5
5
  License: LGPL-3.0-or-later
6
6
  Author: Edvard Rejthar
@@ -37,7 +37,7 @@ Project-URL: Homepage, https://github.com/CZ-NIC/mininterface
37
37
  Description-Content-Type: text/markdown
38
38
 
39
39
  # Mininterface – access to GUI, TUI, web, CLI and config files
40
- [![Build Status](https://github.com/CZ-NIC/mininterface/actions/workflows/run-unittest.yml/badge.svg)](https://github.com/CZ-NIC/mininterface/actions)
40
+ [![Build Status](https://github.com/CZ-NIC/mininterface/actions/workflows/run-unittest.yml/badge.svg?branch=main)](https://github.com/CZ-NIC/mininterface/actions)
41
41
  [![Downloads](https://static.pepy.tech/badge/mininterface)](https://pepy.tech/project/mininterface)
42
42
 
43
43
  Write the program core, do not bother with the input/output.
@@ -144,7 +144,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
144
144
  Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
145
145
 
146
146
  ```bash
147
- pip install mininterface[all] # GPLv3 and compatible
147
+ pip install "mininterface[all]<2" # GPLv3 and compatible
148
148
  ```
149
149
 
150
150
  ## Bundles
@@ -187,7 +187,7 @@ These projects have the code base reduced thanks to the mininterface:
187
187
  Take a look at the following example.
188
188
 
189
189
  1. We define any Env class.
190
- 2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be prompter for
190
+ 2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be prompted for
191
191
  3. Then, we use various dialog methods, like [`confirm`][mininterface.Mininterface.confirm], [`select`][mininterface.Mininterface.select] or [`form`][mininterface.Mininterface.form].
192
192
 
193
193
  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...).
@@ -280,11 +280,11 @@ from pathlib import Path
280
280
  from mininterface import run
281
281
 
282
282
  parser = ArgumentParser()
283
- parser.add_argument("input_file", type=Path, help="Path to the input file.")
284
- parser.add_argument("--time", type=time, help="Given time")
285
283
  subparsers = parser.add_subparsers(dest="command", required=True)
286
284
  sub1 = subparsers.add_parser("build", help="Build something.")
287
285
  sub1.add_argument("--optimize", action="store_true", help="Enable optimizations.")
286
+ parser.add_argument("input_file", type=Path, help="Path to the input file.")
287
+ parser.add_argument("--time", type=time, help="Given time")
288
288
 
289
289
  # Old version
290
290
  # env = parser.parse_args()
@@ -335,4 +335,4 @@ print(m.env.time) # -> 14:21
335
335
  If you're sure enough to start using *Mininterface*, convert the argparse into a dataclass. Then, the IDE will auto-complete the hints as you type.
336
336
 
337
337
  !!! warning
338
- Be aware that in contrast to the argparse, we create default values. This does make sense for most values but might pose a confusion for ex. `parser.add_argument("--path", type=Path)` which becomes `Path('.')`, not `None`.
338
+ The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
@@ -1,5 +1,5 @@
1
1
  # Mininterface – access to GUI, TUI, web, CLI and config files
2
- [![Build Status](https://github.com/CZ-NIC/mininterface/actions/workflows/run-unittest.yml/badge.svg)](https://github.com/CZ-NIC/mininterface/actions)
2
+ [![Build Status](https://github.com/CZ-NIC/mininterface/actions/workflows/run-unittest.yml/badge.svg?branch=main)](https://github.com/CZ-NIC/mininterface/actions)
3
3
  [![Downloads](https://static.pepy.tech/badge/mininterface)](https://pepy.tech/project/mininterface)
4
4
 
5
5
  Write the program core, do not bother with the input/output.
@@ -106,7 +106,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
106
106
  Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
107
107
 
108
108
  ```bash
109
- pip install mininterface[all] # GPLv3 and compatible
109
+ pip install "mininterface[all]<2" # GPLv3 and compatible
110
110
  ```
111
111
 
112
112
  ## Bundles
@@ -149,7 +149,7 @@ These projects have the code base reduced thanks to the mininterface:
149
149
  Take a look at the following example.
150
150
 
151
151
  1. We define any Env class.
152
- 2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be prompter for
152
+ 2. Then, we initialize mininterface with [`run(Env)`][mininterface.run] – the missing fields will be prompted for
153
153
  3. Then, we use various dialog methods, like [`confirm`][mininterface.Mininterface.confirm], [`select`][mininterface.Mininterface.select] or [`form`][mininterface.Mininterface.form].
154
154
 
155
155
  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...).
@@ -242,11 +242,11 @@ from pathlib import Path
242
242
  from mininterface import run
243
243
 
244
244
  parser = ArgumentParser()
245
- parser.add_argument("input_file", type=Path, help="Path to the input file.")
246
- parser.add_argument("--time", type=time, help="Given time")
247
245
  subparsers = parser.add_subparsers(dest="command", required=True)
248
246
  sub1 = subparsers.add_parser("build", help="Build something.")
249
247
  sub1.add_argument("--optimize", action="store_true", help="Enable optimizations.")
248
+ parser.add_argument("input_file", type=Path, help="Path to the input file.")
249
+ parser.add_argument("--time", type=time, help="Given time")
250
250
 
251
251
  # Old version
252
252
  # env = parser.parse_args()
@@ -297,4 +297,4 @@ print(m.env.time) # -> 14:21
297
297
  If you're sure enough to start using *Mininterface*, convert the argparse into a dataclass. Then, the IDE will auto-complete the hints as you type.
298
298
 
299
299
  !!! warning
300
- Be aware that in contrast to the argparse, we create default values. This does make sense for most values but might pose a confusion for ex. `parser.add_argument("--path", type=Path)` which becomes `Path('.')`, not `None`.
300
+ The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.
@@ -0,0 +1,7 @@
1
+ from ._lib.run import run
2
+ from ._mininterface import Mininterface
3
+ from .exceptions import Cancelled
4
+ from .tag import Tag
5
+ from .tag.alias import Options, Validation
6
+
7
+ __all__ = ["run", "Mininterface", "Tag", "Cancelled", "Validation", "Options"]
@@ -9,9 +9,10 @@ try:
9
9
  from tyro.conf import Positional
10
10
  except ImportError:
11
11
  from .exceptions import DependencyRequired
12
+
12
13
  raise DependencyRequired("basic").exit()
13
14
 
14
- from . import run
15
+ from ._lib.run import run
15
16
  from .cli import Command
16
17
  from .tag.flag import File
17
18
  from .tag.path_tag import PathTag
@@ -36,7 +37,7 @@ Showcase_Type = Literal[1, 2]
36
37
 
37
38
  @dataclass
38
39
  class Alert(Command):
39
- """ Dialog: Display the OK dialog with text. """
40
+ """Dialog: Display the OK dialog with text."""
40
41
 
41
42
  text: Positional[str]
42
43
 
@@ -46,7 +47,7 @@ class Alert(Command):
46
47
 
47
48
  @dataclass
48
49
  class Ask(Command):
49
- """ Dialog: Prompt the user to input a value.
50
+ """Dialog: Prompt the user to input a value.
50
51
  By default, we input a str, by the second parameter, you can infer a type,
51
52
  ex. `mininterface --ask 'My heading' int`
52
53
  """
@@ -77,12 +78,15 @@ class Ask(Command):
77
78
  v = Path
78
79
  case "date":
79
80
  from datetime import date
81
+
80
82
  v = date
81
83
  case "datetime":
82
84
  from datetime import datetime
85
+
83
86
  v = datetime
84
87
  case "time":
85
88
  from datetime import time
89
+
86
90
  v = time
87
91
  case "file":
88
92
  v = PathTag(is_file=True)
@@ -95,7 +99,7 @@ class Ask(Command):
95
99
 
96
100
  @dataclass
97
101
  class Confirm(Command):
98
- """ Dialog: Display confirm box. Returns 0 / 1. """
102
+ """Dialog: Display confirm box. Returns 0 / 1."""
99
103
 
100
104
  text: Positional[str]
101
105
  focus: Positional[Literal["yes", "no"]] = "yes"
@@ -108,7 +112,8 @@ class Confirm(Command):
108
112
 
109
113
  @dataclass
110
114
  class Select(Command):
111
- """ Dialog: Prompt the user to select. """
115
+ """Dialog: Prompt the user to select."""
116
+
112
117
  options: Positional[list[str]]
113
118
  title: str = ""
114
119
 
@@ -118,7 +123,7 @@ class Select(Command):
118
123
 
119
124
  @dataclass
120
125
  class Integrate(Command):
121
- """ Integrate to the system. Generates a bash completion for the given program. """
126
+ """Integrate to the system. Generates a bash completion for the given program."""
122
127
 
123
128
  cmd: Positional[File]
124
129
  """Path to the program using mininterface.
@@ -126,17 +131,18 @@ class Integrate(Command):
126
131
  """
127
132
 
128
133
  def run(self):
129
- environ["MININTERFACE_INTEGRATE_TO_SYSTEM"] = '1'
134
+ environ["MININTERFACE_INTEGRATE_TO_SYSTEM"] = "1"
130
135
  srun(self.cmd.absolute(), env=environ)
131
136
  quit()
132
137
 
133
138
 
134
139
  @dataclass
135
140
  class Showcase:
136
- """ Prints various form just to show what's possible.
141
+ """Prints various form just to show what's possible.
137
142
  Choose the interface by MININTERFACE_INTERFACE=...
138
143
  Ex. MININTERFACE_INTERFACE=tui mininterface showcase 2
139
144
  """
145
+
140
146
  showcase: Positional[Showcase_Type] = 1
141
147
 
142
148
 
@@ -151,11 +157,17 @@ class Web(Command):
151
157
 
152
158
  def run(self):
153
159
  from ._web_interface import WebInterface
160
+
154
161
  WebInterface(cmd=self.cmd, port=self.port)
155
162
 
156
163
 
157
164
  def main():
158
- with run([Alert, Ask, Confirm, Select, Integrate, Showcase, Web], prog="Mininterface", description=__doc__, ask_for_missing=False) as m:
165
+ with run(
166
+ [Alert, Ask, Confirm, Select, Integrate, Showcase, Web],
167
+ prog="Mininterface",
168
+ description=__doc__,
169
+ ask_for_missing=False,
170
+ ) as m:
159
171
  pass
160
172
 
161
173
  if isinstance(m.env, Showcase):
@@ -0,0 +1,266 @@
1
+ import re
2
+ import sys
3
+ from argparse import (SUPPRESS, Action, ArgumentParser, _AppendAction,
4
+ _AppendConstAction, _CountAction, _HelpAction,
5
+ _StoreConstAction, _StoreFalseAction, _StoreTrueAction,
6
+ _SubParsersAction, _VersionAction)
7
+ from collections import defaultdict
8
+ from dataclasses import MISSING, Field, dataclass, field, make_dataclass
9
+ from functools import cached_property
10
+ from typing import Annotated, Callable, Optional
11
+ from warnings import warn
12
+
13
+ from ..tag.alias import Options
14
+ from .form_dict import DataClass
15
+
16
+ try:
17
+ from tyro.conf import DisallowNone, OmitSubcommandPrefixes, Positional
18
+ except ImportError:
19
+ from ..exceptions import DependencyRequired
20
+
21
+ raise DependencyRequired("basic")
22
+
23
+
24
+ class Property:
25
+ def __init__(self):
26
+ self._usages = []
27
+
28
+ def add(self, callback: Callable):
29
+ self._usages.append(callback)
30
+
31
+ def generate_property(self):
32
+ def _(this):
33
+ for clb in self._usages:
34
+ v = clb(this)
35
+ if v is not None:
36
+ return v
37
+
38
+ return property(_)
39
+
40
+
41
+ @dataclass
42
+ class ArgparseField:
43
+
44
+ action: Action
45
+ properties: dict[str, Property]
46
+
47
+ @cached_property
48
+ def name(self):
49
+ if n := self.action.option_strings:
50
+ # --get-one → get_one
51
+ return re.sub(r"^--?", "", self.action.option_strings[0]).replace("-", "_")
52
+ else:
53
+ raise ValueError(f"Cannot load argparse, due to field {self.action}")
54
+
55
+ def add(self, callback: Callable):
56
+ if self.action.dest == self.name:
57
+ raise NotImplementedError(
58
+ f"Cannot load argparse, due to field {self.action}. It must be visible from CLI and cannot"
59
+ "be read directly from the program. Solution: Do not use argparse or add a .dest parameter."
60
+ )
61
+ self.properties[self.action.dest].add(callback)
62
+
63
+ @property
64
+ def has_property(self):
65
+ return self.action.dest in self.properties
66
+
67
+
68
+ def parser_to_dataclass(parser: ArgumentParser, name: str = "Args") -> tuple[DataClass | list[DataClass], Optional[str]]:
69
+ """
70
+ Note: Ex. parser.add_argument("--time", type=time) -> does work at all in argparse, here it works.
71
+
72
+ Returns:
73
+ DataClass | list[DataClass]
74
+ Optional[str]: add_version flag
75
+ """
76
+ subparsers: list[_SubParsersAction] = []
77
+ add_version = None
78
+
79
+ normal_actions: list[Action] = []
80
+ has_positionals = False
81
+ for action in parser._actions:
82
+ match action:
83
+ case _HelpAction():
84
+ continue
85
+ case _SubParsersAction():
86
+ if has_positionals:
87
+ warn(
88
+ "This CLI parser have a subcommand placed after positional arguments. The order of arguments changes, see --help."
89
+ )
90
+ subparsers.append(action)
91
+ case _VersionAction():
92
+ # We do not want the version to be part of the dataclass (and appear in `m.form()`).
93
+ add_version = action.version
94
+ case _:
95
+ if not action.option_strings:
96
+ has_positionals = True
97
+ normal_actions.append(action)
98
+
99
+ if subparsers:
100
+ return [
101
+ _make_dataclass_from_actions(
102
+ normal_actions + subactions._actions,
103
+ subname,
104
+ help_,
105
+ subactions.description,
106
+ )
107
+ for subparser in subparsers
108
+ for subname, subactions, help_ in _loop_SubParsersAction(subparser)
109
+ ], add_version
110
+ else:
111
+ return _make_dataclass_from_actions(normal_actions, name, None, parser.description), add_version
112
+
113
+
114
+ def _loop_SubParsersAction(subparser: _SubParsersAction):
115
+ return [
116
+ (subname, subactions, ch_act.help)
117
+ for (subname, subactions), ch_act in zip(subparser.choices.items(), subparser._choices_actions)
118
+ ]
119
+
120
+
121
+ def _make_dataclass_from_actions(
122
+ actions: list[Action], name, helptext: str | None, description: str | None
123
+ ) -> DataClass:
124
+ const_actions = defaultdict(list[ArgparseField])
125
+ normal_fields: list[tuple[str, type, Field]] = []
126
+ pos_fields: list[tuple[str, type, Field]] = []
127
+ properties = defaultdict(Property)
128
+ """ Sometimes, the action.dest differs from the field name.
129
+ Field name is exposed to the CLI, action.dest is used in the program.
130
+ """
131
+ subparser_fields: list[tuple[str, type]] = []
132
+
133
+ for action in actions:
134
+ af = ArgparseField(action, properties)
135
+ opt = {}
136
+
137
+ match action:
138
+ case _HelpAction():
139
+ continue
140
+ case _SubParsersAction():
141
+ # Note that there is only one _SubParsersAction in argparse
142
+ # but to be sure, we allow multiple of them
143
+ # This probably makes a different CLI output than the original argparse but should work.
144
+ for subname, subparser, help_ in _loop_SubParsersAction(action):
145
+ sub_dc = _make_dataclass_from_actions(
146
+ subparser._actions,
147
+ subname.capitalize(),
148
+ help_,
149
+ subparser.description,
150
+ )
151
+ subparser_fields.append((subname, sub_dc)) # required, no default
152
+
153
+ from functools import reduce
154
+
155
+ union_type = reduce(lambda a, b: a | b, [aa[1] for aa in subparser_fields])
156
+
157
+ result = OmitSubcommandPrefixes[Positional[union_type]]
158
+ pos_fields.append(("_subparsers", result))
159
+ subparser_fields.clear()
160
+ continue
161
+ case _AppendAction():
162
+ arg_type = list[action.type or str]
163
+ opt["default_factory"] = list
164
+ case _AppendConstAction():
165
+ # `--one --two` -> env.section = [one, two]
166
+ arg_type = bool
167
+ opt["default"] = False
168
+ const_actions[af.action.dest].append(af)
169
+ af.add(
170
+ lambda self, af=af: (
171
+ [_af.action.const for _af in const_actions[af.action.dest] if getattr(self, _af.name)]
172
+ )
173
+ )
174
+ case _StoreTrueAction():
175
+ arg_type = bool
176
+ case _StoreFalseAction():
177
+ arg_type = bool
178
+ opt["default"] = False
179
+ af.add(lambda self, field_name=af.name: not getattr(self, field_name))
180
+ case _StoreConstAction():
181
+ arg_type = bool
182
+ opt["default"] = False
183
+ af.add(
184
+ lambda self, field_name=af.name, const=action.const: (const if getattr(self, field_name) else None)
185
+ )
186
+ case _CountAction():
187
+ arg_type = int
188
+ case _:
189
+ if action.type:
190
+ arg_type = action.type
191
+ elif action.default:
192
+ arg_type = type(action.default)
193
+ else:
194
+ arg_type = str
195
+
196
+ if "default" not in opt and "default_factory" not in opt:
197
+ if action.choices:
198
+ # With the drop of Python 3.10, use mere:
199
+ # arg_type = Literal[*action.choices]
200
+ if sys.version_info >= (3,11):
201
+ from .future_compatibility import literal
202
+ arg_type = literal(action.choices)
203
+ else:
204
+ # we do not prefer this option as tyro does not understand it
205
+ # and won't display options in the help
206
+ arg_type = Annotated[arg_type, Options(*action.choices)]
207
+
208
+ if not action.option_strings and action.default is None and action.nargs != "?":
209
+ opt["default"] = MISSING
210
+ else:
211
+ if action.default is None:
212
+ # parser.add_argument("--path", type=Path) -> becomes None, not Path('.').
213
+ # By default, argparse put None if not used in the CLI.
214
+ # Which makes tyro output the warning: annotated with type `<class 'str'>`, but the default value `None`
215
+ # We either make None an option by `arg_type |= None`
216
+ # or else we default the value.
217
+ if arg_type is not None:
218
+ arg_type |= None
219
+ opt["default"] = action.default if action.default != SUPPRESS else None
220
+
221
+ # build a dataclass field, either optional, or positional
222
+ opt["metadata"] = {"help": action.help}
223
+ if action.option_strings:
224
+ # normal_fields.append((action.dest, arg_type, field(**opt, **met)))
225
+ # Annotated[arg_type, arg(metavar=metavar)]
226
+ normal_fields.append((af.name, arg_type, field(**opt)))
227
+
228
+ # Generate back-compatible property if dest != field_name
229
+ if af.name != action.dest and not af.has_property:
230
+ af.add(lambda self, field_name=af.name: getattr(self, field_name))
231
+ else:
232
+ pos_fields.append((action.dest, Positional[arg_type], field(**opt)))
233
+
234
+ # Subparser can have the same field name as the parser. We use the latter.
235
+ # Ex:
236
+ # parser.add_argument('--level', type=int, default=1)
237
+ # subparsers = parser.add_subparsers(dest='command')
238
+ # run_parser = subparsers.add_parser('run')
239
+ # run_parser.add_argument('--level', type=int, default=5)
240
+ uniq_fields = []
241
+ seen = set()
242
+ # for f in reversed(subparser_fields + pos_fields + normal_fields):
243
+ for f in reversed(pos_fields + normal_fields):
244
+ if f[0] not in seen:
245
+ seen.add(f[0])
246
+ uniq_fields.append(f)
247
+
248
+ # if subparser_fields:
249
+ # from functools import reduce
250
+ # union_type = reduce(lambda a, b: a | b, [aa[1] for aa in subparser_fields])
251
+ # result = OmitSubcommandPrefixes[Positional[union_type]]
252
+ # uniq_fields.append(("_subparsers", result ))
253
+
254
+ dc = make_dataclass(
255
+ name,
256
+ reversed(uniq_fields),
257
+ namespace={k: prop.generate_property() for k, prop in properties.items()},
258
+ )
259
+ if helptext or description:
260
+ trimmed = (helptext or "").strip()
261
+ needs_colon = trimmed and description and trimmed[-1] not in (".", ":", "!", "?", "…")
262
+
263
+ separator = ": " if needs_colon else ("\n" if trimmed else "")
264
+ dc.__doc__ = trimmed + separator + (description or "")
265
+
266
+ return DisallowNone[dc]