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