Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/src/sage_docbuild/ext/multidocs.py
6378 views
1
# -*- coding: utf-8 -*-
2
"""
3
Sage multidocs extension
4
5
The goal of this extension is to manage a multi-documentation in Sphinx. To be
6
able to compile Sage's huge documentation in parallel, the documentation is cut
7
into a bunch of independent documentations called "sub-docs", which are
8
compiled separately. There is a master document which points to all the
9
sub-docs. The intersphinx extension ensures that the cross-link between the
10
sub-docs are correctly resolved. However some work is needed to build a global
11
index. This is the goal of the ``multidocs`` extension.
12
13
More precisely this extension ensures the correct merging of
14
15
- the todo list if this extension is activated
16
- the python indexes
17
- the list of python modules
18
- the javascript index
19
- the citations
20
"""
21
import os
22
import pickle
23
import shutil
24
import sphinx
25
from sphinx.application import Sphinx
26
from sphinx.util.console import bold
27
from sage.env import SAGE_DOC
28
from pathlib import Path
29
30
logger = sphinx.util.logging.getLogger(__name__)
31
32
CITE_FILENAME = 'citations.pickle'
33
34
35
def merge_environment(app, env):
36
"""
37
Merge the following attributes of the sub-docs environment into the main
38
environment:
39
40
- ``titles`` -- Titles
41
- ``todo_all_todos`` -- todo's
42
- ``indexentries`` -- global python index
43
- ``all_docs`` -- needed by the js index
44
- ``citations`` -- citations
45
- ``domaindata['py']['modules']`` -- list of python modules
46
"""
47
logger.info(bold('Merging environment/index files...'))
48
for curdoc in app.env.config.multidocs_subdoc_list:
49
logger.info(" %s:" % curdoc, nonl=1)
50
docenv = get_env(app, curdoc)
51
if docenv is not None:
52
fixpath = lambda path: os.path.join(curdoc, path)
53
todos = docenv.domaindata['todo'].get('todos', dict())
54
citations = docenv.domaindata['citation'].get('citations', dict())
55
indexentries = docenv.domaindata['index'].get('entries', dict())
56
logger.info(" %s todos, %s index, %s citations" % (
57
sum(len(t) for t in todos.values()),
58
len(indexentries),
59
len(citations)
60
), nonl=1)
61
62
# merge titles
63
for t in docenv.titles:
64
env.titles[fixpath(t)] = docenv.titles[t]
65
# merge the todo links
66
for dct in todos:
67
env.domaindata['todo']['todos'][fixpath(dct)] = todos[dct]
68
# merge the html index links
69
newindex = {}
70
for ind in indexentries:
71
if ind.startswith('sage/'):
72
newindex[fixpath(ind)] = indexentries[ind]
73
else:
74
newindex[ind] = indexentries[ind]
75
env.domaindata['index']['entries'].update(newindex)
76
# merge the all_docs links, needed by the js index
77
newalldoc = {}
78
for ind in docenv.all_docs:
79
newalldoc[fixpath(ind)] = docenv.all_docs[ind]
80
env.all_docs.update(newalldoc)
81
# needed by env.check_consistency (sphinx.environment, line 1734)
82
for ind in newalldoc:
83
# treat sub-document source as orphaned file and don't complain
84
md = env.metadata.get(ind, dict())
85
md['orphan'] = 1
86
env.metadata[ind] = md
87
# merge the citations
88
newcite = {}
89
for ind, (path, tag, lineno) in citations.items():
90
# TODO: Warn on conflicts
91
newcite[ind] = (fixpath(path), tag, lineno)
92
env.domaindata['citation']['citations'].update(newcite)
93
# merge the py:module indexes
94
newmodules = {}
95
from sphinx.domains.python import ModuleEntry
96
for ind,mod in docenv.domaindata['py']['modules'].items():
97
newmodules[ind] = ModuleEntry(fixpath(mod.docname), mod.node_id, mod.synopsis, mod.platform, mod.deprecated)
98
env.domaindata['py']['modules'].update(newmodules)
99
logger.info(", %s modules" % (len(newmodules)))
100
logger.info('... done (%s todos, %s index, %s citations, %s modules)' % (
101
sum(len(t) for t in env.domaindata['todo']['todos'].values()),
102
len(env.domaindata['index']['entries']),
103
len(env.domaindata['citation']['citations']),
104
len(env.domaindata['py']['modules'])))
105
write_citations(app, env.domaindata['citation']['citations'])
106
107
108
def get_env(app, curdoc):
109
"""
110
Get the environment of a sub-doc from the pickle
111
"""
112
from sphinx.application import ENV_PICKLE_FILENAME
113
filename = os.path.join(
114
app.env.doctreedir, curdoc, ENV_PICKLE_FILENAME)
115
try:
116
f = open(filename, 'rb')
117
except OSError:
118
logger.debug(f"Unable to load pickled environment '{filename}'", exc_info=True)
119
return None
120
docenv = pickle.load(f)
121
f.close()
122
return docenv
123
124
125
def merge_js_index(app):
126
"""
127
Merge the JS indexes of the sub-docs into the main JS index
128
"""
129
logger.info('')
130
logger.info(bold('Merging js index files...'))
131
mapping = app.builder.indexer._mapping
132
for curdoc in app.env.config.multidocs_subdoc_list:
133
logger.info(" %s:" % curdoc, nonl=1)
134
fixpath = lambda path: os.path.join(curdoc, path)
135
index = get_js_index(app, curdoc)
136
if index is not None:
137
# merge the mappings
138
logger.info(" %s js index entries" % (len(index._mapping)))
139
for (ref, locs) in index._mapping.items():
140
newmapping = set(map(fixpath, locs))
141
if ref in mapping:
142
newmapping = mapping[ref] | newmapping
143
mapping[str(ref)] = newmapping
144
# merge the titles
145
titles = app.builder.indexer._titles
146
for (res, title) in index._titles.items():
147
titles[fixpath(res)] = title
148
# merge the alltitles
149
alltitles = app.builder.indexer._all_titles
150
for (res, alltitle) in index._all_titles.items():
151
alltitles[fixpath(res)] = alltitle
152
# merge the filenames
153
filenames = app.builder.indexer._filenames
154
for (res, filename) in index._filenames.items():
155
filenames[fixpath(res)] = fixpath(filename)
156
# TODO: merge indexer._objtypes, indexer._objnames as well
157
158
# Setup source symbolic links
159
dest = os.path.join(app.outdir, "_sources", curdoc)
160
if not os.path.exists(dest):
161
os.symlink(os.path.join("..", curdoc, "_sources"), dest)
162
logger.info('... done (%s js index entries)' % (len(mapping)))
163
logger.info(bold('Writing js search indexes...'), nonl=1)
164
return [] # no extra page to setup
165
166
167
def get_js_index(app, curdoc):
168
"""
169
Get the JS index of a sub-doc from the file
170
"""
171
from sphinx.search import IndexBuilder, languages
172
# FIXME: find the correct lang
173
sphinx_version = __import__("sphinx").__version__
174
if (sphinx_version < '1.2'):
175
indexer = IndexBuilder(app.env, 'en',
176
app.config.html_search_options)
177
else:
178
indexer = IndexBuilder(app.env, 'en',
179
app.config.html_search_options, scoring=None)
180
indexfile = os.path.join(app.outdir, curdoc, 'searchindex.js')
181
try:
182
f = open(indexfile)
183
except OSError:
184
logger.info("")
185
logger.warning("Unable to fetch %s " % indexfile)
186
return None
187
indexer.load(f, sphinx.search.js_index)
188
f.close()
189
return indexer
190
191
192
mustbefixed = ['search', 'genindex', 'genindex-all',
193
'py-modindex', 'searchindex.js']
194
195
196
def fix_path_html(app, pagename, templatename, ctx, event_arg):
197
"""
198
Fix the context so that the files
199
200
- :file:`search.html`
201
- :file:`genindex.html`
202
- :file:`py-modindex.html`
203
204
point to the right place, that is in :file:`reference/` instead of
205
:file:`reference/subdocument`.
206
"""
207
# sphinx/builder/html.py line 702
208
# def pathto(otheruri, resource=False,
209
# baseuri=self.get_target_uri(pagename)):
210
old_pathto = ctx['pathto']
211
212
def sage_pathto(otheruri, *args, **opts):
213
if otheruri in mustbefixed:
214
otheruri = os.path.join("..", otheruri)
215
return old_pathto(otheruri, *args, **opts)
216
ctx['pathto'] = sage_pathto
217
218
219
def citation_dir(app: Sphinx) -> Path:
220
outdir = Path(app.outdir).resolve()
221
sage_doc = Path(SAGE_DOC).resolve()
222
if sage_doc in outdir.parents:
223
# Split app.outdir in 3 parts: SAGE_DOC/TYPE/TAIL where TYPE
224
# is a single directory and TAIL can contain multiple directories.
225
# The citation dir is then SAGE_DOC/inventory/TAIL.
226
rel = outdir.relative_to(sage_doc)
227
dirs = list(rel.parts)
228
# If SAGE_DOC does not end with a slash, rel will start with
229
# a slash. Remove this:
230
if dirs[0] == '/':
231
dirs.pop(0)
232
tail = dirs[1:]
233
citedir = (sage_doc / "inventory").joinpath(*tail)
234
else:
235
citedir = outdir / "inventory"
236
os.makedirs(citedir, exist_ok=True)
237
return citedir
238
239
240
def write_citations(app: Sphinx, citations):
241
"""
242
Pickle the citation in a file.
243
"""
244
from sage.misc.temporary_file import atomic_write
245
outdir = citation_dir(app)
246
with atomic_write(outdir / CITE_FILENAME, binary=True) as f:
247
pickle.dump(citations, f)
248
logger.info("Saved pickle file: %s" % CITE_FILENAME)
249
250
251
def fetch_citation(app: Sphinx, env):
252
"""
253
Fetch the global citation index from the refman to allow for cross
254
references.
255
"""
256
logger.info(bold('loading cross citations... '), nonl=1)
257
file = citation_dir(app).parent / CITE_FILENAME
258
if not file.is_file():
259
return
260
with open(file, 'rb') as f:
261
cache = pickle.load(f)
262
logger.info("done (%s citations)." % len(cache))
263
cite = env.domaindata['citation'].get('citations', dict())
264
for ind, (path, tag, lineno) in cache.items():
265
if ind not in cite: # don't override local citation
266
cite[ind] = (os.path.join("..", path), tag, lineno)
267
268
269
def init_subdoc(app):
270
"""
271
Init the merger depending on if we are compiling a sub-doc or the master
272
doc itself.
273
"""
274
if app.config.multidocs_is_master:
275
logger.info(bold("Compiling the master document"))
276
app.connect('env-updated', merge_environment)
277
app.connect('html-collect-pages', merge_js_index)
278
if app.config.multidocs_subdoc_list:
279
# Master file with indexes computed by merging indexes:
280
# Monkey patch index fetching to silence warning about broken index
281
def load_indexer(docnames):
282
logger.info(bold('skipping loading of indexes... '), nonl=1)
283
app.builder.load_indexer = load_indexer
284
285
else:
286
logger.info(bold("Compiling a sub-document"))
287
app.connect('html-page-context', fix_path_html)
288
if not app.config.multidoc_first_pass:
289
app.connect('env-updated', fetch_citation)
290
291
# Monkey patch copy_static_files to make a symlink to "../"
292
def link_static_files():
293
"""
294
Instead of copying static files, make a link to the master static file.
295
See sphinx/builder/html.py line 536::
296
297
class StandaloneHTMLBuilder(Builder):
298
[...]
299
def copy_static_files(self):
300
[...]
301
"""
302
logger.info(bold('linking _static directory.'))
303
static_dir = os.path.join(app.builder.outdir, '_static')
304
master_static_dir = os.path.join('..', '_static')
305
if os.path.lexists(static_dir):
306
try:
307
shutil.rmtree(static_dir)
308
except OSError:
309
os.unlink(static_dir)
310
# This ensures that the symlink we are creating points to an
311
# existing directory. See trac #33608.
312
os.makedirs(os.path.join(app.builder.outdir, master_static_dir),
313
exist_ok=True)
314
os.symlink(master_static_dir, static_dir)
315
316
app.builder.copy_static_files = link_static_files
317
318
if app.config.multidoc_first_pass == 1:
319
app.config.intersphinx_mapping = {}
320
else:
321
app.emit('env-check-consistency', app.env)
322
323
324
def setup(app: Sphinx):
325
app.add_config_value('multidocs_is_master', True, True)
326
app.add_config_value('multidocs_subdoc_list', [], True)
327
app.add_config_value('multidoc_first_pass', 0, False) # 1 = deactivate the loading of the inventory
328
app.connect('builder-inited', init_subdoc)
329
return {'parallel_read_safe': True}
330
331