-
-
Notifications
You must be signed in to change notification settings - Fork 8
/
build_api_docs.py
305 lines (268 loc) · 10.1 KB
/
build_api_docs.py
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
from dataclasses import dataclass
import html
from importlib import import_module
from operator import attrgetter
import re
from typing import List, Optional, TextIO, Type
from pydoc_markdown.interfaces import Context, SourceLinker
from pydoc_markdown.contrib.loaders.python import PythonLoader
from pydoc_markdown.contrib.renderers.markdown import (
MarkdownRenderer,
MarkdownReferenceResolver,
)
from pydoc_markdown.contrib.processors.google import GoogleProcessor
from pydoc_markdown.contrib.processors.filter import FilterProcessor
from pydoc_markdown.contrib.processors.crossref import CrossrefProcessor
from pydoc_markdown.util.docspec import ApiSuite
import docspec_python
import docspec
slugify = re.compile(r"[^a-zA-Z0-9_\-]")
dedup = re.compile(r"(-)\1+")
WHITELIST = ("__init__",)
class MayimRenderer(MarkdownRenderer):
def _render_recursive(
self, fp: TextIO, level: int, obj: docspec.ApiObject
):
self._render_object(fp, level, obj)
level += 1
members = sorted(getattr(obj, "members", []), key=attrgetter("name"))
for member in members:
self._render_recursive(fp, level, member)
def _render_object(self, fp: TextIO, level: int, obj: docspec.ApiObject):
if isinstance(obj, docspec.Indirection):
if obj.parent and obj.parent.name == "mayim":
self._render_root_level_object(fp, level, obj)
else:
super()._render_object(fp, level, obj)
if isinstance(obj, docspec.Class):
assert isinstance(obj.parent, docspec.Module)
module = import_module(obj.parent.name)
cls = getattr(module, obj.name)
resolver = MayimMarkdownReferenceResolver(global_=True)
parent_links = []
for base in cls.__mro__:
resolved = None
fullname = f"{base.__module__}.{base.__qualname__}"
if fullname.startswith("mayim") and base.__name__ != obj.name:
resolved = resolver.resolve_reference(
suite, obj, fullname, [docspec.Indirection]
)
if resolved:
module_name, member_name = self._resolve_name(
resolved
).rsplit(".", 1)
parent_links.append(
f"[{resolved.name}](./{module_name}.html"
f"#{member_name})"
)
if parent_links:
parents = ", ".join(parent_links)
fp.write(f"**Parents**: {parents}\n\n")
if (
obj.parent
and isinstance(obj.parent, docspec.Class)
and isinstance(obj, docspec.Variable)
):
if obj.value:
default = obj.value.replace("\n", "")
fp.write(f"**Default**: `{default}`\n\n")
def _render_root_level_object(
self, fp: TextIO, level: int, obj: docspec.Indirection
):
self._render_header(fp, level, obj)
full_name = "mayim" + obj.target
module_name, member_name = full_name.rsplit(".", 1)
module = list(docspec_python.load_python_modules([module_name]))[0]
item = docspec.get_member(module, member_name)
if item and item.docstring:
docstring = (
html.escape(item.docstring.content)
if self.escape_html_in_docstring
else item.docstring.content
)
lines = docstring.splitlines()
summary = ""
for line in lines:
if not line:
break
summary += f" {line}"
fp.write(f"{summary}\n\n")
fp.write("```{}\n".format("python" if self.code_lang else ""))
fp.write(f"from mayim import {member_name}")
fp.write("\n```\n\n")
fp.write(f"See [{full_name}](./{module_name}.html#{member_name})\n\n")
def _resolve_name(self, obj: docspec.ApiObject) -> str:
name = ""
if obj.path:
for part in obj.path:
if part is not self and part.name != obj.name:
name += self._resolve_name(part)
if not name:
return obj.name
name = f"{name}.{obj.name}"
if isinstance(obj, docspec.Module) and name.startswith("."):
name = f"mayim.{name}"
return name
def _render_toc(self, fp: TextIO, level: int, obj: docspec.ApiObject):
if level > self.toc_maxdepth:
return
title = self._slugify(self._get_title(obj))
display = self._escape(obj.name)
if not self.add_module_prefix and isinstance(obj, docspec.Module):
display = display.split(".")[-1]
fp.write(" " * level + "* [{}](#{})\n".format(display, title))
level += 1
for child in sorted(
getattr(obj, "members", []), key=attrgetter("name")
):
if not isinstance(child, docspec.Indirection):
self._render_toc(fp, level, child)
@staticmethod
def _slugify(text: str) -> str:
slug = slugify.sub("-", text.lower())
return dedup.sub("-", slug).strip("-")
@dataclass
class MayimMarkdownReferenceResolver(MarkdownReferenceResolver):
def resolve_reference(
self,
suite: ApiSuite,
scope: docspec.ApiObject,
ref: str,
exclusions: Optional[List[Type[docspec.ApiObject]]] = None,
) -> Optional[docspec.ApiObject]:
ref_split = ref.split(".")
resolved = self._resolve_local_reference(scope, ref_split)
if resolved and not self._excluded(resolved, exclusions):
return resolved
if self.global_:
def _recurse(
obj: docspec.ApiObject,
) -> Optional[docspec.ApiObject]:
resolved = self._resolve_reference_in_members(obj, ref_split)
if resolved and not self._excluded(resolved, exclusions):
return resolved
if isinstance(obj, docspec.HasMembers):
for member in obj.members:
resolved = _recurse(member)
if resolved and not self._excluded(
resolved, exclusions
):
return resolved
return None
for module in suite:
resolved = _recurse(module)
if resolved and not self._excluded(resolved, exclusions):
return resolved
return None
def _resolve_reference_in_members(
self, obj: Optional[docspec.ApiObject], ref: List[str]
) -> Optional[docspec.ApiObject]:
if not obj:
return None
fullref = ".".join(ref)
for part_name in ref:
retrieved = docspec.get_member(
obj, part_name
) or docspec.get_member(obj, fullref)
if retrieved:
return retrieved
return None
def _excluded(
self,
obj: docspec.ApiObject,
exclusions: Optional[List[Type[docspec.ApiObject]]] = None,
) -> bool:
if not exclusions:
return False
return (
any(isinstance(obj, exc) for exc in exclusions)
if exclusions
else False
)
class MayimFilter(FilterProcessor):
def _match(self, obj: docspec.ApiObject) -> bool:
result = super()._match(obj)
if result:
result = self._additional(obj)
return result
def _additional(self, obj: docspec.ApiObject) -> bool:
if isinstance(obj, docspec.Indirection) and not obj.target.startswith(
"."
):
return False
elif isinstance(obj, docspec.Variable) and obj.name.isupper():
return False
elif obj.name.startswith("_") and obj.name not in WHITELIST:
return False
return True
@dataclass
class MayimSourceLinker(SourceLinker):
def get_source_url(self, obj: docspec.ApiObject) -> Optional[str]:
base = "https://github.com/ahopkins/mayim/tree/main/"
return base + obj.location.filename + f"#L{obj.location.lineno}"
context = Context(directory=".")
loader = PythonLoader(
packages=["mayim"],
)
source_linker = MayimSourceLinker()
renderer = MayimRenderer(
render_module_header=False,
insert_header_anchors=False,
render_page_title=True,
descriptive_class_title=False,
signature_code_block=True,
use_fixed_header_levels=False,
source_linker=source_linker,
code_headers=True,
render_typehint_in_data_header=True,
signature_with_decorators=True,
render_toc=True,
)
source_linker.init(context)
loader.init(context)
renderer.init(context)
modules = list(sorted(loader.load(), key=attrgetter("name")))
suite = ApiSuite(modules)
resolver = renderer.get_resolver(modules)
CrossrefProcessor().process(modules, resolver)
GoogleProcessor().process(modules, resolver)
MayimFilter(
documented_only=False,
do_not_filter_modules=False,
skip_empty_modules=True,
).process(modules, resolver)
index_content = """
## Index
"""
page_links = []
for module in modules:
link = f"/api/{module.name}.md"
file_path = f"docs/src{link}"
with open(file_path, "w") as f:
renderer.render_single_page(f, [module], page_title=module.name)
if module.name != "mayim":
index_content += f"- [{module.name}](./{module.name}.html)\n"
page_links.append(link)
renderer.render_page_title = False
renderer.use_fixed_header_levels = True
renderer.render_toc = False
renderer.header_level_by_type["Indirection"] = 3
main_modules = [module for module in modules if module.name == "mayim"]
file_path = "docs/src/api/index.md"
with open(file_path, "w") as f:
f.write("# Mayim Package\n")
f.write("\n## Root objects\n\n")
renderer.render_single_page(f, main_modules)
f.write(index_content)
links = ",\n ".join([f"'{link}'" for link in page_links])
api_pages = f"""
module.exports = {{
apiPages: [
'/api/',
{links}
]
}}
"""
file_path = "docs/src/.vuepress/apiPages.js"
with open(file_path, "w") as f:
f.write(api_pages)