usage-spec-typer 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.
typer_usage/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Usage spec integration for Typer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
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(app: typer.Typer, bin_name: str | None = None) -> str:
|
|
26
|
+
"""Generate usage spec in KDL format from a Typer app."""
|
|
27
|
+
spec = convert_root(app, bin_name)
|
|
28
|
+
return core_generate(spec, format="kdl", comment="@generated by usage-spec-typer from Typer metadata")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_kdl(app: typer.Typer, bin_name: str | None = None) -> str:
|
|
32
|
+
"""Generate usage spec in KDL format (alias for generate)."""
|
|
33
|
+
return generate(app, bin_name)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_json(app: typer.Typer, bin_name: str | None = None) -> str:
|
|
37
|
+
"""Generate usage spec in JSON format from a Typer app."""
|
|
38
|
+
spec = convert_root(app, bin_name)
|
|
39
|
+
return core_generate(spec, format="json")
|
typer_usage/convert.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Convert Typer apps to usage spec.
|
|
2
|
+
|
|
3
|
+
Typer is built on top of click, so this module reuses click conversion logic
|
|
4
|
+
and adds Typer-specific handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import click as _click
|
|
12
|
+
import typer
|
|
13
|
+
from usage_spec import Spec, SpecArg, SpecFlag, SpecCommand, SpecChoices
|
|
14
|
+
|
|
15
|
+
from click_usage.convert import convert_root as _click_convert_root, _convert_arg, _convert_flag, _convert_command
|
|
16
|
+
|
|
17
|
+
# Typer auto-generated flags to skip
|
|
18
|
+
_TYPER_BUILTIN_FLAG_NAMES = frozenset({
|
|
19
|
+
"install-completion",
|
|
20
|
+
"show-completion",
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_typer_argument(arg: _click.Argument) -> bool:
|
|
25
|
+
return hasattr(arg, "help") # TyperArgument has help; standard click.Argument does not
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _convert_typer_arg(arg: _click.Argument) -> SpecArg:
|
|
29
|
+
"""Convert a TyperArgument, which has help/hidden that standard click.Argument lacks."""
|
|
30
|
+
result = _convert_arg(arg)
|
|
31
|
+
|
|
32
|
+
if _is_typer_argument(arg):
|
|
33
|
+
result.help = getattr(arg, "help", "") or ""
|
|
34
|
+
result.hide = getattr(arg, "hidden", False)
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _convert_typer_command(cmd: _click.BaseCommand) -> SpecCommand:
|
|
40
|
+
"""Convert a click command that may contain Typer-specific params."""
|
|
41
|
+
sc = _convert_command(cmd)
|
|
42
|
+
|
|
43
|
+
# Replace args with Typer-aware conversion
|
|
44
|
+
sc.args = []
|
|
45
|
+
for param in cmd.params:
|
|
46
|
+
if isinstance(param, _click.Argument):
|
|
47
|
+
sc.args.append(_convert_typer_arg(param))
|
|
48
|
+
|
|
49
|
+
# Filter out Typer built-in flags
|
|
50
|
+
sc.flags = [f for f in sc.flags if f.long not in _TYPER_BUILTIN_FLAG_NAMES]
|
|
51
|
+
|
|
52
|
+
return sc
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_typer_builtin_option(opt: _click.Option) -> bool:
|
|
56
|
+
name = opt.name or ""
|
|
57
|
+
return name in _TYPER_BUILTIN_FLAG_NAMES or name in ("help", "version")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def convert_root(app: typer.Typer, bin_name: str | None = None) -> Spec:
|
|
61
|
+
"""Convert a Typer app to a Spec object."""
|
|
62
|
+
name = bin_name or app.info.name or ""
|
|
63
|
+
|
|
64
|
+
spec = Spec(
|
|
65
|
+
name=name,
|
|
66
|
+
bin=name,
|
|
67
|
+
version="",
|
|
68
|
+
about=app.info.help or "",
|
|
69
|
+
long="",
|
|
70
|
+
usage="",
|
|
71
|
+
flags=[],
|
|
72
|
+
args=[],
|
|
73
|
+
cmds=[],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Handle empty Typer apps (no commands registered)
|
|
77
|
+
if not app.registered_commands and not app.registered_groups:
|
|
78
|
+
return spec
|
|
79
|
+
|
|
80
|
+
# Get the underlying click command from Typer
|
|
81
|
+
try:
|
|
82
|
+
click_cmd = typer.main.get_command(app)
|
|
83
|
+
except RuntimeError:
|
|
84
|
+
return spec
|
|
85
|
+
|
|
86
|
+
if not name:
|
|
87
|
+
name = click_cmd.name or ""
|
|
88
|
+
spec.name = name
|
|
89
|
+
spec.bin = name
|
|
90
|
+
|
|
91
|
+
# If Typer has a single command, it returns a Command, not a Group.
|
|
92
|
+
# We treat that single command as the root.
|
|
93
|
+
is_group = isinstance(click_cmd, _click.Group)
|
|
94
|
+
|
|
95
|
+
if is_group:
|
|
96
|
+
# Multi-command app: root is the group, commands are subcommands
|
|
97
|
+
spec.about = app.info.help or click_cmd.short_help or click_cmd.help or ""
|
|
98
|
+
spec.long = (app.info.help and click_cmd.help and click_cmd.help) or ""
|
|
99
|
+
|
|
100
|
+
# Root-level params from the group
|
|
101
|
+
for param in click_cmd.params:
|
|
102
|
+
if isinstance(param, _click.Option):
|
|
103
|
+
if _is_typer_builtin_option(param):
|
|
104
|
+
if param.name == "version" and param.default is not None:
|
|
105
|
+
spec.version = str(param.default)
|
|
106
|
+
continue
|
|
107
|
+
spec.flags.append(_convert_flag(param))
|
|
108
|
+
elif isinstance(param, _click.Argument):
|
|
109
|
+
spec.args.append(_convert_typer_arg(param))
|
|
110
|
+
|
|
111
|
+
# Subcommands
|
|
112
|
+
group = click_cmd # type: _click.Group
|
|
113
|
+
if group.commands:
|
|
114
|
+
for sub_name, sub_cmd in group.commands.items():
|
|
115
|
+
if sub_cmd.hidden:
|
|
116
|
+
continue
|
|
117
|
+
spec.cmds.append(_convert_typer_command(sub_cmd))
|
|
118
|
+
else:
|
|
119
|
+
ctx = _click.Context(click_cmd)
|
|
120
|
+
for sub_name in group.list_commands(ctx):
|
|
121
|
+
sub_cmd = group.get_command(ctx, sub_name)
|
|
122
|
+
if sub_cmd and not sub_cmd.hidden:
|
|
123
|
+
spec.cmds.append(_convert_typer_command(sub_cmd))
|
|
124
|
+
else:
|
|
125
|
+
# Single-command app: treat the command as root
|
|
126
|
+
spec.about = app.info.help or click_cmd.short_help or click_cmd.help or ""
|
|
127
|
+
|
|
128
|
+
for param in click_cmd.params:
|
|
129
|
+
if isinstance(param, _click.Option):
|
|
130
|
+
if _is_typer_builtin_option(param):
|
|
131
|
+
continue
|
|
132
|
+
spec.flags.append(_convert_flag(param))
|
|
133
|
+
elif isinstance(param, _click.Argument):
|
|
134
|
+
spec.args.append(_convert_typer_arg(param))
|
|
135
|
+
|
|
136
|
+
# Filter out Typer built-in flags
|
|
137
|
+
spec.flags = [f for f in spec.flags if f.long not in _TYPER_BUILTIN_FLAG_NAMES]
|
|
138
|
+
|
|
139
|
+
return spec
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: usage-spec-typer
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Generates usage spec for CLIs written with Typer
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: cli,kdl,shell-completion,typer,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: typer>=0.9
|
|
17
|
+
Requires-Dist: usage-spec
|
|
18
|
+
Requires-Dist: usage-spec-click
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: typer>=0.9; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# usage-spec-typer
|
|
25
|
+
|
|
26
|
+
Generates [usage spec](https://usage.jdx.dev) for CLIs written with [Typer](https://typer.tiangolo.com/).
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pip install usage-spec-typer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import typer
|
|
38
|
+
from typer_usage import generate
|
|
39
|
+
|
|
40
|
+
app = typer.Typer(help="My CLI tool")
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def hello(name: str = typer.Argument(help="Your name")):
|
|
44
|
+
"""Say hello"""
|
|
45
|
+
|
|
46
|
+
@app.command()
|
|
47
|
+
def build(
|
|
48
|
+
output: str = typer.Option("dist", help="Output directory"),
|
|
49
|
+
verbose: bool = typer.Option(False, help="Enable verbose output"),
|
|
50
|
+
):
|
|
51
|
+
"""Build the project"""
|
|
52
|
+
|
|
53
|
+
print(generate(app, "mycli"))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### `generate(app, bin_name=None)`
|
|
59
|
+
|
|
60
|
+
Generates a usage spec in KDL format from a Typer `Typer` app.
|
|
61
|
+
|
|
62
|
+
### `generate_kdl(app, bin_name=None)`
|
|
63
|
+
|
|
64
|
+
Alias for `generate()`.
|
|
65
|
+
|
|
66
|
+
### `generate_json(app, bin_name=None)`
|
|
67
|
+
|
|
68
|
+
Generates a usage spec in JSON format.
|
|
69
|
+
|
|
70
|
+
### `convert_root(app, bin_name=None)`
|
|
71
|
+
|
|
72
|
+
Converts a Typer `Typer` app to the `Spec` data structure.
|
|
73
|
+
|
|
74
|
+
## Supported Features
|
|
75
|
+
|
|
76
|
+
| Typer Feature | Usage Spec Mapping |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `app.info.name` / `bin_name` | `name` / `bin` |
|
|
79
|
+
| `app.info.help` | `about` |
|
|
80
|
+
| `typer.Option()` | `flag` |
|
|
81
|
+
| `typer.Argument()` | `arg` |
|
|
82
|
+
| `typer.Option(..., help=)` | Flag `help` |
|
|
83
|
+
| `typer.Argument(..., help=)` | Arg `help` |
|
|
84
|
+
| `typer.Option(...)` (ellipsis) | `flag required=#true` |
|
|
85
|
+
| `bool` type options | Boolean flag (no arg) |
|
|
86
|
+
| `--flag/--no-flag` | `negate` |
|
|
87
|
+
| `count=True` | `count=#true` |
|
|
88
|
+
| `type=click.Choice()` | `choices` |
|
|
89
|
+
| `default=...` | `default` |
|
|
90
|
+
| `hidden=True` | `hide=#true` |
|
|
91
|
+
| `envvar="..."` | `env` |
|
|
92
|
+
| `app.add_typer()` | `cmd` (recursive) |
|
|
93
|
+
| Subcommand groups | `subcommand_required=#true` |
|
|
94
|
+
| Single command app | Treated as root-level |
|
|
95
|
+
| `--install-completion` / `--show-completion` | Filtered out |
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
typer_usage/__init__.py,sha256=Q3SRnYm2AVmWkklUoET8iyszPfsbe4M-vY95nTed2ng,1132
|
|
2
|
+
typer_usage/convert.py,sha256=JoGEvrM5gINcF1O7KF1SwODVSZjls1QuKjGXWy3ueHc,4643
|
|
3
|
+
usage_spec_typer-1.0.0.dist-info/METADATA,sha256=IZH7cZ6FL4xVzBY_HT1P7Ap-BhNzMWOjhpMcvNb5fRs,2639
|
|
4
|
+
usage_spec_typer-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
5
|
+
usage_spec_typer-1.0.0.dist-info/RECORD,,
|