argparse-usage 0.1.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,6 @@
1
+ """Generate usage specs from Python argparse.ArgumentParser."""
2
+
3
+ from argparse_usage.generator import generate_usage_spec as generate
4
+
5
+ __all__ = ["generate"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,251 @@
1
+ """Generate usage specs from argparse.ArgumentParser."""
2
+
3
+ import argparse
4
+ from collections.abc import Iterator
5
+ from typing import Any
6
+
7
+ from argparse_usage.kdl_utils import escape_string, format_arg, format_flag
8
+
9
+
10
+ def _get_all_actions(parser: argparse.ArgumentParser) -> Iterator[argparse.Action]:
11
+ """Yield all actions from parser and its parent parsers."""
12
+ # Get parent parsers first
13
+ parents = getattr(parser, "_parents", None)
14
+ if parents:
15
+ for parent in parents:
16
+ yield from _get_all_actions(parent)
17
+
18
+ # Then get actions from this parser
19
+ for action in parser._actions:
20
+ yield action
21
+
22
+
23
+ def _is_positional(action: argparse.Action) -> bool:
24
+ """Check if action is a positional argument."""
25
+ return len(action.option_strings) == 0
26
+
27
+
28
+ def _is_flag(action: argparse.Action) -> bool:
29
+ """Check if action is a flag (optional argument)."""
30
+ return len(action.option_strings) > 0
31
+
32
+
33
+ def _get_flag_names(action: argparse.Action) -> tuple[str | None, str | None]:
34
+ """Extract short and long flag names from an action."""
35
+ short = None
36
+ long = None
37
+
38
+ for opt in action.option_strings:
39
+ if opt.startswith("--"):
40
+ long = opt
41
+ elif opt.startswith("-"):
42
+ short = opt
43
+
44
+ return short, long
45
+
46
+
47
+ def _get_var_info(nargs: Any) -> tuple[bool, int | None, int | None]:
48
+ """Extract variadic information from nargs."""
49
+ if nargs in ("*", "+"):
50
+ return True, 1 if nargs == "+" else 0, None
51
+ elif isinstance(nargs, int) and nargs > 1:
52
+ return True, nargs, nargs
53
+ elif nargs is None:
54
+ return False, None, None
55
+ else:
56
+ return False, None, None
57
+
58
+
59
+ def _convert_action_to_spec(action: argparse.Action) -> str | None:
60
+ """Convert an argparse Action to usage spec KDL."""
61
+ # Skip help action
62
+ if isinstance(action, argparse._HelpAction):
63
+ return None
64
+
65
+ # Skip subparsers action (handled separately)
66
+ if isinstance(action, argparse._SubParsersAction):
67
+ return None
68
+
69
+ if _is_flag(action):
70
+ return _convert_flag_to_spec(action)
71
+ elif _is_positional(action):
72
+ return _convert_positional_to_spec(action)
73
+
74
+ return None
75
+
76
+
77
+ def _convert_flag_to_spec(action: argparse.Action) -> str:
78
+ """Convert a flag action to usage spec KDL."""
79
+ short, long = _get_flag_names(action)
80
+ help_text = getattr(action, "help", None)
81
+ required = getattr(action, "required", False)
82
+ default = getattr(action, "default", None)
83
+ choices = getattr(action, "choices", None)
84
+
85
+ # Determine action type
86
+ is_count = isinstance(action, argparse._CountAction)
87
+ is_store_true = isinstance(action, argparse._StoreTrueAction)
88
+ is_store_false = isinstance(action, argparse._StoreFalseAction)
89
+
90
+ # Get variadic info
91
+ nargs = getattr(action, "nargs", None)
92
+ var, var_min, var_max = _get_var_info(nargs)
93
+
94
+ # For count actions, default is typically 0
95
+ if is_count and default is None:
96
+ default = 0
97
+
98
+ # For store_true/false, default is opposite of what would happen
99
+ if is_store_true and default is None:
100
+ default = False
101
+ if is_store_false and default is None:
102
+ default = True
103
+
104
+ # Handle ellipsis notation for variadic flags
105
+ if var and long:
106
+ if long.endswith("..."):
107
+ long = long[:-3]
108
+ elif nargs in ("*", "+"):
109
+ # Add ellipsis to long form for variadic
110
+ long = f"{long}..."
111
+
112
+ return format_flag(
113
+ short=short,
114
+ long=long,
115
+ help_text=help_text,
116
+ required=required,
117
+ default=default,
118
+ count=is_count,
119
+ var=var,
120
+ var_min=var_min,
121
+ var_max=var_max,
122
+ choices=choices,
123
+ )
124
+
125
+
126
+ def _convert_positional_to_spec(action: argparse.Action) -> str:
127
+ """Convert a positional argument to usage spec KDL."""
128
+ name = action.dest
129
+ help_text = getattr(action, "help", None)
130
+ required = getattr(action, "required", False)
131
+ default = getattr(action, "default", None)
132
+ choices = getattr(action, "choices", None)
133
+
134
+ # Get variadic info
135
+ nargs = getattr(action, "nargs", None)
136
+ var, var_min, var_max = _get_var_info(nargs)
137
+
138
+ # Handle optional positionals (nargs='?')
139
+ if nargs == "?":
140
+ required = False
141
+
142
+ return format_arg(
143
+ name=name,
144
+ help_text=help_text,
145
+ required=required,
146
+ default=default,
147
+ var=var,
148
+ var_min=var_min,
149
+ var_max=var_max,
150
+ choices=choices,
151
+ )
152
+
153
+
154
+ def _format_subcommand(name: str, parser: argparse.ArgumentParser) -> str:
155
+ """Format a subcommand with its spec."""
156
+ lines = [f"cmd {name} {{"]
157
+
158
+ # Add help text if available
159
+ help_text = getattr(parser, "description", None) or getattr(
160
+ parser, "_help_text", None
161
+ )
162
+ if help_text:
163
+ lines.append(f" help={escape_string(help_text)}")
164
+
165
+ # Process all actions (skip help and subparsers)
166
+ for action in parser._actions:
167
+ if isinstance(action, (argparse._HelpAction, argparse._SubParsersAction)):
168
+ continue
169
+
170
+ spec = _convert_action_to_spec(action)
171
+ if spec:
172
+ # Indent each line
173
+ for line in spec.split("\n"):
174
+ lines.append(f" {line}")
175
+
176
+ # Handle sub-subcommands
177
+ for action in parser._actions:
178
+ if isinstance(action, argparse._SubParsersAction):
179
+ for sub_name, sub_parser in action.choices.items():
180
+ lines.append(_format_subcommand(sub_name, sub_parser))
181
+
182
+ lines.append("}")
183
+ return "\n".join(lines)
184
+
185
+
186
+ def generate_usage_spec(
187
+ parser: argparse.ArgumentParser,
188
+ name: str | None = None,
189
+ version: str | None = None,
190
+ author: str | None = None,
191
+ bin_name: str | None = None,
192
+ ) -> str:
193
+ """Generate a usage spec KDL string from an ArgumentParser.
194
+
195
+ Args:
196
+ parser: The ArgumentParser instance to convert.
197
+ name: The friendly name for the CLI (defaults to parser.prog or description).
198
+ version: The version of the CLI.
199
+ author: The author of the CLI.
200
+ bin_name: The binary name (defaults to parser.prog).
201
+
202
+ Returns:
203
+ A KDL-formatted usage spec string.
204
+ """
205
+ lines = []
206
+
207
+ lines.append("// @generated by argparse-usage from Python argparse")
208
+ lines.append("")
209
+
210
+ if name:
211
+ lines.append(f"name {escape_string(name)}")
212
+ elif parser.prog:
213
+ lines.append(f"name {escape_string(parser.prog)}")
214
+ elif parser.description:
215
+ # Try to extract name from description
216
+ import re
217
+
218
+ match = re.match(r"^(\w+)", parser.description)
219
+ if match:
220
+ lines.append(f"name {escape_string(match.group(1))}")
221
+
222
+ bin_value = bin_name or parser.prog or "cli"
223
+ lines.append(f"bin {escape_string(bin_value)}")
224
+
225
+ if version:
226
+ lines.append(f"version {escape_string(version)}")
227
+
228
+ if author:
229
+ lines.append(f"author {escape_string(author)}")
230
+
231
+ if parser.description:
232
+ lines.append(f"about {escape_string(parser.description)}")
233
+
234
+ lines.append("")
235
+
236
+ # Process all actions (skip help and subparsers)
237
+ for action in parser._actions:
238
+ if isinstance(action, (argparse._HelpAction, argparse._SubParsersAction)):
239
+ continue
240
+
241
+ spec = _convert_action_to_spec(action)
242
+ if spec:
243
+ lines.append(spec)
244
+
245
+ # Handle subcommands
246
+ for action in parser._actions:
247
+ if isinstance(action, argparse._SubParsersAction):
248
+ for name, sub_parser in action.choices.items():
249
+ lines.append(_format_subcommand(name, sub_parser))
250
+
251
+ return "\n".join(lines)
@@ -0,0 +1,150 @@
1
+ """KDL formatting utilities for usage spec generation."""
2
+
3
+ import argparse
4
+
5
+
6
+ def escape_string(value: str) -> str:
7
+ """Escape a string value for KDL format."""
8
+ if not value:
9
+ return '""'
10
+
11
+ value = str(value)
12
+
13
+ # If string contains special characters, use raw string format r#"..."#
14
+ needs_raw = any(c in value for c in ['"', "\n", "\r", "\t", "\\"])
15
+
16
+ if needs_raw:
17
+ # Use raw string format to avoid escaping
18
+ return f'r#"{value}"#'
19
+ else:
20
+ return f'"{value}"'
21
+
22
+
23
+ def format_flag(
24
+ short: str | None,
25
+ long: str | None,
26
+ help_text: str | None = None,
27
+ required: bool = False,
28
+ default: str | bool | int | None = None,
29
+ count: bool = False,
30
+ var: bool = False,
31
+ var_min: int | None = None,
32
+ var_max: int | None = None,
33
+ choices: list[str] | None = None,
34
+ long_help: str | None = None,
35
+ ) -> str:
36
+ """Format a flag definition for KDL spec."""
37
+ parts = []
38
+
39
+ # Build flag name (e.g., "-f --force")
40
+ flag_names = []
41
+ if short:
42
+ flag_names.append(short)
43
+ if long:
44
+ flag_names.append(long)
45
+ flag_str = " ".join(flag_names)
46
+
47
+ # Check if we need a block (for complex attributes like choices or long_help)
48
+ needs_block = choices is not None or (long_help and len(long_help) > 50)
49
+
50
+ # Simple attributes (inline)
51
+ attrs = []
52
+ if help_text:
53
+ attrs.append(f"help={escape_string(help_text)}")
54
+ if required:
55
+ attrs.append("required=#true")
56
+ if default is not None and default != argparse.SUPPRESS:
57
+ if isinstance(default, bool):
58
+ attrs.append(f"default=#{str(default).lower()}")
59
+ else:
60
+ attrs.append(f"default={escape_string(str(default))}")
61
+ if count:
62
+ attrs.append("count=#true")
63
+ if var or var_min is not None or var_max is not None:
64
+ attrs.append("var=#true")
65
+ if var_min is not None:
66
+ attrs.append(f"var_min={var_min}")
67
+ if var_max is not None:
68
+ attrs.append(f"var_max={var_max}")
69
+
70
+ if needs_block:
71
+ parts.append(f"flag {escape_string(flag_str)} {{")
72
+ for attr in attrs:
73
+ parts.append(f" {attr}")
74
+ if long_help:
75
+ parts.append(f" long_help={escape_string(long_help)}")
76
+ if choices:
77
+ arg_name = long.lstrip("-") if long else "value"
78
+ parts.append(f' arg "<{arg_name}>" {{')
79
+ parts.append(f" choices {' '.join(escape_string(c) for c in choices)}")
80
+ parts.append(" }")
81
+ parts.append("}")
82
+ else:
83
+ line = f"flag {escape_string(flag_str)}"
84
+ if attrs:
85
+ line += " " + " ".join(attrs)
86
+ parts.append(line)
87
+
88
+ return "\n".join(parts)
89
+
90
+
91
+ def format_arg(
92
+ name: str,
93
+ help_text: str | None = None,
94
+ required: bool = True,
95
+ default: str | None = None,
96
+ var: bool = False,
97
+ var_min: int | None = None,
98
+ var_max: int | None = None,
99
+ choices: list[str] | None = None,
100
+ long_help: str | None = None,
101
+ ) -> str:
102
+ """Format an argument definition for KDL spec."""
103
+ parts = []
104
+
105
+ # Build arg name (e.g., "<file>" or "[file]" or "<file>..." or "[file]...")
106
+ if not var and not var_min and not var_max:
107
+ arg_name = f"[{name}]" if not required else f"<{name}>"
108
+ else:
109
+ # Variadic argument
110
+ if var_min == 0:
111
+ # Zero or more -> optional
112
+ arg_name = f"[{name}]..."
113
+ else:
114
+ # One or more -> required
115
+ arg_name = f"<{name}>..."
116
+
117
+ # Check if we need a block (for complex attributes like choices or long_help)
118
+ needs_block = choices is not None or (long_help and len(long_help) > 50)
119
+
120
+ # Simple attributes (inline)
121
+ attrs = []
122
+ if help_text:
123
+ attrs.append(f"help={escape_string(help_text)}")
124
+ if not required:
125
+ attrs.append("required=#false")
126
+ if default is not None and default != argparse.SUPPRESS:
127
+ attrs.append(f"default={escape_string(str(default))}")
128
+ if var:
129
+ attrs.append("var=#true")
130
+ if var_min is not None:
131
+ attrs.append(f"var_min={var_min}")
132
+ if var_max is not None:
133
+ attrs.append(f"var_max={var_max}")
134
+
135
+ if needs_block:
136
+ parts.append(f"arg {escape_string(arg_name)} {{")
137
+ for attr in attrs:
138
+ parts.append(f" {attr}")
139
+ if long_help:
140
+ parts.append(f" long_help={escape_string(long_help)}")
141
+ if choices:
142
+ parts.append(f" choices {' '.join(escape_string(c) for c in choices)}")
143
+ parts.append("}")
144
+ else:
145
+ line = f"arg {escape_string(arg_name)}"
146
+ if attrs:
147
+ line += " " + " ".join(attrs)
148
+ parts.append(line)
149
+
150
+ return "\n".join(parts)
File without changes
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.3
2
+ Name: argparse-usage
3
+ Version: 0.1.0
4
+ Summary: Generate usage spec KDL files from Python argparse.ArgumentParser
5
+ Author: acidghost
6
+ Author-email: acidghost <1787979+acidghost@users.noreply.github.com>
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+
10
+ # argparse-usage
11
+
12
+ Generate [usage](https://usage.jdx.dev/) KDL files from Python's `argparse.ArgumentParser`.
13
+
14
+ This library converts Python `argparse` definitions to the [usage spec format](https://usage.jdx.dev/spec/reference/), enabling automatic generation of:
15
+
16
+ - Markdown documentation
17
+ - Manpages
18
+ - Shell completions (bash, fish, zsh, powershell, nu)
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install argparse-usage
24
+ # or
25
+ uv add argparse-usage
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ import argparse
32
+ import argparse_usage
33
+
34
+ # Create your ArgumentParser as usual
35
+ parser = argparse.ArgumentParser(
36
+ prog='mycli',
37
+ description='My CLI tool',
38
+ )
39
+
40
+ parser.add_argument('-v', '--verbose', action='count', default=0, help='Increase verbosity')
41
+ parser.add_argument('-f', '--force', action='store_true', help='Force operation')
42
+ parser.add_argument('files', nargs='+', help='Files to process')
43
+
44
+ # Add subcommands
45
+ subparsers = parser.add_subparsers(dest='command')
46
+ build_cmd = subparsers.add_parser('build', help='Build project')
47
+ test_cmd = subparsers.add_parser('test', help='Run tests')
48
+
49
+ # Generate usage spec
50
+ spec = argparse_usage.generate(
51
+ parser,
52
+ name='My CLI',
53
+ version='1.0.0',
54
+ author='Your Name',
55
+ )
56
+
57
+ print(spec)
58
+ ```
59
+
60
+ Output:
61
+
62
+ ```kdl
63
+ // @generated by argparse-usage from Python argparse
64
+
65
+ name "My CLI"
66
+ bin "mycli"
67
+ version "1.0.0"
68
+ author "Your Name"
69
+ about "My CLI tool"
70
+
71
+ arg "<files>..." help="Files to process" var=#true var_min=1
72
+
73
+ flag "-v --verbose" help="Increase verbosity" default="0" count=#true
74
+ flag "-f --force" help="Force operation" default=#false
75
+
76
+ cmd build {
77
+ // options
78
+ }
79
+
80
+ cmd test {
81
+ // options
82
+ }
83
+ ```
84
+
85
+ ## Generating Documentation
86
+
87
+ Save the spec and use the `usage` CLI to generate documentation:
88
+
89
+ ```bash
90
+ # Save spec to file
91
+ python mycli.py --usage-spec > mycli.usage.kdl
92
+
93
+ # Generate markdown documentation
94
+ usage generate markdown --file mycli.usage.kdl --out-file README.md
95
+
96
+ # Generate shell completions
97
+ usage generate completion bash mycli --file mycli.usage.kdl > mycli-completion.bash
98
+ ```
99
+
100
+ ## Supported Features
101
+
102
+ ### Flags (optional arguments)
103
+
104
+ | argparse action | usage spec mapping |
105
+ | ---------------------- | -------------------------------------------------------------- |
106
+ | `action='store_true'` | `flag "--force" default=#false` |
107
+ | `action='store_false'` | `flag "--quiet" default=#true` |
108
+ | `action='count'` | `flag "-v --verbose" count=#true` |
109
+ | `nargs='*'` | `flag "--files..." var=#true` |
110
+ | `nargs='+'` | `flag "--tags..." var=#true var_min=1` |
111
+ | `choices=[...]` | `flag "--format" { arg "<format>" { choices "json" "yaml" } }` |
112
+
113
+ ### Arguments (positional)
114
+
115
+ | argparse nargs | usage spec mapping |
116
+ | ---------------------- | --------------------------------------- |
117
+ | `nargs=None` (default) | `arg "<file>"` |
118
+ | `nargs='?'` | `arg "[file]"` |
119
+ | `nargs='*'` | `arg "[files]..." var=#true` |
120
+ | `nargs='+'` | `arg "<files>..." var=#true var_min=1` |
121
+ | `nargs=N` | `arg "<coords>..." var_min=N var_max=N` |
122
+
123
+ ### Parent Parsers
124
+
125
+ Inheritance from parent parsers is supported:
126
+
127
+ ```python
128
+ parent_parser = argparse.ArgumentParser(add_help=False)
129
+ parent_parser.add_argument('-v', '--verbose', action='count')
130
+
131
+ parser = argparse.ArgumentParser(parents=[parent_parser])
132
+ ```
133
+
134
+ ### Subcommands
135
+
136
+ Nested subcommands are fully supported:
137
+
138
+ ```python
139
+ subparsers = parser.add_subparsers()
140
+ add_cmd = subparsers.add_parser('add')
141
+ add_cmd.add_argument('name')
142
+ ```
143
+
144
+ Output:
145
+
146
+ ```kdl
147
+ cmd add {
148
+ arg "<name>"
149
+ }
150
+ ```
151
+
152
+ ## API Reference
153
+
154
+ ### `generate(parser, name=None, version=None, author=None, bin_name=None)`
155
+
156
+ Generate a usage spec KDL string from an ArgumentParser.
157
+
158
+ **Parameters:**
159
+
160
+ - `parser` (ArgumentParser): The ArgumentParser instance to convert
161
+ - `name` (str, optional): Friendly name for the CLI (defaults to parser.prog)
162
+ - `version` (str, optional): Version of the CLI
163
+ - `author` (str, optional): Author of the CLI
164
+ - `bin_name` (str, optional): Binary name (defaults to parser.prog)
165
+
166
+ **Returns:**
167
+
168
+ - `str`: KDL-formatted usage spec string
169
+
170
+ ## Examples
171
+
172
+ See the [examples/](examples/) directory for complete examples:
173
+
174
+ - [basic_usage.py](examples/basic_usage.py) - Basic usage with flags, args, and subcommands
175
+
176
+ ## Contributing
177
+
178
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,7 @@
1
+ argparse_usage/__init__.py,sha256=reBHhbeJMGSXH6e01vl4CneZM6FtP_Gs3g51B-hWXHA,180
2
+ argparse_usage/generator.py,sha256=Ofr0M0P2ImcejvsWra52n-FJ0XsnXee9L3ojZ9LPGIk,7560
3
+ argparse_usage/kdl_utils.py,sha256=_n91qL_sNdGA8GcT8rAJq2t_dLbucJ2mC4EKh079H6o,4685
4
+ argparse_usage/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ argparse_usage-0.1.0.dist-info/WHEEL,sha256=mydTeHxOpFHo-DnYhAd_3ATePms-g4rrYvM7wJK8P-U,80
6
+ argparse_usage-0.1.0.dist-info/METADATA,sha256=N8yeQQ8a8tYZ7HpkK9vIQjSjGoYYpzdcF5b6_36Q2hk,4867
7
+ argparse_usage-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.10.9
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any