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