Skip to content

Commit 5ed31ec

Browse files
committed
feat(template): generate API reference pages from modules
Add a mise task that builds API reference pages from public Python modules and updates the docs template to rely on generated pages instead of a manual reference stub. This helps generated projects keep their API docs and navigation in sync with the codebase while reducing template maintenance.
1 parent 81ba7b7 commit 5ed31ec

File tree

7 files changed

+121
-12
lines changed

7 files changed

+121
-12
lines changed

.config/cspell.config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,22 @@ maxFileSize: 500KB
1414
useGitignore: true
1515
words:
1616
- arithmatex
17+
- bfadeab
1718
- chtml
19+
- codspeedhq
1820
- direnv
1921
- envops
2022
- envrc
2123
- fieldz
24+
- fspath
2225
- grimp
2326
- hynek
2427
- inlinehilite
2528
- kwargs
2629
- liblaf
2730
- linenums
2831
- lucide
32+
- mergery
2933
- mkdocs
3034
- mkdocstrings
3135
- noxfile
@@ -44,6 +48,7 @@ words:
4448
- pyrightconfig
4549
- pytest
4650
- pyvista
51+
- rumdl
4752
- sdist
4853
- strftime
4954
- taiki

mise-tasks/gen/ref-pages.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.14"
4+
# dependencies = [
5+
# "tomlkit>=0.14.0",
6+
# ]
7+
# ///
8+
9+
from __future__ import annotations
10+
11+
import argparse
12+
import dataclasses
13+
import os
14+
from collections.abc import Sequence
15+
from pathlib import Path
16+
from typing import Any
17+
18+
import tomlkit
19+
20+
MARKDOWN_TEMPLATE: str = """\
21+
::: {module_path}
22+
options:
23+
toc_label: {module_path}
24+
"""
25+
MODULE_SYMBOL: str = '<code class="doc-symbol doc-symbol-toc doc-symbol-module"></code>'
26+
27+
28+
class Args(argparse.Namespace):
29+
api_root: Path
30+
docs_dir: Path
31+
print_nav: bool
32+
src: Path
33+
34+
35+
@dataclasses.dataclass
36+
class Nav:
37+
module_path: str = ""
38+
index: str | None = None
39+
children: dict[str, Nav] = dataclasses.field(default_factory=dict)
40+
41+
def add(self, parts: Sequence[str], full_doc_path: Path) -> None:
42+
if not parts:
43+
self.index = os.fspath(full_doc_path)
44+
return
45+
if parts[0] in self.children:
46+
child = self.children[parts[0]]
47+
else:
48+
child = type(self)(
49+
module_path=f"{self.module_path}.{parts[0]}"
50+
if self.module_path
51+
else parts[0]
52+
)
53+
self.children[parts[0]] = child
54+
child.add(parts[1:], full_doc_path)
55+
56+
def dump(self) -> Any:
57+
children: list[Any] = []
58+
if self.index is not None:
59+
children.append(self.index)
60+
for _, child in sorted(self.children.items()):
61+
children.append(child.dump())
62+
if len(children) == 1:
63+
return children[0]
64+
return {f"{MODULE_SYMBOL} {self.module_path}": children}
65+
66+
67+
def is_public(part: str) -> bool:
68+
return not part.startswith("_") or (part.startswith("__") and part.endswith("__"))
69+
70+
71+
def parse_args() -> Args:
72+
parser: argparse.ArgumentParser = argparse.ArgumentParser()
73+
parser.add_argument("src", nargs="?", default="src", type=Path)
74+
parser.add_argument("--api-root", default="reference/", type=Path)
75+
parser.add_argument("--docs-dir", default="docs/", type=Path)
76+
parser.add_argument("--print-nav", action="store_true")
77+
return parser.parse_args(namespace=Args())
78+
79+
80+
def main() -> None:
81+
args: Args = parse_args()
82+
nav = Nav()
83+
for path in args.src.rglob("*.py"):
84+
relative: Path = path.relative_to(args.src)
85+
module_path: Path = relative.with_suffix("")
86+
parts: tuple[str, ...] = tuple(module_path.parts)
87+
if not all(is_public(part) for part in parts):
88+
continue
89+
doc_path: Path = relative.with_suffix(".md")
90+
if parts[-1] == "__init__":
91+
parts: tuple[str, ...] = parts[:-1]
92+
doc_path: Path = doc_path.with_name("README.md")
93+
elif parts[-1] == "__main__":
94+
continue
95+
doc_path: Path = args.api_root / doc_path
96+
full_doc_path: Path = args.docs_dir / doc_path
97+
full_doc_path.parent.mkdir(parents=True, exist_ok=True)
98+
full_doc_path.write_text(MARKDOWN_TEMPLATE.format(module_path=".".join(parts)))
99+
nav.add(parts, doc_path)
100+
print(tomlkit.dumps(nav.dump()))
101+
102+
103+
if __name__ == "__main__":
104+
main()

template/.config/mise/conf.d/50-python.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ description = "Serve the documentation site locally"
1919
run = ["rm --force --recursive '{{ config_root }}/.cache'", "zensical serve"]
2020

2121
[tasks."gen:init"]
22-
description = "Generate __init__.py files from __init__.pyi stubs"
22+
description = "Generate lazy-loading __init__.py files from __init__.pyi stubs"
2323
file = "https://raw.githubusercontent.com/liblaf/copier-python/refs/heads/main/mise-tasks/gen/init.py"
2424

25+
[tasks."gen:ref-pages"]
26+
description = "Generate API reference pages from public Python modules"
27+
file = "https://raw.githubusercontent.com/liblaf/copier-python/refs/heads/main/mise-tasks/gen/ref-pages.py"
28+
2529
[tasks.install]
26-
description = "Install project dependencies from lockfiles"
30+
description = "Install project dependencies from pixi.lock or uv.lock"
2731
file = "https://raw.githubusercontent.com/liblaf/copier-python/refs/heads/main/mise-tasks/install.sh"
2832

2933
[tasks.lint]
@@ -35,9 +39,9 @@ description = "Lint and format Python files"
3539
run = ["lint-imports", "ruff check --fix", "ruff format"]
3640

3741
[tasks."lint:toml"]
38-
description = "Sort, format, and lint TOML files"
42+
description = "Sort, format, and lint TOML files with toml-sort and tombi"
3943
file = "https://raw.githubusercontent.com/liblaf/copier-python/refs/heads/main/mise-tasks/lint/toml.sh"
4044

4145
[tasks.upgrade]
42-
description = "Upgrade project dependencies from lockfiles"
46+
description = "Upgrade project dependencies from pixi.lock or uv.lock"
4347
file = "https://raw.githubusercontent.com/liblaf/copier-python/refs/heads/main/mise-tasks/upgrade.sh"

template/README.md.jinja

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<!-- -*- mode: markdown; -*- -->
22

33
<div align="center" markdown>
4-
<a name="readme-top"></a>
54

65
![{{github_repo}}](https://socialify.git.ci/{{github_user}}/{{github_repo}}/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pattern=Transparent&pulls=1&stargazers=1&theme=Auto)
76

@@ -12,7 +11,7 @@
1211

1312
[Changelog](https://github.com/{{github_user}}/{{github_repo}}/blob/main/CHANGELOG.md) · [Report Bug](https://github.com/{{github_user}}/{{github_repo}}/issues) · [Request Feature](https://github.com/{{github_user}}/{{github_repo}}/issues)
1413

15-
![](https://cdn.jsdelivr.net/gh/andreasbm/readme/assets/lines/rainbow.png)
14+
![Rule](https://cdn.jsdelivr.net/gh/andreasbm/readme/assets/lines/rainbow.png)
1615

1716
</div>
1817

@@ -32,7 +31,7 @@ uv add {{ package_name }}
3231

3332
You can use Github Codespaces for online development:
3433

35-
[![](https://github.com/codespaces/badge.svg)](https://codespaces.new/{{github_user}}/{{github_repo}})
34+
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/{{github_user}}/{{github_repo}})
3635

3736
Or clone it for local development:
3837

template/docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
--8<-- "README.md"
1+
TODO

template/docs/reference/README.md.jinja

Lines changed: 0 additions & 1 deletion
This file was deleted.

template/zensical.toml.jinja

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ site_author = "{{ author_name }}"
1010
# Navigation
1111
nav = [
1212
{ "Home" = "README.md" },
13-
{ "API Reference" = "reference/README.md" },
1413
]
1514
# Repository
1615
repo_url = "https://github.com/{{ github_user }}/{{ github_repo }}"
@@ -98,11 +97,10 @@ extensions = [
9897
find_stubs_package = true
9998
show_inheritance_diagram = true
10099
# Headings
101-
parameter_headings = true
100+
heading_level = 1
102101
show_root_heading = true
103102
show_symbol_type_heading = true
104103
show_symbol_type_toc = true
105-
type_parameter_headings = true
106104
# Members
107105
inherited_members = true
108106
filters = "public"

0 commit comments

Comments
 (0)