usage-spec-argparse 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,39 @@
1
+ """Usage spec integration for argparse."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from usage_spec import Spec, SpecArg, SpecFlag, SpecCommand, SpecChoices, render_kdl, validate_kdl, render_json, generate as core_generate
8
+
9
+ from .convert import convert_root
10
+
11
+ __all__ = [
12
+ "convert_root",
13
+ "generate",
14
+ "generate_kdl",
15
+ "generate_json",
16
+ "render_kdl",
17
+ "Spec",
18
+ "SpecArg",
19
+ "SpecFlag",
20
+ "SpecCommand",
21
+ "SpecChoices",
22
+ ]
23
+
24
+
25
+ def generate(parser: argparse.ArgumentParser, bin_name: str | None = None) -> str:
26
+ """Generate usage spec in KDL format from an argparse.ArgumentParser."""
27
+ spec = convert_root(parser, bin_name)
28
+ return core_generate(spec, format="kdl", comment="@generated by usage-spec-argparse from argparse metadata")
29
+
30
+
31
+ def generate_kdl(parser: argparse.ArgumentParser, bin_name: str | None = None) -> str:
32
+ """Generate usage spec in KDL format (alias for generate)."""
33
+ return generate(parser, bin_name)
34
+
35
+
36
+ def generate_json(parser: argparse.ArgumentParser, bin_name: str | None = None) -> str:
37
+ """Generate usage spec in JSON format from an argparse.ArgumentParser."""
38
+ spec = convert_root(parser, bin_name)
39
+ return core_generate(spec, format="json")
@@ -0,0 +1,226 @@
1
+ """Convert argparse.ArgumentParser to usage spec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from typing import Any
7
+
8
+ from usage_spec import Spec, SpecArg, SpecFlag, SpecCommand, SpecChoices
9
+
10
+ # Action class names that represent boolean flags
11
+ _BOOL_ACTION_NAMES = frozenset({
12
+ "_StoreTrueAction",
13
+ "_StoreFalseAction",
14
+ })
15
+
16
+ # Action class names that represent count flags
17
+ _COUNT_ACTION_NAMES = frozenset({
18
+ "_CountAction",
19
+ })
20
+
21
+ # Action class names for subparsers
22
+ _SUBPARSERS_ACTION_NAMES = frozenset({
23
+ "_SubParsersAction",
24
+ "SubParsersAction",
25
+ })
26
+
27
+ # Built-in flag names to skip
28
+ _BUILTIN_FLAG_NAMES = frozenset({"help", "version"})
29
+
30
+
31
+ def _is_bool_action(action: argparse.Action) -> bool:
32
+ return type(action).__name__ in _BOOL_ACTION_NAMES
33
+
34
+
35
+ def _is_count_action(action: argparse.Action) -> bool:
36
+ return type(action).__name__ in _COUNT_ACTION_NAMES
37
+
38
+
39
+ def _is_subparsers_action(action: argparse.Action) -> bool:
40
+ return type(action).__name__ in _SUBPARSERS_ACTION_NAMES
41
+
42
+
43
+ def _is_optional(action: argparse.Action) -> bool:
44
+ return bool(action.option_strings)
45
+
46
+
47
+ def _extract_short_long(option_strings: list[str]) -> tuple[str, str]:
48
+ short = ""
49
+ long = ""
50
+ for opt in option_strings:
51
+ if opt.startswith("--"):
52
+ long = opt[2:]
53
+ elif opt.startswith("-"):
54
+ short = opt[1:]
55
+ return short, long
56
+
57
+
58
+ def _convert_arg(action: argparse.Action) -> SpecArg:
59
+ nargs = action.nargs
60
+ # Use argparse's own 'required' attribute for positional args
61
+ required = getattr(action, "required", True)
62
+ # Variadic: nargs is '*' or '+' or argparse.REMAINDER
63
+ variadic = nargs in ("*", "+", argparse.REMAINDER) if nargs else False
64
+
65
+ result = SpecArg(
66
+ name=action.dest or action.metavar or "",
67
+ help=action.help if action.help != argparse.SUPPRESS else "",
68
+ required=required,
69
+ var=variadic,
70
+ hide=action.help == argparse.SUPPRESS,
71
+ default=[str(action.default)] if action.default not in (None, argparse.SUPPRESS) else [],
72
+ choices=None,
73
+ )
74
+
75
+ if action.choices:
76
+ result.choices = SpecChoices(values=[str(c) for c in action.choices])
77
+
78
+ return result
79
+
80
+
81
+ def _convert_flag(action: argparse.Action) -> SpecFlag:
82
+ short, long = _extract_short_long(action.option_strings)
83
+ is_bool = _is_bool_action(action)
84
+ is_count = _is_count_action(action)
85
+ is_store_false = type(action).__name__ == "_StoreFalseAction"
86
+
87
+ flag = SpecFlag(
88
+ short=short,
89
+ long=long,
90
+ help=action.help if action.help != argparse.SUPPRESS else "",
91
+ help_long="",
92
+ required=bool(action.required) if hasattr(action, "required") else False,
93
+ hide=action.help == argparse.SUPPRESS,
94
+ global_=False,
95
+ count=is_count,
96
+ var=bool(action.nargs and action.nargs in ("*", "+")),
97
+ negate=f"--{long}" if is_store_false else "",
98
+ deprecated="",
99
+ default=[],
100
+ default_bool=None,
101
+ env="",
102
+ arg=None,
103
+ )
104
+
105
+ # Non-boolean, non-count options have an argument
106
+ if not is_bool and not is_count:
107
+ arg_name = (long or short).replace("-", "_").upper()
108
+ flag.arg = SpecArg(
109
+ name=arg_name,
110
+ help="",
111
+ required=flag.required,
112
+ var=flag.var,
113
+ hide=False,
114
+ default=[],
115
+ choices=None,
116
+ )
117
+
118
+ if action.choices:
119
+ flag.arg.choices = SpecChoices(values=[str(c) for c in action.choices])
120
+
121
+ # Default values
122
+ if action.default not in (None, argparse.SUPPRESS, True, False):
123
+ if is_bool or is_count:
124
+ pass # skip for booleans
125
+ else:
126
+ flag.default = [str(action.default)]
127
+ elif action.default is True:
128
+ flag.default_bool = True
129
+ # False defaults are omitted for booleans
130
+
131
+ return flag
132
+
133
+
134
+ def _get_subcommand_helps(parser: argparse.ArgumentParser) -> dict[str, str]:
135
+ """Extract help texts from subparsers' _choices_actions."""
136
+ helps: dict[str, str] = {}
137
+ for action in parser._actions:
138
+ if _is_subparsers_action(action) and hasattr(action, "_choices_actions"):
139
+ for ca in action._choices_actions:
140
+ if ca.dest and ca.help:
141
+ helps[ca.dest] = ca.help
142
+ return helps
143
+
144
+
145
+ def _convert_command(parser: argparse.ArgumentParser, help_override: str = "") -> SpecCommand:
146
+ name = parser.prog
147
+ # Strip parent prefix from name (e.g. "app sub" -> "sub")
148
+ if " " in name:
149
+ name = name.rsplit(" ", 1)[-1]
150
+
151
+ # Use help_override from _choices_actions if available, otherwise description
152
+ cmd_help = help_override or parser.description or ""
153
+
154
+ sc = SpecCommand(
155
+ name=name,
156
+ help=cmd_help,
157
+ help_long="",
158
+ hide=False,
159
+ deprecated="",
160
+ aliases=[],
161
+ subcommand_required=False,
162
+ flags=[],
163
+ args=[],
164
+ cmds=[],
165
+ )
166
+
167
+ sub_helps = _get_subcommand_helps(parser)
168
+
169
+ for action in parser._actions:
170
+ if _is_optional(action):
171
+ action_name = _extract_short_long(action.option_strings)[1] or _extract_short_long(action.option_strings)[0]
172
+ if action_name in _BUILTIN_FLAG_NAMES:
173
+ continue
174
+ sc.flags.append(_convert_flag(action))
175
+ elif _is_subparsers_action(action):
176
+ if hasattr(action, "choices") and action.choices:
177
+ for sub_name, sub_parser in action.choices.items():
178
+ sc.cmds.append(_convert_command(sub_parser, sub_helps.get(sub_name, "")))
179
+ else:
180
+ sc.args.append(_convert_arg(action))
181
+
182
+ # subcommand_required: has subcommands but no positional args
183
+ if sc.cmds and not sc.args:
184
+ sc.subcommand_required = True
185
+
186
+ return sc
187
+
188
+
189
+ def convert_root(parser: argparse.ArgumentParser, bin_name: str | None = None) -> Spec:
190
+ """Convert an argparse.ArgumentParser to a Spec object."""
191
+ name = bin_name or parser.prog
192
+
193
+ spec = Spec(
194
+ name=name,
195
+ bin=name,
196
+ version="",
197
+ about=parser.description or "",
198
+ long=parser.epilog or "",
199
+ usage=parser.usage or "",
200
+ flags=[],
201
+ args=[],
202
+ cmds=[],
203
+ )
204
+
205
+ sub_helps = _get_subcommand_helps(parser)
206
+
207
+ for action in parser._actions:
208
+ if _is_optional(action):
209
+ action_name = _extract_short_long(action.option_strings)[1] or _extract_short_long(action.option_strings)[0]
210
+ # Extract version string before skipping
211
+ if action_name == "version" and hasattr(action, "version"):
212
+ spec.version = action.version or ""
213
+ continue
214
+ # Skip built-in flags
215
+ if action_name in _BUILTIN_FLAG_NAMES:
216
+ continue
217
+ spec.flags.append(_convert_flag(action))
218
+ elif _is_subparsers_action(action):
219
+ if hasattr(action, "choices") and action.choices:
220
+ for sub_name, sub_parser in action.choices.items():
221
+ spec.cmds.append(_convert_command(sub_parser, sub_helps.get(sub_name, "")))
222
+ else:
223
+ # Positional argument
224
+ spec.args.append(_convert_arg(action))
225
+
226
+ return spec
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: usage-spec-argparse
3
+ Version: 1.0.0
4
+ Summary: Generates usage spec for CLIs written with argparse
5
+ License-Expression: MIT
6
+ Keywords: argparse,cli,kdl,shell-completion,usage
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: Topic :: Utilities
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: usage-spec
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8.0; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # usage-spec-argparse
22
+
23
+ Generates [usage spec](https://usage.jdx.dev) for CLIs written with [argparse](https://docs.python.org/3/library/argparse.html).
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ pip install usage-spec-argparse
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ import argparse
35
+ from argparse_usage import generate
36
+
37
+ parser = argparse.ArgumentParser(prog="mycli", description="My CLI tool")
38
+ parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
39
+ parser.add_argument("-f", "--file", help="Input file")
40
+ parser.add_argument("--no-color", action="store_false", help="Disable colored output")
41
+
42
+ print(generate(parser))
43
+ ```
44
+
45
+ ## API
46
+
47
+ ### `generate(parser, bin_name=None)`
48
+
49
+ Generates a usage spec in KDL format from an `argparse.ArgumentParser`.
50
+
51
+ ### `generate_kdl(parser, bin_name=None)`
52
+
53
+ Alias for `generate()`.
54
+
55
+ ### `generate_json(parser, bin_name=None)`
56
+
57
+ Generates a usage spec in JSON format.
58
+
59
+ ### `convert_root(parser, bin_name=None)`
60
+
61
+ Converts an `argparse.ArgumentParser` to the `Spec` data structure.
62
+
63
+ ## Supported Features
64
+
65
+ | argparse Feature | Usage Spec Mapping |
66
+ |---|---|
67
+ | `prog` | `name` / `bin` |
68
+ | `--version` action | `version` |
69
+ | `description` | `about` |
70
+ | `epilog` | `long_about` |
71
+ | `add_argument()` (optional) | `flag` |
72
+ | `add_argument()` (positional) | `arg` |
73
+ | `required=True` | `flag required=#true` |
74
+ | `action="store_true"/"store_false"` | Boolean flag (no arg) |
75
+ | `action="store_false"` | `negate` |
76
+ | `action="count"` | `count=#true` |
77
+ | `nargs="?"` | `required=#false` |
78
+ | `nargs="*"/"+"` | `var=#true` |
79
+ | `choices=[...]` | `choices` |
80
+ | `default=...` | `default` |
81
+ | `help=SUPPRESS` | `hide=#true` |
82
+ | `add_subparsers()` | `cmd` (recursive) |
83
+ | Non-runnable subcommand groups | `subcommand_required=#true` |
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,5 @@
1
+ argparse_usage/__init__.py,sha256=CRyyK06_Wl59WfyeSVBiTCCcRhtrAqOY3SXhBhbVZZE,1228
2
+ argparse_usage/convert.py,sha256=qQZ1-V5uZ8dq1p7GGa4sG1evAknQU0QAtqOcyu_4tSg,7178
3
+ usage_spec_argparse-1.0.0.dist-info/METADATA,sha256=5mnohy3BmfNQgOiUxP3_Ls_2BP2WNZD9YUr9mrW3pkU,2512
4
+ usage_spec_argparse-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ usage_spec_argparse-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any