-
Notifications
You must be signed in to change notification settings - Fork 0
/
igraph_dash_docset.py
executable file
·283 lines (216 loc) · 8.43 KB
/
igraph_dash_docset.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
#!/usr/bin/env python
from pathlib import Path
from typing import Iterable, Optional, TypeVar
import logging
import json
import os
import shutil
import sqlite3
import re
import tarfile
import urllib.request
#: Tpye variable used in generic type annotations
T = TypeVar("T")
def first(it: Iterable[T], *, default: Optional[T] = None) -> Optional[T]:
"""Returns the first item of an iterable, or a default item if the iterable
yields no items.
"""
for item in it:
return item
return default
def download_release() -> Optional[str]:
"""Downloads the latest stable igraph release, and extracts the
documentation to the html directory.
Returns:
the version number of the downloaded release, or `None` if no release
was found
"""
logging.info("Looking for latest igraph release on GitHub ...")
with urllib.request.urlopen(
"https://api.github.com/repos/igraph/igraph/releases"
) as response:
data = json.load(response)
release = first(
entry for entry in data if not entry["prerelease"] and not entry["draft"]
)
if not release:
logging.error("No releases found on GitHub; this is probably a bug.")
return None
version = release["tag_name"]
logging.info(f"Found version {version}. Downloading ...")
tarball = release["assets"][0]["browser_download_url"]
with urllib.request.urlopen(tarball) as stream:
tarfile.open(fileobj=stream, mode="r:gz").extractall()
srcdir = Path(f"igraph-{version}")
htmldir = Path("html")
if htmldir.is_dir():
shutil.rmtree(htmldir)
shutil.move(str(srcdir / "doc" / "html"), ".")
shutil.rmtree(srcdir)
return version
def create_docset(docdir: str, docset_name: str = "igraph") -> None:
"""Creates a Dash docset from the igraph documentation in the given directory.
Parameters:
docdir: the directory where the source documentation is located
"""
docpath = Path(docdir)
logging.info("Creating docset ...")
# Create directory structure and put files in place
dsdir = Path(f"{docset_name}.docset")
assetdir = Path("assets")
contdir = dsdir / "Contents"
htmldir = contdir / "Resources" / "Documents"
if dsdir.is_dir():
logging.warning("Warning: Deleting existing docset.")
shutil.rmtree(dsdir)
htmldir.mkdir(parents=True, exist_ok=True)
for file in docpath.glob("*.*"):
shutil.copy(file, htmldir)
shutil.copy(assetdir / "Info.plist", contdir)
shutil.copy(assetdir / "icon.png", dsdir)
shutil.copy(assetdir / "[email protected]", dsdir)
# Set up SQLite index
with sqlite3.connect(contdir / "Resources" / "docSet.dsidx") as conn:
cur = conn.cursor()
create_index_from_igraph_documentation(htmldir, cur)
conn.commit()
def create_index_from_igraph_documentation(htmldir: Path, cur) -> None:
"""Parses igraph's HTML documentation from the given directory and inserts
approriate index entries into a freshly created SQLite3 database in Dash
format.
Parameters:
htmldir: the folder in which igraph's HTML documentation is to be found
cur: a database cursor used to execute SQL statements
"""
from bs4 import BeautifulSoup # type: ignore
from lxml.html import parse, tostring, fromstring # type: ignore
cur.execute(
"CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, "
"type TEXT, path TEXT);"
)
cur.execute("CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path);")
# Parse the index page from the igraph docs to find symbols
# Results will be stored into the docsysm dictionary
with (htmldir / "ix01.html").open() as fp:
page = fp.read()
soup = BeautifulSoup(page, features="lxml")
docsyms = {}
for tag in soup.find_all("dt"):
ch = list(tag.children)
name = ch[1].text.split()[0]
link = ch[1].attrs["href"].strip()
# This is a first guess about the symbol type; it will be refined later
if name.endswith("_t"):
kind = "Type"
elif "_rngtype_" in name:
kind = "Type"
else:
kind = "Function"
docsyms[name] = (name, kind, link)
# Update HTML files with information on which symbols they document
# Also refine symbol type guesses and add Sections/Guides
for file in htmldir.glob("igraph-*.html"):
tree = parse(str(file))
# Add sections / guides
title = tree.find("//h1[@class='title']")
if title is not None:
a = first(title.getchildren())
title_string = re.sub(r"^.*?\.\s*", "", title.text_content().strip())
name = title_string
if name in ["Installation", "Introduction", "Tutorial"]:
kind = "Guide"
else:
kind = "Section"
link = (
"<dash_entry_titleDescription=igraph>"
+ file.name
+ "#"
+ a.attrib["name"]
)
title.insert(
title.index(a) + 1,
fromstring(
"<a name='//apple_ref/cpp/%s/%s' class='dashAnchor' />"
% (kind, name)
),
)
cur.execute(
"INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES (?,?,?)",
(name, kind, link),
)
anchors = tree.findall("//a[@name]")
for a in anchors:
name = a.attrib["name"]
if name in docsyms:
_, kind, link = docsyms[name]
if title is not None:
link = (
"<dash_entry_titleDescription=%s>"
% urllib.request.quote(title_string)
+ link
)
# Parse the declaration of the symbol, if present, and refine
# the guess about its type.
pre = a.find("../../../../..//pre")
if pre is not None:
code = pre.text_content().strip()
if code.startswith("typedef enum"):
kind = "Enum"
elif code.startswith("typedef struct"):
kind = "Struct"
elif code.startswith("typedef"):
kind = "Type"
elif code.startswith("#define"):
kind = "Define"
docsyms[name] = (name, kind, link)
p = a.getparent()
p.insert(
p.index(a) + 1,
fromstring(
"<a name='//apple_ref/cpp/%s/%s' class='dashAnchor' />"
% (kind, name)
),
)
with file.open("bw") as htmlfile:
htmlfile.write(tostring(tree))
# Insert symbols into index
for triplet in docsyms.values():
cur.execute(
"INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES (?,?,?)",
triplet,
)
# print('name: %s, kind: %s, link: %s' % triplet)
def create_dash_submission(version: str, revision: int = 0) -> None:
"""Prepares a submission for https://github.com/Kapeli/Dash-User-Contributions.
The docset must be present in the current directory.
"""
from string import Template
logging.info("Creating Dash submission ...")
assetdir = Path("assets")
subdir = Path("submission")
if subdir.is_dir():
logging.warning("Warning: Deleting existing submission directory.")
shutil.rmtree(subdir)
subdir.mkdir(parents=True)
with open(assetdir / "docset.json", "r") as fp:
tem = Template(fp.read())
with (subdir / "docset.json").open("w") as fp:
fp.write(tem.substitute(version=version, revision=revision))
with tarfile.open("igraph.tgz", "w:gz") as tar:
tar.add("igraph.docset")
shutil.move("igraph.tgz", subdir)
shutil.copy("README.md", subdir)
shutil.copy(assetdir / "icon.png", subdir)
shutil.copy(assetdir / "[email protected]", subdir)
def main() -> None:
logging.basicConfig(format="%(message)s", level=logging.INFO)
os.chdir(os.path.dirname(os.path.realpath(__file__)))
version = download_release()
if version is None:
return
create_docset("html")
shutil.rmtree("html")
create_dash_submission(version)
logging.info("Done!")
if __name__ == "__main__":
main()