gencodo-py 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.
- gencodo/__init__.py +23 -0
- gencodo/_core.py +287 -0
- gencodo/_jinja_env.py +31 -0
- gencodo/_types.py +92 -0
- gencodo/py.typed +0 -0
- gencodo/templates/md/command.md.j2 +43 -0
- gencodo/templates/md/index.md.j2 +25 -0
- gencodo/templates/rst/command.rst.j2 +54 -0
- gencodo/templates/rst/index.rst.j2 +39 -0
- gencodo_py-0.1.0.dist-info/METADATA +144 -0
- gencodo_py-0.1.0.dist-info/RECORD +12 -0
- gencodo_py-0.1.0.dist-info/WHEEL +4 -0
gencodo/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""gencodo -- Generate CLI reference documentation from argparse-based applications."""
|
|
2
|
+
|
|
3
|
+
from gencodo._core import gen_docs, gen_docs_tree, get_bundled_templates
|
|
4
|
+
from gencodo._types import (
|
|
5
|
+
Command,
|
|
6
|
+
CommandGroup,
|
|
7
|
+
ExampleInfo,
|
|
8
|
+
FlagInfo,
|
|
9
|
+
TemplateInfo,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Command",
|
|
14
|
+
"CommandGroup",
|
|
15
|
+
"ExampleInfo",
|
|
16
|
+
"FlagInfo",
|
|
17
|
+
"TemplateInfo",
|
|
18
|
+
"gen_docs",
|
|
19
|
+
"gen_docs_tree",
|
|
20
|
+
"get_bundled_templates",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
gencodo/_core.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Core documentation generation logic for gencodo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib.resources
|
|
7
|
+
import pathlib
|
|
8
|
+
import re
|
|
9
|
+
from collections.abc import Callable, Sequence
|
|
10
|
+
from typing import Literal, TextIO
|
|
11
|
+
|
|
12
|
+
from gencodo._jinja_env import _make_jinja_env
|
|
13
|
+
from gencodo._types import (
|
|
14
|
+
Command,
|
|
15
|
+
CommandGroup,
|
|
16
|
+
ExampleInfo,
|
|
17
|
+
FlagInfo,
|
|
18
|
+
TemplateInfo,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"gen_docs",
|
|
23
|
+
"gen_docs_tree",
|
|
24
|
+
"get_bundled_templates",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _instantiate_command(command_class: type[Command]) -> Command:
|
|
29
|
+
"""Instantiate a command class for parser introspection.
|
|
30
|
+
|
|
31
|
+
Tries command_class(None) first (craft_cli compatible),
|
|
32
|
+
then command_class() for simple classes.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
return command_class(None) # type: ignore[call-arg]
|
|
36
|
+
except TypeError:
|
|
37
|
+
return command_class() # type: ignore[call-arg]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _extract_flags(command_class: type[Command]) -> list[FlagInfo]:
|
|
41
|
+
"""Extract optional flags from a command class as FlagInfo objects.
|
|
42
|
+
|
|
43
|
+
Positional arguments and suppressed flags are excluded.
|
|
44
|
+
The longest option string is used as the flag name.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
command_class: A command class satisfying the Command protocol.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
A list of FlagInfo objects for the command's optional flags.
|
|
51
|
+
"""
|
|
52
|
+
parser = argparse.ArgumentParser(prog=command_class.name, add_help=False)
|
|
53
|
+
_instantiate_command(command_class).fill_parser(parser)
|
|
54
|
+
flags: list[FlagInfo] = []
|
|
55
|
+
for action in parser._actions:
|
|
56
|
+
if not action.option_strings:
|
|
57
|
+
continue
|
|
58
|
+
if action.help == argparse.SUPPRESS:
|
|
59
|
+
continue
|
|
60
|
+
name = max(action.option_strings, key=len)
|
|
61
|
+
usage = action.help or ""
|
|
62
|
+
if action.default is None or action.default is argparse.SUPPRESS:
|
|
63
|
+
default = ""
|
|
64
|
+
else:
|
|
65
|
+
default = str(action.default)
|
|
66
|
+
flags.append(FlagInfo(name=name, usage=usage, default_value=default))
|
|
67
|
+
return flags
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _infer_related(
|
|
71
|
+
command_class: type[Command],
|
|
72
|
+
command_groups: Sequence[CommandGroup],
|
|
73
|
+
) -> list[str]:
|
|
74
|
+
"""Determine the list of related command names for a command.
|
|
75
|
+
|
|
76
|
+
If the command has explicit ``related_commands``, those are validated
|
|
77
|
+
and returned. Otherwise, non-hidden siblings in the same group are returned.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
command_class: The command class to find related commands for.
|
|
81
|
+
command_groups: All command groups in the application.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
A list of related command names.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If an explicit related command name is not found.
|
|
88
|
+
"""
|
|
89
|
+
if command_class.related_commands is not None:
|
|
90
|
+
all_names = {c.name for g in command_groups for c in g.commands}
|
|
91
|
+
for name in command_class.related_commands:
|
|
92
|
+
if name not in all_names:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"related command {name!r} not found in command_groups"
|
|
95
|
+
)
|
|
96
|
+
return list(command_class.related_commands)
|
|
97
|
+
for group in command_groups:
|
|
98
|
+
if command_class in group.commands:
|
|
99
|
+
siblings = [
|
|
100
|
+
c.name
|
|
101
|
+
for c in group.commands
|
|
102
|
+
if c is not command_class and not c.hidden
|
|
103
|
+
]
|
|
104
|
+
return sorted(siblings)
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _build_template_context(
|
|
109
|
+
command_class: type[Command],
|
|
110
|
+
appname: str,
|
|
111
|
+
command_groups: Sequence[CommandGroup],
|
|
112
|
+
) -> dict[str, object]:
|
|
113
|
+
"""Build the complete Jinja2 template context dict for a command.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
command_class: The command class to build context for.
|
|
117
|
+
appname: The application name used in usage strings.
|
|
118
|
+
command_groups: All command groups in the application.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
A dict with keys: ref, command_name, short, long, synopsis,
|
|
122
|
+
examples, flags, related_commands, heading_len, appname.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ValueError: If command_name or help_msg is empty.
|
|
126
|
+
"""
|
|
127
|
+
command_name = command_class.name
|
|
128
|
+
short = command_class.help_msg
|
|
129
|
+
if not command_name:
|
|
130
|
+
raise ValueError("command_name must not be empty")
|
|
131
|
+
if not short:
|
|
132
|
+
raise ValueError("short (help_msg) must not be empty")
|
|
133
|
+
|
|
134
|
+
long = (command_class.overview or "").strip()
|
|
135
|
+
|
|
136
|
+
parser = argparse.ArgumentParser(
|
|
137
|
+
prog=f"{appname} {command_name}", add_help=False
|
|
138
|
+
)
|
|
139
|
+
_instantiate_command(command_class).fill_parser(parser)
|
|
140
|
+
raw_usage = parser.format_usage()
|
|
141
|
+
synopsis = re.sub(r"^usage:\s*", "", raw_usage).strip()
|
|
142
|
+
|
|
143
|
+
examples = [
|
|
144
|
+
ExampleInfo(info=info, usage=usage)
|
|
145
|
+
for info, usage in command_class.examples
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
flags = _extract_flags(command_class)
|
|
149
|
+
related_commands = _infer_related(command_class, command_groups)
|
|
150
|
+
heading_len = len(command_name)
|
|
151
|
+
ref = command_name.replace("-", "_").replace(" ", "_")
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"ref": ref,
|
|
155
|
+
"command_name": command_name,
|
|
156
|
+
"short": short,
|
|
157
|
+
"long": long,
|
|
158
|
+
"synopsis": synopsis,
|
|
159
|
+
"examples": examples,
|
|
160
|
+
"flags": flags,
|
|
161
|
+
"related_commands": related_commands,
|
|
162
|
+
"heading_len": heading_len,
|
|
163
|
+
"appname": appname,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def gen_docs(
|
|
168
|
+
command_class: type[Command],
|
|
169
|
+
writer: TextIO,
|
|
170
|
+
template: str,
|
|
171
|
+
appname: str,
|
|
172
|
+
command_groups: Sequence[CommandGroup],
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Render documentation for a single command to a writer.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
command_class: The command class to document.
|
|
178
|
+
writer: A text stream to write the rendered output to.
|
|
179
|
+
template: A Jinja2 template string for the command page.
|
|
180
|
+
appname: The application name used in usage strings.
|
|
181
|
+
command_groups: All command groups (used for related commands).
|
|
182
|
+
"""
|
|
183
|
+
env = _make_jinja_env()
|
|
184
|
+
compiled = env.from_string(template)
|
|
185
|
+
context = _build_template_context(command_class, appname, command_groups)
|
|
186
|
+
writer.write(compiled.render(context))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def gen_docs_tree(
|
|
190
|
+
appname: str,
|
|
191
|
+
command_groups: Sequence[CommandGroup],
|
|
192
|
+
output_dir: pathlib.Path,
|
|
193
|
+
templates: TemplateInfo,
|
|
194
|
+
file_prepender: Callable[[str], str] | None = None,
|
|
195
|
+
file_extension: str = ".md",
|
|
196
|
+
) -> list[str]:
|
|
197
|
+
"""Generate a documentation tree for all non-hidden commands.
|
|
198
|
+
|
|
199
|
+
Creates one file per command plus an index file in the output directory.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
appname: The application name used in usage strings.
|
|
203
|
+
command_groups: All command groups in the application.
|
|
204
|
+
output_dir: Directory to write generated files into (created if needed).
|
|
205
|
+
templates: Template configuration for index and command pages.
|
|
206
|
+
file_prepender: Optional callable returning a string to prepend to each file.
|
|
207
|
+
file_extension: File extension for command pages (default: ".md").
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
A list of generated command filenames (not including the index).
|
|
211
|
+
"""
|
|
212
|
+
output_dir = pathlib.Path(output_dir)
|
|
213
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
214
|
+
|
|
215
|
+
env = _make_jinja_env()
|
|
216
|
+
compiled_cmd = env.from_string(templates.command_template)
|
|
217
|
+
compiled_idx = env.from_string(templates.index_template)
|
|
218
|
+
|
|
219
|
+
generated: list[str] = []
|
|
220
|
+
files_context: list[dict[str, str]] = []
|
|
221
|
+
|
|
222
|
+
for group in command_groups:
|
|
223
|
+
for cmd in group.commands:
|
|
224
|
+
if cmd.hidden:
|
|
225
|
+
continue
|
|
226
|
+
filename = cmd.name.replace(" ", "-") + file_extension
|
|
227
|
+
context = _build_template_context(cmd, appname, command_groups)
|
|
228
|
+
content = compiled_cmd.render(context)
|
|
229
|
+
if file_prepender is not None:
|
|
230
|
+
content = file_prepender(filename) + content
|
|
231
|
+
(output_dir / filename).write_text(content, encoding="utf-8")
|
|
232
|
+
generated.append(filename)
|
|
233
|
+
files_context.append(
|
|
234
|
+
{
|
|
235
|
+
"filename": filename,
|
|
236
|
+
"command_name": cmd.name,
|
|
237
|
+
"short": cmd.help_msg,
|
|
238
|
+
"group_name": group.name,
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
idx_content = compiled_idx.render(files=files_context, appname=appname)
|
|
243
|
+
if file_prepender is not None:
|
|
244
|
+
idx_content = file_prepender(templates.index_file_name) + idx_content
|
|
245
|
+
(output_dir / templates.index_file_name).write_text(
|
|
246
|
+
idx_content, encoding="utf-8"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return generated
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_bundled_templates(
|
|
253
|
+
format: Literal["rst", "md"] = "rst",
|
|
254
|
+
index_file_name: str | None = None,
|
|
255
|
+
) -> TemplateInfo:
|
|
256
|
+
"""Load bundled default templates for the given format.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
format: Template format -- "rst" for reStructuredText, "md" for Markdown.
|
|
260
|
+
index_file_name: Override the default index filename.
|
|
261
|
+
Defaults to "index.rst" or "index.md" based on format.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
A TemplateInfo with the bundled templates loaded.
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
ValueError: If format is not "rst" or "md".
|
|
268
|
+
"""
|
|
269
|
+
if format not in ("rst", "md"):
|
|
270
|
+
raise ValueError(f"format must be 'rst' or 'md', got {format!r}")
|
|
271
|
+
|
|
272
|
+
templates_pkg = importlib.resources.files("gencodo") / "templates" / format
|
|
273
|
+
command_template = (
|
|
274
|
+
(templates_pkg / f"command.{format}.j2").read_text(encoding="utf-8")
|
|
275
|
+
)
|
|
276
|
+
index_template = (
|
|
277
|
+
(templates_pkg / f"index.{format}.j2").read_text(encoding="utf-8")
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if index_file_name is None:
|
|
281
|
+
index_file_name = f"index.{format}"
|
|
282
|
+
|
|
283
|
+
return TemplateInfo(
|
|
284
|
+
index_file_name=index_file_name,
|
|
285
|
+
index_template=index_template,
|
|
286
|
+
command_template=command_template,
|
|
287
|
+
)
|
gencodo/_jinja_env.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Jinja2 environment configuration for gencodo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from jinja2 import Environment, StrictUndefined
|
|
6
|
+
from jinja2.filters import do_indent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _make_jinja_env() -> Environment:
|
|
10
|
+
"""Create a configured Jinja2 Environment for documentation generation.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
A Jinja2 Environment with custom filters for documentation rendering.
|
|
14
|
+
"""
|
|
15
|
+
env = Environment(
|
|
16
|
+
autoescape=False,
|
|
17
|
+
undefined=StrictUndefined,
|
|
18
|
+
trim_blocks=True,
|
|
19
|
+
lstrip_blocks=True,
|
|
20
|
+
keep_trailing_newline=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def _indent(s: str, width: int = 4, *, blank: bool = False) -> str:
|
|
24
|
+
return do_indent(s, width=width, first=True, blank=blank)
|
|
25
|
+
|
|
26
|
+
def _repeat(s: str, n: int) -> str:
|
|
27
|
+
return s * n
|
|
28
|
+
|
|
29
|
+
env.filters["indent"] = _indent
|
|
30
|
+
env.filters["repeat"] = _repeat
|
|
31
|
+
return env
|
gencodo/_types.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Type definitions for gencodo: protocols, dataclasses, and named tuples."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import dataclasses
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import NamedTuple, Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Command",
|
|
12
|
+
"CommandGroup",
|
|
13
|
+
"ExampleInfo",
|
|
14
|
+
"FlagInfo",
|
|
15
|
+
"TemplateInfo",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class Command(Protocol):
|
|
21
|
+
"""Protocol that any CLI command class must satisfy.
|
|
22
|
+
|
|
23
|
+
Any class with these attributes and methods can be used with gencodo
|
|
24
|
+
via structural subtyping -- no inheritance required.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
"""The command name as invoked on the command line."""
|
|
29
|
+
help_msg: str
|
|
30
|
+
"""Short one-line help string."""
|
|
31
|
+
overview: str
|
|
32
|
+
"""Longer description of the command (may be multi-line)."""
|
|
33
|
+
hidden: bool
|
|
34
|
+
"""Whether this command should be excluded from generated documentation."""
|
|
35
|
+
examples: list[tuple[str, str]]
|
|
36
|
+
"""List of (description, command_string) example pairs."""
|
|
37
|
+
related_commands: list[str] | None
|
|
38
|
+
"""Explicit list of related command names, or None to infer from siblings."""
|
|
39
|
+
|
|
40
|
+
def fill_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
41
|
+
"""Add command-specific arguments to the parser."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CommandGroup(NamedTuple):
|
|
46
|
+
"""A named group of commands."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
"""Human-readable group name."""
|
|
50
|
+
commands: Sequence[type[Command]]
|
|
51
|
+
"""Command classes in this group."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclasses.dataclass(frozen=True, slots=True, eq=True, order=False)
|
|
55
|
+
class ExampleInfo:
|
|
56
|
+
"""Structured representation of a usage example."""
|
|
57
|
+
|
|
58
|
+
info: str
|
|
59
|
+
"""Human-readable description of what the example demonstrates."""
|
|
60
|
+
usage: str
|
|
61
|
+
"""The literal command string for the example."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclasses.dataclass(frozen=True, slots=True, eq=True, order=False)
|
|
65
|
+
class FlagInfo:
|
|
66
|
+
"""Structured representation of a single CLI flag."""
|
|
67
|
+
|
|
68
|
+
name: str
|
|
69
|
+
"""The flag name as it appears on the command line (e.g., '--verbose')."""
|
|
70
|
+
usage: str
|
|
71
|
+
"""One-line description of the flag's purpose."""
|
|
72
|
+
default_value: str
|
|
73
|
+
"""The default value shown in documentation."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclasses.dataclass(frozen=True, slots=True, eq=True, order=False)
|
|
77
|
+
class TemplateInfo:
|
|
78
|
+
"""Jinja2 template configuration for documentation generation."""
|
|
79
|
+
|
|
80
|
+
index_file_name: str
|
|
81
|
+
"""Output filename for the index document."""
|
|
82
|
+
index_template: str
|
|
83
|
+
"""Jinja2 template string for the index document."""
|
|
84
|
+
command_template: str
|
|
85
|
+
"""Jinja2 template string for per-command documents."""
|
|
86
|
+
|
|
87
|
+
def __post_init__(self) -> None:
|
|
88
|
+
"""Validate that no field is empty or whitespace-only."""
|
|
89
|
+
for field_name in ("index_file_name", "index_template", "command_template"):
|
|
90
|
+
value = getattr(self, field_name)
|
|
91
|
+
if not value.strip():
|
|
92
|
+
raise ValueError(f"TemplateInfo.{field_name} must not be empty")
|
gencodo/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# {{ command_name }}
|
|
2
|
+
|
|
3
|
+
{{ short }}
|
|
4
|
+
|
|
5
|
+
**Usage:**
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{{ synopsis }}
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
{{ long }}
|
|
14
|
+
|
|
15
|
+
{% if flags %}
|
|
16
|
+
## Options
|
|
17
|
+
|
|
18
|
+
| Flag | Description | Default |
|
|
19
|
+
|------|-------------|---------|
|
|
20
|
+
{% for flag in flags %}
|
|
21
|
+
| `{{ flag.name }}` | {{ flag.usage }} | {% if flag.default_value %}`{{ flag.default_value }}`{% endif %} |
|
|
22
|
+
{% endfor %}
|
|
23
|
+
|
|
24
|
+
{% endif %}
|
|
25
|
+
{% if examples %}
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
{% for example in examples %}
|
|
29
|
+
**{{ example.info }}**
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
{{ example.usage }}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
{% endfor %}
|
|
36
|
+
{% endif %}
|
|
37
|
+
{% if related_commands %}
|
|
38
|
+
## See also
|
|
39
|
+
|
|
40
|
+
{% for cmd in related_commands %}
|
|
41
|
+
- [{{ cmd }}]({{ cmd }}.md)
|
|
42
|
+
{% endfor %}
|
|
43
|
+
{% endif %}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# CLI Reference
|
|
2
|
+
|
|
3
|
+
Command-line interface reference for **{{ appname }}**.
|
|
4
|
+
|
|
5
|
+
This reference documentation is automatically generated from the command
|
|
6
|
+
definitions and provides detailed information about each available command.
|
|
7
|
+
|
|
8
|
+
## Available Commands
|
|
9
|
+
|
|
10
|
+
{% for group_name, group_files in files | groupby('group_name') %}
|
|
11
|
+
### {{ group_name }}
|
|
12
|
+
|
|
13
|
+
{% for file in group_files %}
|
|
14
|
+
- [{{ file.command_name }}]({{ file.filename }}) -- {{ file.short }}
|
|
15
|
+
{% endfor %}
|
|
16
|
+
|
|
17
|
+
{% endfor %}
|
|
18
|
+
|
|
19
|
+
## Quick Reference Table
|
|
20
|
+
|
|
21
|
+
| Command | Description |
|
|
22
|
+
|---------|-------------|
|
|
23
|
+
{% for file in files %}
|
|
24
|
+
| [{{ file.command_name }}]({{ file.filename }}) | {{ file.short }} |
|
|
25
|
+
{% endfor %}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
.. _ref_{{ ref }}:
|
|
2
|
+
|
|
3
|
+
{{ command_name }}
|
|
4
|
+
{{ '=' | repeat(heading_len) }}
|
|
5
|
+
|
|
6
|
+
{{ short }}
|
|
7
|
+
|
|
8
|
+
**Usage:**
|
|
9
|
+
|
|
10
|
+
.. code-block:: bash
|
|
11
|
+
|
|
12
|
+
{{ synopsis }}
|
|
13
|
+
|
|
14
|
+
Overview
|
|
15
|
+
--------
|
|
16
|
+
|
|
17
|
+
{{ long }}
|
|
18
|
+
|
|
19
|
+
{% if flags %}
|
|
20
|
+
Options
|
|
21
|
+
-------
|
|
22
|
+
|
|
23
|
+
{% for flag in flags %}
|
|
24
|
+
.. option:: {{ flag.name }}
|
|
25
|
+
|
|
26
|
+
{{ flag.usage | indent(3) }}
|
|
27
|
+
{% if flag.default_value %}
|
|
28
|
+
|
|
29
|
+
Default: ``{{ flag.default_value }}``
|
|
30
|
+
{% endif %}
|
|
31
|
+
|
|
32
|
+
{% endfor %}
|
|
33
|
+
{% endif %}
|
|
34
|
+
{% if examples %}
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
|
|
38
|
+
{% for example in examples %}
|
|
39
|
+
**{{ example.info }}**
|
|
40
|
+
|
|
41
|
+
.. code-block:: bash
|
|
42
|
+
|
|
43
|
+
{{ example.usage }}
|
|
44
|
+
|
|
45
|
+
{% endfor %}
|
|
46
|
+
{% endif %}
|
|
47
|
+
{% if related_commands %}
|
|
48
|
+
See also
|
|
49
|
+
--------
|
|
50
|
+
|
|
51
|
+
{% for cmd in related_commands %}
|
|
52
|
+
- :ref:`{{ cmd }} <ref_{{ cmd | replace('-', '_') | replace(' ', '_') }}>`
|
|
53
|
+
{% endfor %}
|
|
54
|
+
{% endif %}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.. _cli_reference:
|
|
2
|
+
|
|
3
|
+
CLI Reference
|
|
4
|
+
=============
|
|
5
|
+
|
|
6
|
+
Command-line interface reference for **{{ appname }}**.
|
|
7
|
+
|
|
8
|
+
This reference documentation is automatically generated from the command
|
|
9
|
+
definitions and provides detailed information about each available command.
|
|
10
|
+
|
|
11
|
+
Available Commands
|
|
12
|
+
------------------
|
|
13
|
+
|
|
14
|
+
{% for group_name, group_files in files | groupby('group_name') %}
|
|
15
|
+
{{ group_name }}
|
|
16
|
+
{{ '~' | repeat(group_name | length) }}
|
|
17
|
+
|
|
18
|
+
.. toctree::
|
|
19
|
+
:maxdepth: 1
|
|
20
|
+
|
|
21
|
+
{% for file in group_files %}
|
|
22
|
+
{{ file.filename[:-4] }}
|
|
23
|
+
{% endfor %}
|
|
24
|
+
|
|
25
|
+
{% endfor %}
|
|
26
|
+
|
|
27
|
+
Quick Reference Table
|
|
28
|
+
---------------------
|
|
29
|
+
|
|
30
|
+
.. list-table::
|
|
31
|
+
:header-rows: 1
|
|
32
|
+
:widths: 25 75
|
|
33
|
+
|
|
34
|
+
* - Command
|
|
35
|
+
- Description
|
|
36
|
+
{% for file in files %}
|
|
37
|
+
* - :ref:`{{ file.command_name }} <ref_{{ file.command_name | replace('-', '_') | replace(' ', '_') }}>`
|
|
38
|
+
- {{ file.short }}
|
|
39
|
+
{% endfor %}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gencodo-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate CLI reference documentation from argparse-based applications using Jinja2 templates
|
|
5
|
+
Project-URL: Homepage, https://github.com/canonical/gencodo-py
|
|
6
|
+
Project-URL: Issues, https://github.com/canonical/gencodo-py/issues
|
|
7
|
+
Author: gencodo contributors
|
|
8
|
+
License-Expression: LGPL-3.0-only
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Documentation
|
|
18
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: jinja2>=3.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# gencodo-py
|
|
29
|
+
|
|
30
|
+
Generate CLI reference documentation from argparse-based applications using Jinja2 templates.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install gencodo-py
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
Define your CLI commands as plain Python classes (no base class required):
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import argparse
|
|
44
|
+
|
|
45
|
+
class GreetCommand:
|
|
46
|
+
name = "greet"
|
|
47
|
+
help_msg = "Greet a specific person"
|
|
48
|
+
overview = "Personalize your greeting by specifying a name."
|
|
49
|
+
hidden = False
|
|
50
|
+
examples = [("Greet Alice", "myapp greet Alice")]
|
|
51
|
+
related_commands = None
|
|
52
|
+
|
|
53
|
+
def fill_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
54
|
+
parser.add_argument("name", help="Name to greet")
|
|
55
|
+
parser.add_argument("--formal", action="store_true", default=False,
|
|
56
|
+
help="Use formal style")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Generate documentation:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from gencodo import CommandGroup, gen_docs_tree, get_bundled_templates
|
|
63
|
+
|
|
64
|
+
groups = [CommandGroup(name="Greetings", commands=[GreetCommand])]
|
|
65
|
+
templates = get_bundled_templates("md") # or "rst"
|
|
66
|
+
|
|
67
|
+
gen_docs_tree(
|
|
68
|
+
appname="myapp",
|
|
69
|
+
command_groups=groups,
|
|
70
|
+
output_dir="docs/cli-ref",
|
|
71
|
+
templates=templates,
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API Reference
|
|
76
|
+
|
|
77
|
+
### Types
|
|
78
|
+
|
|
79
|
+
- **`Command`** -- Protocol that any CLI command class must satisfy (structural subtyping).
|
|
80
|
+
- **`CommandGroup`** -- NamedTuple grouping commands under a name.
|
|
81
|
+
- **`ExampleInfo`** -- Dataclass for a usage example (info, usage).
|
|
82
|
+
- **`FlagInfo`** -- Dataclass for a CLI flag (name, usage, default_value).
|
|
83
|
+
- **`TemplateInfo`** -- Dataclass for Jinja2 template configuration.
|
|
84
|
+
|
|
85
|
+
### Functions
|
|
86
|
+
|
|
87
|
+
- **`gen_docs(command_class, writer, template, appname, command_groups)`** -- Render docs for a single command to a TextIO writer.
|
|
88
|
+
- **`gen_docs_tree(appname, command_groups, output_dir, templates, ...)`** -- Generate a full documentation tree (one file per command + index).
|
|
89
|
+
- **`get_bundled_templates(format="rst", index_file_name=None)`** -- Load bundled reST or Markdown templates.
|
|
90
|
+
|
|
91
|
+
### Command Protocol
|
|
92
|
+
|
|
93
|
+
Your command classes need these attributes/methods:
|
|
94
|
+
|
|
95
|
+
| Attribute | Type | Description |
|
|
96
|
+
|-----------|------|-------------|
|
|
97
|
+
| `name` | `str` | Command name |
|
|
98
|
+
| `help_msg` | `str` | Short help string |
|
|
99
|
+
| `overview` | `str` | Longer description |
|
|
100
|
+
| `hidden` | `bool` | Exclude from docs if True |
|
|
101
|
+
| `examples` | `list[tuple[str, str]]` | (description, command) pairs |
|
|
102
|
+
| `related_commands` | `list[str] \| None` | Explicit related commands, or None to auto-infer |
|
|
103
|
+
| `fill_parser(parser)` | method | Add arguments to an ArgumentParser |
|
|
104
|
+
|
|
105
|
+
## Template Customization
|
|
106
|
+
|
|
107
|
+
### Bundled Templates
|
|
108
|
+
|
|
109
|
+
Use `get_bundled_templates("rst")` or `get_bundled_templates("md")` for the built-in templates.
|
|
110
|
+
|
|
111
|
+
### Custom Templates
|
|
112
|
+
|
|
113
|
+
Pass your own Jinja2 template strings via `TemplateInfo`:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from gencodo import TemplateInfo
|
|
117
|
+
|
|
118
|
+
templates = TemplateInfo(
|
|
119
|
+
index_file_name="index.md",
|
|
120
|
+
index_template="# Commands\n{% for f in files %}...\n{% endfor %}",
|
|
121
|
+
command_template="# {{ command_name }}\n{{ short }}\n...",
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Available template variables for command templates:
|
|
126
|
+
|
|
127
|
+
| Variable | Type | Description |
|
|
128
|
+
|----------|------|-------------|
|
|
129
|
+
| `ref` | `str` | Anchor reference (underscored name) |
|
|
130
|
+
| `command_name` | `str` | Command name |
|
|
131
|
+
| `short` | `str` | Short help message |
|
|
132
|
+
| `long` | `str` | Overview text |
|
|
133
|
+
| `synopsis` | `str` | Usage synopsis |
|
|
134
|
+
| `examples` | `list[ExampleInfo]` | Usage examples |
|
|
135
|
+
| `flags` | `list[FlagInfo]` | Optional flags |
|
|
136
|
+
| `related_commands` | `list[str]` | Related command names |
|
|
137
|
+
| `heading_len` | `int` | Length of command name |
|
|
138
|
+
| `appname` | `str` | Application name |
|
|
139
|
+
|
|
140
|
+
Custom Jinja2 filters available: `indent(width)`, `repeat(n)`.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
LGPL-3.0-only
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
gencodo/__init__.py,sha256=jn-eancNlIWTvin_HqfjREHb8VzTHpwxnEG5j8z467k,467
|
|
2
|
+
gencodo/_core.py,sha256=fx0HS35Ur20lHhC7g6gSHtYBmoB6ma91xAgefuGykOc,9421
|
|
3
|
+
gencodo/_jinja_env.py,sha256=GadUxPluIB5y_G2VVnGZ_SpVTN7PGc9hf9Suat5DJos,859
|
|
4
|
+
gencodo/_types.py,sha256=piDxKr0zanskQGmTV-Il6EKxGuvYeKBkTab41__EXtA,2907
|
|
5
|
+
gencodo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
gencodo/templates/md/command.md.j2,sha256=7lC23VA5hnotciY1FLClctMYqCQNqjbHtV80fWA-G0o,612
|
|
7
|
+
gencodo/templates/md/index.md.j2,sha256=e9_2YZilPpj9JTcx4d5yu0tiJI8Ro-tetCo_GBlzm4I,648
|
|
8
|
+
gencodo/templates/rst/command.rst.j2,sha256=agRAQdnSRGW3kR5QjWmWgcBeNHsCHziFI2O44tFfd38,741
|
|
9
|
+
gencodo/templates/rst/index.rst.j2,sha256=iUGoAPRwzdHnZob-Gl1j6XnKVVkZEmR5cXJfdziPXu4,838
|
|
10
|
+
gencodo_py-0.1.0.dist-info/METADATA,sha256=RqQF2hKFTsMIMuri35r6_twek8YSVX5jalvezLf2ero,4821
|
|
11
|
+
gencodo_py-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
+
gencodo_py-0.1.0.dist-info/RECORD,,
|