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,,
|