-
Notifications
You must be signed in to change notification settings - Fork 291
Expand file tree
/
Copy pathgenerate_api_docs.py
More file actions
219 lines (174 loc) · 7.3 KB
/
Copy pathgenerate_api_docs.py
File metadata and controls
219 lines (174 loc) · 7.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/env python
# Generate the Markdown API reference for slack_bolt using griffe + griffe2md.
import os
import re
import shutil
import griffe
from griffe2md import default_config, render_object_docs
from markdown_it import MarkdownIt
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DOCS_BASE_PATH = os.path.join(REPO_ROOT, "docs", "english")
REFERENCE_SUBDIR = "reference"
REFERENCE_DIR = os.path.join(DOCS_BASE_PATH, REFERENCE_SUBDIR)
SIDEBAR_DOC_ID_PREFIX = "tools/bolt-python/"
PACKAGE = "slack_bolt"
# Shared CommonMark tokenizer. Used to locate code blocks
_MD_PARSER = MarkdownIt("commonmark")
# --------------------------------------------------------------------------- #
# griffe2md rendering
# --------------------------------------------------------------------------- #
# griffe2md renders a griffe object to Markdown.
CONFIG = dict(default_config)
CONFIG.update(
docstring_style="google",
summary=False,
show_if_no_docstring=False,
show_submodules=False,
show_root_heading=False,
show_root_full_path=False,
show_root_members_full_path=False,
show_object_full_path=False,
heading_level=2,
# big to keep signatures single-line, so output never depends on Black.
line_length=10**9,
)
def _render_module(module):
# Render a module to MDX-safe Markdown via griffe2md + post-processing.
return _post_process(render_object_docs(module, CONFIG))
# --------------------------------------------------------------------------- #
# MDX-safety post-processing
# --------------------------------------------------------------------------- #
_CODE_SPLIT_RE = re.compile(r"(```[\s\S]*?```|`[^`]*`)")
_CODE_TAG_RE = re.compile(r"</?code>")
_ANCHOR_LINK_RE = re.compile(r"\[([^\]]+)\]\(#[^)]*\)")
def _reflow_indented_code(text):
# Convert indented docstring code blocks into fenced ``python`` blocks.
blocks = [t for t in _MD_PARSER.parse(text) if t.type == "code_block" and t.map]
if not blocks:
return text
lines = text.split("\n")
out = []
cursor = 0 # 0-indexed line pointer into `lines`
for token in blocks:
start, end = token.map
out.extend(lines[cursor:start])
out.append("```python")
out.extend(token.content.rstrip("\n").split("\n"))
out.append("```")
out.append("")
cursor = end
if cursor < len(lines) and not lines[cursor].strip():
cursor += 1
out.extend(lines[cursor:])
return "\n".join(out)
def _escape_prose(chunk):
# Strip griffe2md markup, then escape MDX-hazardous characters in prose.
chunk = _CODE_TAG_RE.sub("", chunk)
chunk = _ANCHOR_LINK_RE.sub(r"\1", chunk)
return chunk.replace("<", "<").replace("{", "{")
def _post_process(text):
# Make griffe2md's Markdown safe to compile as MDX.
text = _reflow_indented_code(text)
out = []
for i, chunk in enumerate(_CODE_SPLIT_RE.split(text)):
# Odd indices are the captured code spans/blocks -- leave them verbatim.
out.append(chunk if i % 2 else _escape_prose(chunk))
return "".join(out).rstrip("\n")
# --------------------------------------------------------------------------- #
# Module -> page
# --------------------------------------------------------------------------- #
def _relative_path(module):
# Path of *module* relative to the top package (``""`` for slack_bolt).
if module.name == PACKAGE:
return ""
return module.canonical_path.split(".", 1)[1].replace(".", "/")
def _iter_modules(module):
# Yield *module* and every submodule, depth-first in source order.
yield module
for member in module.members.values():
if not member.is_alias and member.is_module:
yield from _iter_modules(member)
# --------------------------------------------------------------------------- #
# Routes
# --------------------------------------------------------------------------- #
def _doc_route(rel_path):
base = "{}/{}".format(REFERENCE_SUBDIR, rel_path) if rel_path else REFERENCE_SUBDIR
return "/" + SIDEBAR_DOC_ID_PREFIX + base
# --------------------------------------------------------------------------- #
# Generation
# --------------------------------------------------------------------------- #
def _load_package():
return griffe.load(
PACKAGE,
search_paths=[REPO_ROOT],
docstring_parser=griffe.Parser.google,
resolve_aliases=True,
)
def _build_pages(root):
# Render every module into an in-memory page record
pages = {}
for module in _iter_modules(root):
rel_path = _relative_path(module)
pages[rel_path] = {
"module": module,
"is_package": module.is_init_module,
"title": module.canonical_path,
# griffe's module.name is the bare final component (e.g. "app").
"sidebar_label": module.name,
"content": _render_module(module),
}
return pages
def _submodule_links(rel_path, pages):
# Sorted child module/subpackage links for a package overview page.
prefix = rel_path + "/" if rel_path else ""
depth = prefix.count("/")
children = []
for other_rel, page in pages.items():
if not other_rel or not other_rel.startswith(prefix):
continue
if other_rel.count("/") != depth:
continue
children.append((page["title"], _doc_route(other_rel)))
children.sort()
return children
def _write_pages(pages):
for rel_path, page in pages.items():
if page["is_package"] or not rel_path:
path = os.path.join(REFERENCE_DIR, rel_path, "index.md")
else:
path = os.path.join(REFERENCE_DIR, rel_path + ".md")
os.makedirs(os.path.dirname(path), exist_ok=True)
frontmatter = ["---", "sidebar_label: {}".format(page["sidebar_label"]), "title: {}".format(page["title"])]
# Pin the top-level reference index to the top of its sidebar category;
if not rel_path:
frontmatter.append("sidebar_position: 1")
# A module whose file is <folder>/<folder>.md collides with the folder's
# index.md route; pin it with a relative slug.
basename = os.path.basename(path)[: -len(".md")]
parent = os.path.basename(os.path.dirname(path))
if basename == parent and basename != "index":
frontmatter.append("slug: {}".format(basename))
frontmatter.append("---")
body_parts = []
if page["content"]:
body_parts.append(page["content"])
body_parts.append("")
if page["is_package"] or not rel_path:
links = _submodule_links(rel_path, pages)
if links:
body_parts.append("## Submodules")
body_parts.append("")
body_parts += ["- [{}]({})".format(title, route) for title, route in links]
body_parts.append("")
with open(path, "w", encoding="utf-8") as handle:
handle.write("\n".join(frontmatter) + "\n\n" + "\n".join(body_parts).rstrip("\n") + "\n")
def main():
# Rebuild the reference tree
shutil.rmtree(REFERENCE_DIR, ignore_errors=True)
os.makedirs(REFERENCE_DIR, exist_ok=True)
root = _load_package()
pages = _build_pages(root)
_write_pages(pages)
print("Generated {} reference pages".format(len(pages)))
if __name__ == "__main__":
main()