usage-spec-click 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.
click_usage/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Usage spec integration for click."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
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(cmd: click.BaseCommand, bin_name: str | None = None) -> str:
|
|
26
|
+
"""Generate usage spec in KDL format from a click Command/Group."""
|
|
27
|
+
spec = convert_root(cmd, bin_name)
|
|
28
|
+
return core_generate(spec, format="kdl", comment="@generated by usage-spec-click from click metadata")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_kdl(cmd: click.BaseCommand, bin_name: str | None = None) -> str:
|
|
32
|
+
"""Generate usage spec in KDL format (alias for generate)."""
|
|
33
|
+
return generate(cmd, bin_name)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_json(cmd: click.BaseCommand, bin_name: str | None = None) -> str:
|
|
37
|
+
"""Generate usage spec in JSON format from a click Command/Group."""
|
|
38
|
+
spec = convert_root(cmd, bin_name)
|
|
39
|
+
return core_generate(spec, format="json")
|
click_usage/convert.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Convert click commands to usage spec."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from usage_spec import Spec, SpecArg, SpecFlag, SpecCommand, SpecChoices
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _convert_arg(arg: click.Argument) -> SpecArg:
|
|
12
|
+
variadic = arg.nargs == -1
|
|
13
|
+
required = arg.required
|
|
14
|
+
|
|
15
|
+
from enum import Enum
|
|
16
|
+
_has_real_default = arg.default is not None and not isinstance(arg.default, Enum)
|
|
17
|
+
|
|
18
|
+
result = SpecArg(
|
|
19
|
+
name=arg.name or arg.human_readable_name.lower(),
|
|
20
|
+
help=getattr(arg, "help", "") or "",
|
|
21
|
+
required=required,
|
|
22
|
+
var=variadic,
|
|
23
|
+
hide=getattr(arg, "hidden", False),
|
|
24
|
+
default=[str(arg.default)] if _has_real_default else [],
|
|
25
|
+
choices=None,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if isinstance(arg.type, click.Choice):
|
|
29
|
+
result.choices = SpecChoices(values=list(arg.type.choices))
|
|
30
|
+
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_short_long(opts: list[str]) -> tuple[str, str]:
|
|
35
|
+
short = ""
|
|
36
|
+
long = ""
|
|
37
|
+
for opt in opts:
|
|
38
|
+
if opt.startswith("--"):
|
|
39
|
+
long = opt[2:]
|
|
40
|
+
elif opt.startswith("-"):
|
|
41
|
+
short = opt[1:]
|
|
42
|
+
return short, long
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _convert_flag(opt: click.Option) -> SpecFlag:
|
|
46
|
+
short, long = _extract_short_long(opt.opts)
|
|
47
|
+
|
|
48
|
+
is_bool = opt.is_bool_flag or (opt.is_flag and not opt.multiple and not opt.count)
|
|
49
|
+
|
|
50
|
+
# Detect negation from secondary opts (--flag/--no-flag pattern)
|
|
51
|
+
negate = ""
|
|
52
|
+
if opt.secondary_opts:
|
|
53
|
+
for sec in opt.secondary_opts:
|
|
54
|
+
if sec.startswith("--no-"):
|
|
55
|
+
# negate points to the negative form itself
|
|
56
|
+
negate = sec
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
flag = SpecFlag(
|
|
60
|
+
short=short,
|
|
61
|
+
long=long,
|
|
62
|
+
help=opt.help or "",
|
|
63
|
+
help_long="",
|
|
64
|
+
required=opt.required,
|
|
65
|
+
hide=opt.hidden,
|
|
66
|
+
global_=False,
|
|
67
|
+
count=opt.count,
|
|
68
|
+
var=opt.multiple,
|
|
69
|
+
negate=negate,
|
|
70
|
+
deprecated=str(opt.deprecated) if isinstance(opt.deprecated, str) else "",
|
|
71
|
+
default=[],
|
|
72
|
+
default_bool=None,
|
|
73
|
+
env=str(opt.envvar) if isinstance(opt.envvar, str) else (opt.envvar[0] if opt.envvar and isinstance(opt.envvar, (list, tuple)) else ""),
|
|
74
|
+
arg=None,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Non-boolean options have an argument
|
|
78
|
+
if not is_bool and not opt.count:
|
|
79
|
+
arg_name = (long or short).replace("-", "_").upper()
|
|
80
|
+
flag.arg = SpecArg(
|
|
81
|
+
name=arg_name,
|
|
82
|
+
help="",
|
|
83
|
+
required=opt.required,
|
|
84
|
+
var=opt.multiple,
|
|
85
|
+
hide=False,
|
|
86
|
+
default=[],
|
|
87
|
+
choices=None,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if isinstance(opt.type, click.Choice):
|
|
91
|
+
flag.arg.choices = SpecChoices(values=list(opt.type.choices))
|
|
92
|
+
|
|
93
|
+
# Default values - click uses Sentinel enum for unset defaults
|
|
94
|
+
from enum import Enum
|
|
95
|
+
_has_real_default = opt.default is not None and not isinstance(opt.default, Enum)
|
|
96
|
+
|
|
97
|
+
if _has_real_default and not (is_bool and opt.default is False):
|
|
98
|
+
if is_bool or opt.count:
|
|
99
|
+
if opt.default is True:
|
|
100
|
+
flag.default_bool = True
|
|
101
|
+
else:
|
|
102
|
+
flag.default = [str(opt.default)]
|
|
103
|
+
|
|
104
|
+
return flag
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _convert_command(cmd: click.BaseCommand) -> SpecCommand:
|
|
108
|
+
is_group = isinstance(cmd, click.Group)
|
|
109
|
+
|
|
110
|
+
sc = SpecCommand(
|
|
111
|
+
name=cmd.name or "",
|
|
112
|
+
help=cmd.short_help or cmd.help or "",
|
|
113
|
+
help_long=cmd.short_help and cmd.help and cmd.help or "",
|
|
114
|
+
hide=cmd.hidden,
|
|
115
|
+
deprecated=str(cmd.deprecated) if isinstance(cmd.deprecated, str) else "",
|
|
116
|
+
aliases=[],
|
|
117
|
+
subcommand_required=False,
|
|
118
|
+
flags=[],
|
|
119
|
+
args=[],
|
|
120
|
+
cmds=[],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for param in cmd.params:
|
|
124
|
+
if isinstance(param, click.Option):
|
|
125
|
+
# Skip help and version flags
|
|
126
|
+
name = param.name or ""
|
|
127
|
+
if name in ("help", "version"):
|
|
128
|
+
continue
|
|
129
|
+
sc.flags.append(_convert_flag(param))
|
|
130
|
+
elif isinstance(param, click.Argument):
|
|
131
|
+
sc.args.append(_convert_arg(param))
|
|
132
|
+
|
|
133
|
+
# Subcommands for Groups
|
|
134
|
+
if is_group:
|
|
135
|
+
group = cmd # type: click.Group
|
|
136
|
+
# Try eager commands dict first, then lazy loading
|
|
137
|
+
if group.commands:
|
|
138
|
+
for sub_name, sub_cmd in group.commands.items():
|
|
139
|
+
if sub_cmd.hidden:
|
|
140
|
+
continue
|
|
141
|
+
sc.cmds.append(_convert_command(sub_cmd))
|
|
142
|
+
else:
|
|
143
|
+
# Lazy loading via list_commands + get_command
|
|
144
|
+
ctx = click.Context(cmd)
|
|
145
|
+
for sub_name in group.list_commands(ctx):
|
|
146
|
+
sub_cmd = group.get_command(ctx, sub_name)
|
|
147
|
+
if sub_cmd and not sub_cmd.hidden:
|
|
148
|
+
sc.cmds.append(_convert_command(sub_cmd))
|
|
149
|
+
|
|
150
|
+
# subcommand_required: group with no positional args
|
|
151
|
+
if sc.cmds and not sc.args:
|
|
152
|
+
sc.subcommand_required = True
|
|
153
|
+
|
|
154
|
+
return sc
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _extract_version_from_callback(opt: click.Option) -> str:
|
|
158
|
+
"""Extract version string from click.version_option callback closure."""
|
|
159
|
+
cb = opt.callback
|
|
160
|
+
if not cb or not cb.__closure__:
|
|
161
|
+
return ""
|
|
162
|
+
for cell in cb.__closure__:
|
|
163
|
+
val = cell.cell_contents
|
|
164
|
+
if isinstance(val, str) and val and val[0].isdigit():
|
|
165
|
+
return val
|
|
166
|
+
return ""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def convert_root(cmd: click.BaseCommand, bin_name: str | None = None) -> Spec:
|
|
170
|
+
"""Convert a click Command or Group to a Spec object."""
|
|
171
|
+
name = bin_name or cmd.name or ""
|
|
172
|
+
|
|
173
|
+
spec = Spec(
|
|
174
|
+
name=name,
|
|
175
|
+
bin=name,
|
|
176
|
+
version="",
|
|
177
|
+
about=cmd.short_help or cmd.help or "",
|
|
178
|
+
long=cmd.short_help and cmd.help and cmd.help or "",
|
|
179
|
+
usage="",
|
|
180
|
+
flags=[],
|
|
181
|
+
args=[],
|
|
182
|
+
cmds=[],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
for param in cmd.params:
|
|
186
|
+
if isinstance(param, click.Option):
|
|
187
|
+
pname = param.name or ""
|
|
188
|
+
# Version flag detection
|
|
189
|
+
if pname == "version":
|
|
190
|
+
version = _extract_version_from_callback(param)
|
|
191
|
+
if version:
|
|
192
|
+
spec.version = version
|
|
193
|
+
continue
|
|
194
|
+
if pname == "help":
|
|
195
|
+
continue
|
|
196
|
+
spec.flags.append(_convert_flag(param))
|
|
197
|
+
elif isinstance(param, click.Argument):
|
|
198
|
+
spec.args.append(_convert_arg(param))
|
|
199
|
+
|
|
200
|
+
# Subcommands
|
|
201
|
+
is_group = isinstance(cmd, click.Group)
|
|
202
|
+
if is_group:
|
|
203
|
+
group = cmd # type: click.Group
|
|
204
|
+
if group.commands:
|
|
205
|
+
for sub_name, sub_cmd in group.commands.items():
|
|
206
|
+
if sub_cmd.hidden:
|
|
207
|
+
continue
|
|
208
|
+
spec.cmds.append(_convert_command(sub_cmd))
|
|
209
|
+
else:
|
|
210
|
+
ctx = click.Context(cmd)
|
|
211
|
+
for sub_name in group.list_commands(ctx):
|
|
212
|
+
sub_cmd = group.get_command(ctx, sub_name)
|
|
213
|
+
if sub_cmd and not sub_cmd.hidden:
|
|
214
|
+
spec.cmds.append(_convert_command(sub_cmd))
|
|
215
|
+
|
|
216
|
+
return spec
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: usage-spec-click
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Generates usage spec for CLIs written with click
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: cli,click,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: click>=8.0
|
|
17
|
+
Requires-Dist: usage-spec
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: click>=8.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# usage-spec-click
|
|
24
|
+
|
|
25
|
+
Generates [usage spec](https://usage.jdx.dev) for CLIs written with [click](https://click.palletsprojects.com/).
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install usage-spec-click
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import click
|
|
37
|
+
from click_usage import generate
|
|
38
|
+
|
|
39
|
+
@click.command()
|
|
40
|
+
@click.version_option("1.0.0")
|
|
41
|
+
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output")
|
|
42
|
+
@click.option("-f", "--file", type=str, help="Input file")
|
|
43
|
+
@click.option("--color/--no-color", default=False, help="Color output")
|
|
44
|
+
def cli(verbose, file, color):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
print(generate(cli, "mycli"))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### `generate(cmd, bin_name=None)`
|
|
53
|
+
|
|
54
|
+
Generates a usage spec in KDL format from a click `Command` or `Group`.
|
|
55
|
+
|
|
56
|
+
### `generate_kdl(cmd, bin_name=None)`
|
|
57
|
+
|
|
58
|
+
Alias for `generate()`.
|
|
59
|
+
|
|
60
|
+
### `generate_json(cmd, bin_name=None)`
|
|
61
|
+
|
|
62
|
+
Generates a usage spec in JSON format.
|
|
63
|
+
|
|
64
|
+
### `convert_root(cmd, bin_name=None)`
|
|
65
|
+
|
|
66
|
+
Converts a click `Command` or `Group` to the `Spec` data structure.
|
|
67
|
+
|
|
68
|
+
## Supported Features
|
|
69
|
+
|
|
70
|
+
| click Feature | Usage Spec Mapping |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `cmd.name` | `name` / `bin` |
|
|
73
|
+
| `@click.version_option()` | `version` |
|
|
74
|
+
| `cmd.short_help` / `cmd.help` | `about` / `long_about` |
|
|
75
|
+
| `@click.option()` | `flag` |
|
|
76
|
+
| `required=True` | `flag required=#true` |
|
|
77
|
+
| `is_flag=True` | Boolean flag (no arg) |
|
|
78
|
+
| `--flag/--no-flag` | `negate` |
|
|
79
|
+
| `count=True` | `count=#true` |
|
|
80
|
+
| `multiple=True` | `var=#true` |
|
|
81
|
+
| `type=click.Choice()` | `choices` |
|
|
82
|
+
| `default=...` | `default` |
|
|
83
|
+
| `hidden=True` | `hide=#true` |
|
|
84
|
+
| `deprecated="..."` | `deprecated` |
|
|
85
|
+
| `envvar="..."` | `env` |
|
|
86
|
+
| `@click.argument()` | `arg` |
|
|
87
|
+
| `@click.group()` subcommands | `cmd` (recursive) |
|
|
88
|
+
| Groups without positional args | `subcommand_required=#true` |
|
|
89
|
+
| `cmd.hidden` | Commands hidden from listing |
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
click_usage/__init__.py,sha256=23iG56mt-XirF0yrA27g-TqgzFE0KcIHdaWMW-JAlhw,1170
|
|
2
|
+
click_usage/convert.py,sha256=gy2I-jkuUnnnZF1Of2p16NNhLlaeK1IvU8g4ERWjTGg,6808
|
|
3
|
+
usage_spec_click-1.0.0.dist-info/METADATA,sha256=75kUob2pvSDKOWGYr_O-PrtK6xTUE4ShMBuNzKVzeYc,2580
|
|
4
|
+
usage_spec_click-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
5
|
+
usage_spec_click-1.0.0.dist-info/RECORD,,
|