Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/doc/common/builder.py
8815 views
1
#!/usr/bin/env python
2
"""
3
The documentation builder
4
5
It is the starting point for building documentation, and is
6
responsible to figure out what to build and with which options. The
7
actual documentation build for each individual document is then done
8
in a subprocess call to sphinx, see :func:`builder_helper`.
9
10
* The builder can be configured in build_options.py
11
* The sphinx subprocesses are configured in conf.py
12
"""
13
14
import logging, optparse, os, shutil, subprocess, sys, re
15
16
import sphinx.cmdline
17
import sphinx.util.console
18
import sphinx.ext.intersphinx
19
20
#We remove the current directory from sys.path right away
21
#so that we import sage from the proper spot
22
try:
23
sys.path.remove(os.path.realpath(os.getcwd()))
24
except ValueError:
25
pass
26
27
from sage.misc.cachefunc import cached_method
28
from sage.misc.misc import sage_makedirs as mkdir
29
from sage.env import SAGE_DOC, SAGE_SRC
30
31
# Load the options, including
32
# SAGE_DOC, LANGUAGES, SPHINXOPTS, PAPER, OMIT,
33
# PAPEROPTS, ALLSPHINXOPTS, NUM_THREADS, WEBSITESPHINXOPTS
34
# from build_options.py.
35
execfile(os.path.join(SAGE_DOC, 'common' , 'build_options.py'))
36
37
38
##########################################
39
# Parallel Building Ref Manual #
40
##########################################
41
def build_ref_doc(args):
42
doc = args[0]
43
lang = args[1]
44
format = args[2]
45
kwds = args[3]
46
args = args[4:]
47
if format == 'inventory': # you must not use the inventory to build the inventory
48
kwds['use_multidoc_inventory'] = False
49
getattr(ReferenceSubBuilder(doc, lang), format)(*args, **kwds)
50
51
##########################################
52
# Builders #
53
##########################################
54
55
def builder_helper(type):
56
"""
57
Returns a function which builds the documentation for
58
output type type.
59
"""
60
def f(self, *args, **kwds):
61
output_dir = self._output_dir(type)
62
63
options = ALLSPHINXOPTS
64
65
if self.name == 'website':
66
# WEBSITESPHINXOPTS is either empty or " -A hide_pdf_links=1 "
67
options += WEBSITESPHINXOPTS
68
69
if kwds.get('use_multidoc_inventory', True):
70
options += ' -D multidoc_first_pass=0'
71
else:
72
options += ' -D multidoc_first_pass=1'
73
74
build_command = '-b %s -d %s %s %s %s'%(type, self._doctrees_dir(),
75
options, self.dir,
76
output_dir)
77
logger.debug(build_command)
78
79
# Execute custom-sphinx-build.py
80
sys.argv = [os.path.join(SAGE_DOC, 'common', 'custom-sphinx-build.py')]
81
sys.argv.extend(build_command.split())
82
try:
83
execfile(sys.argv[0])
84
except Exception:
85
import traceback
86
logger.error(traceback.format_exc())
87
raise
88
89
# Print message about location of output:
90
# - by default if html output
91
# - if verbose and if not pdf output
92
# - if pdf: print custom message here if verbose, and print
93
# full message below (see pdf method) after 'make all-pdf'
94
# is done running
95
96
if 'output/html' in output_dir:
97
logger.warning("Build finished. The built documents can be found in %s",
98
output_dir)
99
elif 'output/pdf' not in output_dir:
100
logger.info("Build finished. The built documents can be found in %s",
101
output_dir)
102
else:
103
logger.info("LaTeX file written to %s; now making PDF.",
104
output_dir)
105
106
f.is_output_format = True
107
return f
108
109
110
class DocBuilder(object):
111
def __init__(self, name, lang='en'):
112
"""
113
INPUT:
114
115
- ``name`` - the name of a subdirectory in SAGE_DOC, such as
116
'tutorial' or 'bordeaux_2008'
117
118
- ``lang`` - (default "en") the language of the document.
119
"""
120
doc = name.split(os.path.sep)
121
122
if doc[0] in LANGUAGES:
123
lang = doc[0]
124
doc.pop(0)
125
126
self.name = os.path.join(*doc)
127
self.lang = lang
128
self.dir = os.path.join(SAGE_DOC, self.lang, self.name)
129
130
#Make sure the .static and .templates directories are there
131
mkdir(os.path.join(self.dir, "static"))
132
mkdir(os.path.join(self.dir, "templates"))
133
134
def _output_dir(self, type):
135
"""
136
Returns the directory where the output of type type is stored.
137
If the directory does not exist, then it will automatically be
138
created.
139
140
EXAMPLES::
141
142
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
143
sage: b = builder.DocBuilder('tutorial')
144
sage: b._output_dir('html')
145
'.../doc/output/html/en/tutorial'
146
"""
147
d = os.path.join(SAGE_DOC, "output", type, self.lang, self.name)
148
mkdir(d)
149
return d
150
151
def _doctrees_dir(self):
152
"""
153
Returns the directory where the doctrees are stored. If the
154
directory does not exist, then it will automatically be
155
created.
156
157
EXAMPLES::
158
159
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
160
sage: b = builder.DocBuilder('tutorial')
161
sage: b._doctrees_dir()
162
'.../doc/output/doctrees/en/tutorial'
163
"""
164
d = os.path.join(SAGE_DOC, "output", 'doctrees', self.lang, self.name)
165
mkdir(d)
166
return d
167
168
def _output_formats(self):
169
"""
170
Returns a list of the possible output formats.
171
172
EXAMPLES::
173
174
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
175
sage: b = builder.DocBuilder('tutorial')
176
sage: b._output_formats()
177
['changes', 'html', 'htmlhelp', 'inventory', 'json', 'latex', 'linkcheck', 'pickle', 'web']
178
179
"""
180
#Go through all the attributes of self and check to
181
#see which ones have an 'is_output_format' attribute. These
182
#are the ones created with builder_helper.
183
output_formats = []
184
for attr in dir(self):
185
if hasattr(getattr(self, attr), 'is_output_format'):
186
output_formats.append(attr)
187
output_formats.sort()
188
return output_formats
189
190
def pdf(self):
191
"""
192
Builds the PDF files for this document. This is done by first
193
(re)-building the LaTeX output, going into that LaTeX
194
directory, and running 'make all-pdf' there.
195
196
EXAMPLES::
197
198
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
199
sage: b = builder.DocBuilder('tutorial')
200
sage: b.pdf() #not tested
201
"""
202
self.latex()
203
tex_dir = self._output_dir('latex')
204
pdf_dir = self._output_dir('pdf')
205
if subprocess.call("cd '%s' && $MAKE all-pdf && mv -f *.pdf '%s'"%(tex_dir, pdf_dir), shell=True):
206
raise RuntimeError("failed to run $MAKE all-pdf in %s"%tex_dir)
207
208
logger.warning("Build finished. The built documents can be found in %s", pdf_dir)
209
210
def clean(self, *args):
211
shutil.rmtree(self._doctrees_dir())
212
output_formats = list(args) if args else self._output_formats()
213
for format in output_formats:
214
shutil.rmtree(self._output_dir(format), ignore_errors=True)
215
216
html = builder_helper('html')
217
pickle = builder_helper('pickle')
218
web = pickle
219
json = builder_helper('json')
220
htmlhelp = builder_helper('htmlhelp')
221
latex = builder_helper('latex')
222
changes = builder_helper('changes')
223
linkcheck = builder_helper('linkcheck')
224
# import the customized builder for object.inv files
225
inventory = builder_helper('inventory')
226
227
##########################################
228
# Parallel Building Ref Manual #
229
##########################################
230
def build_other_doc(args):
231
document = args[0]
232
name = args[1]
233
kwds = args[2]
234
args = args[3:]
235
logger.warning("\nBuilding %s.\n" % document)
236
getattr(get_builder(document), name)(*args, **kwds)
237
238
class AllBuilder(object):
239
"""
240
A class used to build all of the documentation.
241
"""
242
def __getattr__(self, attr):
243
"""
244
For any attributes not explicitly defined, we just go through
245
all of the documents and call their attr. For example,
246
'AllBuilder().json()' will go through all of the documents
247
and call the json() method on their builders.
248
"""
249
from functools import partial
250
return partial(self._wrapper, attr)
251
252
def _wrapper(self, name, *args, **kwds):
253
"""
254
This is the function which goes through all of the documents
255
and does the actual building.
256
"""
257
import time
258
start = time.time()
259
docs = self.get_all_documents()
260
refs = [x for x in docs if x.endswith('reference')]
261
others = [x for x in docs if not x.endswith('reference')]
262
263
# Build the reference manual twice to resolve references. That is,
264
# build once with the inventory builder to construct the intersphinx
265
# inventory files, and then build the second time for real. So the
266
# first build should be as fast as possible;
267
logger.warning("\nBuilding reference manual, first pass.\n")
268
for document in refs:
269
getattr(get_builder(document), 'inventory')(*args, **kwds)
270
271
logger.warning("Building reference manual, second pass.\n")
272
for document in refs:
273
getattr(get_builder(document), name)(*args, **kwds)
274
275
# build the other documents in parallel
276
from multiprocessing import Pool
277
pool = Pool(NUM_THREADS, maxtasksperchild=1)
278
L = [(doc, name, kwds) + args for doc in others]
279
# map_async handles KeyboardInterrupt correctly. Plain map and
280
# apply_async does not, so don't use it.
281
x = pool.map_async(build_other_doc, L, 1)
282
try:
283
x.get(99999)
284
pool.close()
285
pool.join()
286
except Exception:
287
pool.terminate()
288
logger.error('Error building the documentation.')
289
if INCREMENTAL_BUILD:
290
logger.error('''
291
Note: incremental documentation builds sometimes cause spurious
292
error messages. To be certain that these are real errors, run
293
"make doc-clean" first and try again.''')
294
raise
295
logger.warning("Elapsed time: %.1f seconds."%(time.time()-start))
296
logger.warning("Done building the documentation!")
297
298
def get_all_documents(self):
299
"""
300
Returns a list of all of the documents. A document is a directory within one of
301
the language subdirectories of SAGE_DOC specified by the global LANGUAGES
302
variable.
303
304
EXAMPLES::
305
306
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
307
sage: documents = builder.AllBuilder().get_all_documents()
308
sage: 'en/tutorial' in documents
309
True
310
sage: documents[0] == 'en/reference'
311
True
312
"""
313
documents = []
314
for lang in LANGUAGES:
315
for document in os.listdir(os.path.join(SAGE_DOC, lang)):
316
if (document not in OMIT
317
and os.path.isdir(os.path.join(SAGE_DOC, lang, document))):
318
documents.append(os.path.join(lang, document))
319
320
# Ensure that the reference guide is compiled first so that links from
321
# the other document to it are correctly resolved.
322
if 'en/reference' in documents:
323
documents.remove('en/reference')
324
documents.insert(0, 'en/reference')
325
326
return documents
327
328
329
class WebsiteBuilder(DocBuilder):
330
def html(self):
331
"""
332
After we've finished building the website index page, we copy
333
everything one directory up. Then we call
334
:meth:`create_html_redirects`.
335
"""
336
DocBuilder.html(self)
337
html_output_dir = self._output_dir('html')
338
for f in os.listdir(html_output_dir):
339
src = os.path.join(html_output_dir, f)
340
dst = os.path.join(html_output_dir, '..', f)
341
if os.path.isdir(src):
342
shutil.rmtree(dst, ignore_errors=True)
343
shutil.copytree(src, dst)
344
else:
345
shutil.copy2(src, dst)
346
self.create_html_redirects()
347
348
def create_html_redirects(self):
349
"""
350
Writes a number of small HTML files; these are files which
351
used to contain the main content of the reference manual
352
before before splitting the manual into multiple
353
documents. After the split, those files have moved, so in each
354
old location, write a file which redirects to the new version.
355
(This is so old URLs to pieces of the reference manual still
356
open the correct files.)
357
"""
358
# The simple html template which will cause a redirect to the
359
# correct file
360
html_template = """<html><head>
361
<meta HTTP-EQUIV="REFRESH" content="0; url=%s">
362
</head><body></body></html>"""
363
364
reference_dir = os.path.abspath(os.path.join(self._output_dir('html'),
365
'..', 'reference'))
366
reference_builder = ReferenceBuilder('reference')
367
refdir = os.path.join(os.environ['SAGE_DOC'], 'en', 'reference')
368
for document in reference_builder.get_all_documents(refdir):
369
#path is the directory above reference dir
370
path = os.path.abspath(os.path.join(reference_dir, '..'))
371
372
# the name of the subdocument
373
document_name = document.split('/')[1]
374
375
# the sage directory within a subdocument, for example
376
# /path/to/.../output/html/en/reference/algebras/sage
377
sage_directory = os.path.join(path, document, 'sage')
378
379
# Walk through all of the files in the sage_directory
380
for dirpath, dirnames, filenames in os.walk(sage_directory):
381
# a string like reference/algebras/sage/algebras
382
short_path = dirpath[len(path)+1:]
383
384
# a string like sage/algebras
385
shorter_path = os.path.join(*short_path.split(os.sep)[2:])
386
387
#Make the shorter path directory
388
try:
389
os.makedirs(os.path.join(reference_dir, shorter_path))
390
except OSError:
391
pass
392
393
for filename in filenames:
394
if not filename.endswith('html'):
395
continue
396
397
# the name of the html file we are going to create
398
redirect_filename = os.path.join(reference_dir, shorter_path, filename)
399
400
# the number of levels up we need to use in the relative url
401
levels_up = len(shorter_path.split(os.sep))
402
403
# the relative url that we will redirect to
404
redirect_url = "/".join(['..']*levels_up + [document_name, shorter_path, filename])
405
406
# write the html file which performs the redirect
407
with open(redirect_filename, 'w') as f:
408
f.write(html_template % redirect_url)
409
410
411
def clean(self):
412
"""
413
When we clean the output for the website index, we need to
414
remove all of the HTML that were placed in the parent
415
directory.
416
"""
417
html_output_dir = self._output_dir('html')
418
parent_dir = os.path.realpath(os.path.join(html_output_dir, '..'))
419
for filename in os.listdir(html_output_dir):
420
parent_filename = os.path.join(parent_dir, filename)
421
if not os.path.exists(parent_filename):
422
continue
423
if os.path.isdir(parent_filename):
424
shutil.rmtree(parent_filename, ignore_errors=True)
425
else:
426
os.unlink(parent_filename)
427
428
DocBuilder.clean(self)
429
430
431
class ReferenceBuilder(AllBuilder):
432
"""
433
This class builds the reference manual. It uses DocBuilder to
434
build the top-level page and ReferenceSubBuilder for each
435
sub-component.
436
"""
437
def __init__(self, name, lang='en'):
438
"""
439
Records the reference manual's name, in case it's not
440
identical to 'reference'.
441
"""
442
AllBuilder.__init__(self)
443
doc = name.split(os.path.sep)
444
445
if doc[0] in LANGUAGES:
446
lang = doc[0]
447
doc.pop(0)
448
449
self.name = doc[0]
450
self.lang = lang
451
452
def _output_dir(self, type, lang='en'):
453
"""
454
Returns the directory where the output of type type is stored.
455
If the directory does not exist, then it will automatically be
456
created.
457
458
EXAMPLES::
459
460
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
461
sage: b = builder.ReferenceBuilder('reference')
462
sage: b._output_dir('html')
463
'.../doc/output/html/en/reference'
464
"""
465
d = os.path.join(SAGE_DOC, "output", type, lang, self.name)
466
mkdir(d)
467
return d
468
469
def _wrapper(self, format, *args, **kwds):
470
"""
471
Builds reference manuals. For each language, it builds the
472
top-level document and its components.
473
"""
474
for lang in LANGUAGES:
475
refdir = os.path.join(SAGE_DOC, lang, self.name)
476
if not os.path.exists(refdir):
477
continue
478
output_dir = self._output_dir(format, lang)
479
from multiprocessing import Pool
480
pool = Pool(NUM_THREADS, maxtasksperchild=1)
481
L = [(doc, lang, format, kwds) + args for doc in self.get_all_documents(refdir)]
482
# (See comment in AllBuilder._wrapper about using map instead of apply.)
483
x = pool.map_async(build_ref_doc, L, 1)
484
try:
485
x.get(99999)
486
pool.close()
487
pool.join()
488
except Exception:
489
pool.terminate()
490
logger.error('Error building the documentation.')
491
if INCREMENTAL_BUILD:
492
logger.error('''
493
Note: incremental documentation builds sometimes cause spurious
494
error messages. To be certain that these are real errors, run
495
"make doc-clean" first and try again.''')
496
raise
497
# The html refman must be build at the end to ensure correct
498
# merging of indexes and inventories.
499
# Sphinx is run here in the current process (not in a
500
# subprocess) and the IntersphinxCache gets populated to be
501
# used for the second pass of the reference manual and for
502
# the other documents.
503
getattr(DocBuilder(self.name, lang), format)(*args, **kwds)
504
505
# PDF: we need to build master index file which lists all
506
# of the PDF file. So we create an html file, based on
507
# the file index.html from the "website" target.
508
if format == 'pdf':
509
# First build the website page. (This only takes a
510
# few seconds.)
511
getattr(get_builder('website'), 'html')()
512
# Copy the relevant pieces of
513
# output/html/en/website/_static to output_dir.
514
# (Don't copy all of _static to save some space: we
515
# don't need all of the MathJax stuff, and in
516
# particular we don't need the fonts.)
517
website_dir = os.path.join(SAGE_DOC, 'output', 'html',
518
'en', 'website')
519
static_files = ['COPYING.txt', 'basic.css', 'blank.gif',
520
'default.css', 'doctools.js', 'favicon.ico',
521
'file.png', 'jquery.js', 'minus.png',
522
'pdf.png', 'plus.png', 'pygments.css',
523
'sage.css', 'sageicon.png', 'sagelogo.png',
524
'searchtools.js', 'sidebar.js', 'underscore.js']
525
mkdir(os.path.join(output_dir, '_static'))
526
for f in static_files:
527
try:
528
shutil.copyfile(os.path.join(website_dir, '_static', f),
529
os.path.join(output_dir, '_static', f))
530
except IOError: # original file does not exist
531
pass
532
# Now modify website's index.html page and write it
533
# to output_dir.
534
f = open(os.path.join(website_dir, 'index.html'))
535
html = f.read().replace('Documentation', 'Reference')
536
f.close()
537
html_output_dir = os.path.dirname(website_dir)
538
html = html.replace('http://www.sagemath.org',
539
os.path.join(html_output_dir, 'index.html'))
540
# From index.html, we want the preamble and the tail.
541
html_end_preamble = html.find('<h1>Sage Reference')
542
html_bottom = html.rfind('</table>') + len('</table>')
543
# For the content, we modify doc/en/reference/index.rst,
544
# which has two parts: the body and the table of contents.
545
f = open(os.path.join(SAGE_DOC, lang, 'reference', 'index.rst'))
546
rst = f.read()
547
f.close()
548
# Replace rst links with html links. There are two forms:
549
#
550
# `blah`__ followed by __ LINK
551
#
552
# :doc:`blah <module/index>`
553
#
554
# Change the first form to
555
#
556
# <a href="LINK">blah</a>
557
#
558
# Change the second form to
559
#
560
# <a href="module/module.pdf">blah <img src="_static/pdf.png" /></a>
561
#
562
rst = re.sub('`([^`]*)`__\.\n\n__ (.*)',
563
r'<a href="\2">\1</a>.', rst)
564
rst = re.sub(r':doc:`([^<]*?)\s+<(.*)/index>`',
565
r'<a href="\2/\2.pdf">\1 <img src="_static/pdf.png" /></a>',
566
rst)
567
# Get rid of todolist and miscellaneous rst markup.
568
rst = rst.replace('.. toctree::', '')
569
rst = rst.replace(':maxdepth: 2', '')
570
rst = rst.replace('todolist', '')
571
start = rst.find('=\n') + 1
572
end = rst.find('Table of Contents')
573
# Body: add paragraph <p> markup.
574
rst_body = rst[start:end]
575
rst_body = rst_body.replace('\n\n', '</p>\n<p>')
576
start = rst.find('Table of Contents') + 2*len('Table of Contents') + 1
577
# Don't include the indices.
578
end = rst.find('Indices and Tables')
579
# TOC: change * to <li>, change rst headers to html headers.
580
rst_toc = rst[start:end]
581
rst_toc = rst_toc.replace('*', '<li>')
582
rst_toc = re.sub('\n([A-Z][a-zA-Z, ]*)\n-*\n',
583
'</ul>\n\n\n<h2>\\1</h2>\n\n<ul>\n', rst_toc)
584
# Now write the file.
585
new_index = open(os.path.join(output_dir, 'index.html'), 'w')
586
new_index.write(html[:html_end_preamble])
587
new_index.write('<h1>' + rst[:rst.find('\n')] +
588
' (PDF version)'+ '</h1>')
589
new_index.write(rst_body)
590
new_index.write('<h2>Table of Contents</h2>\n\n<ul>')
591
new_index.write(rst_toc)
592
new_index.write('</ul>\n\n')
593
new_index.write(html[html_bottom:])
594
new_index.close()
595
logger.warning('''
596
PDF documents have been created in subdirectories of
597
598
%s
599
600
Alternatively, you can open
601
602
%s
603
604
for a webpage listing all of the documents.''' % (output_dir,
605
os.path.join(output_dir,
606
'index.html')))
607
608
def get_all_documents(self, refdir):
609
"""
610
Returns a list of all reference manual components to build.
611
We add a component name if it's a subdirectory of the manual's
612
directory and contains a file named 'index.rst'.
613
614
We return the largest component (most subdirectory entries)
615
first since they will take the longest to build.
616
617
EXAMPLES::
618
619
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
620
sage: b = builder.ReferenceBuilder('reference')
621
sage: refdir = os.path.join(os.environ['SAGE_DOC'], 'en', b.name)
622
sage: sorted(b.get_all_documents(refdir))
623
['reference/algebras', 'reference/arithgroup', ..., 'reference/tensor']
624
"""
625
documents = []
626
627
for doc in os.listdir(refdir):
628
directory = os.path.join(refdir, doc)
629
if os.path.exists(os.path.join(directory, 'index.rst')):
630
n = len(os.listdir(directory))
631
documents.append((-n, os.path.join(self.name, doc)))
632
633
return [ doc[1] for doc in sorted(documents) ]
634
635
636
class ReferenceSubBuilder(DocBuilder):
637
"""
638
This class builds sub-components of the reference manual. It is
639
resposible for making sure the auto generated ReST files for the
640
Sage library are up to date.
641
642
When building any output, we must first go through and check
643
to see if we need to update any of the autogenerated ReST
644
files. There are two cases where this would happen:
645
646
1. A new module gets added to one of the toctrees.
647
648
2. The actual module gets updated and possibly contains a new
649
title.
650
"""
651
def __init__(self, *args, **kwds):
652
DocBuilder.__init__(self, *args, **kwds)
653
self._wrap_builder_helpers()
654
655
def _wrap_builder_helpers(self):
656
from functools import partial, update_wrapper
657
for attr in dir(self):
658
if hasattr(getattr(self, attr), 'is_output_format'):
659
f = partial(self._wrapper, attr)
660
f.is_output_format = True
661
update_wrapper(f, getattr(self, attr))
662
setattr(self, attr, f)
663
664
def _wrapper(self, build_type, *args, **kwds):
665
"""
666
This is the wrapper around the builder_helper methods that
667
goes through and makes sure things are up to date.
668
"""
669
# Delete the auto-generated .rst files, if the inherited
670
# and/or underscored members options have changed.
671
global options
672
inherit_prev = self.get_cache().get('option_inherited')
673
underscore_prev = self.get_cache().get('option_underscore')
674
if (inherit_prev is None or inherit_prev != options.inherited or
675
underscore_prev is None or underscore_prev != options.underscore):
676
logger.info("Detected change(s) in inherited and/or underscored members option(s).")
677
self.clean_auto()
678
self.get_cache.clear_cache()
679
680
# After "sage -clone", refresh the .rst file mtimes in
681
# environment.pickle.
682
if options.update_mtimes:
683
logger.info("Checking for .rst file mtimes to update...")
684
self.update_mtimes()
685
686
#Update the .rst files for modified Python modules
687
logger.info("Updating .rst files with modified modules...")
688
for module_name in self.get_modified_modules():
689
self.write_auto_rest_file(module_name.replace(os.path.sep, '.'))
690
691
#Write the .rst files for newly included modules
692
logger.info("Writing .rst files for newly-included modules...")
693
for module_name in self.get_newly_included_modules(save=True):
694
self.write_auto_rest_file(module_name)
695
696
#Copy over the custom .rst files from _sage
697
_sage = os.path.join(self.dir, '_sage')
698
if os.path.exists(_sage):
699
logger.info("Copying over custom .rst files from %s ...", _sage)
700
shutil.copytree(_sage, os.path.join(self.dir, 'sage'))
701
702
getattr(DocBuilder, build_type)(self, *args, **kwds)
703
704
def cache_filename(self):
705
"""
706
Returns the filename where the pickle of the dictionary of
707
already generated ReST files is stored.
708
"""
709
return os.path.join(self._doctrees_dir(), 'reference.pickle')
710
711
@cached_method
712
def get_cache(self):
713
"""
714
Retrieve the cache of already generated ReST files. If it
715
doesn't exist, then we just return an empty dictionary. If it
716
is corrupted, return an empty dictionary.
717
"""
718
filename = self.cache_filename()
719
if not os.path.exists(filename):
720
return {}
721
import cPickle
722
file = open(self.cache_filename(), 'rb')
723
try:
724
cache = cPickle.load(file)
725
except StandardError:
726
logger.debug("Cache file '%s' is corrupted; ignoring it..."% filename)
727
cache = {}
728
else:
729
logger.debug("Loaded .rst file cache: %s", filename)
730
finally:
731
file.close()
732
return cache
733
734
def save_cache(self):
735
"""
736
Save the cache of already generated ReST files.
737
"""
738
cache = self.get_cache()
739
740
global options
741
cache['option_inherited'] = options.inherited
742
cache['option_underscore'] = options.underscore
743
744
import cPickle
745
file = open(self.cache_filename(), 'wb')
746
cPickle.dump(cache, file)
747
file.close()
748
logger.debug("Saved .rst file cache: %s", self.cache_filename())
749
750
def get_sphinx_environment(self):
751
"""
752
Returns the Sphinx environment for this project.
753
"""
754
from sphinx.environment import BuildEnvironment
755
class Foo(object):
756
pass
757
config = Foo()
758
config.values = []
759
760
env_pickle = os.path.join(self._doctrees_dir(), 'environment.pickle')
761
try:
762
env = BuildEnvironment.frompickle(config, env_pickle)
763
logger.debug("Opened Sphinx environment: %s", env_pickle)
764
return env
765
except IOError as err:
766
logger.debug("Failed to open Sphinx environment: %s", err)
767
pass
768
769
def update_mtimes(self):
770
"""
771
Updates the modification times for ReST files in the Sphinx
772
environment for this project.
773
"""
774
env = self.get_sphinx_environment()
775
if env is not None:
776
import time
777
for doc in env.all_docs:
778
env.all_docs[doc] = time.time()
779
logger.info("Updated %d .rst file mtimes", len(env.all_docs))
780
# This is the only place we need to save (as opposed to
781
# load) Sphinx's pickle, so we do it right here.
782
env_pickle = os.path.join(self._doctrees_dir(),
783
'environment.pickle')
784
785
# When cloning a new branch (see
786
# SAGE_LOCAL/bin/sage-clone), we hard link the doc output.
787
# To avoid making unlinked, potentially inconsistent
788
# copies of the environment, we *don't* use
789
# env.topickle(env_pickle), which first writes a temporary
790
# file. We adapt sphinx.environment's
791
# BuildEnvironment.topickle:
792
import cPickle, types
793
794
# remove unpicklable attributes
795
env.set_warnfunc(None)
796
del env.config.values
797
picklefile = open(env_pickle, 'wb')
798
# remove potentially pickling-problematic values from config
799
for key, val in vars(env.config).items():
800
if key.startswith('_') or isinstance(val, (types.ModuleType,
801
types.FunctionType,
802
types.ClassType)):
803
del env.config[key]
804
try:
805
cPickle.dump(env, picklefile, cPickle.HIGHEST_PROTOCOL)
806
finally:
807
picklefile.close()
808
809
logger.debug("Saved Sphinx environment: %s", env_pickle)
810
811
def get_modified_modules(self):
812
"""
813
Returns an iterator for all the modules that have been modified
814
since the documentation was last built.
815
"""
816
env = self.get_sphinx_environment()
817
if env is None:
818
logger.debug("Stopped check for modified modules.")
819
return
820
try:
821
added, changed, removed = env.get_outdated_files(False)
822
logger.info("Sphinx found %d modified modules", len(changed))
823
except OSError as err:
824
logger.debug("Sphinx failed to determine modified modules: %s", err)
825
self.clean_auto()
826
return
827
for name in changed:
828
# Only pay attention to files in a directory sage/... In
829
# particular, don't treat a file like 'sagetex.rst' in
830
# doc/en/reference/misc as an autogenerated file: see
831
# #14199.
832
if name.startswith('sage' + os.sep):
833
yield name
834
835
def print_modified_modules(self):
836
"""
837
Prints a list of all the modules that have been modified since
838
the documentation was last built.
839
"""
840
for module_name in self.get_modified_modules():
841
print module_name
842
843
def get_all_rst_files(self, exclude_sage=True):
844
"""
845
Returns an iterator for all rst files which are not
846
autogenerated.
847
"""
848
for directory, subdirs, files in os.walk(self.dir):
849
if exclude_sage and directory.startswith(os.path.join(self.dir, 'sage')):
850
continue
851
for filename in files:
852
if not filename.endswith('.rst'):
853
continue
854
yield os.path.join(directory, filename)
855
856
def get_all_included_modules(self):
857
"""
858
Returns an iterator for all modules which are included in the
859
reference manual.
860
"""
861
for filename in self.get_all_rst_files():
862
for module in self.get_modules(filename):
863
yield module
864
865
def get_newly_included_modules(self, save=False):
866
"""
867
Returns an iterator for all modules that appear in the
868
toctrees that don't appear in the cache.
869
"""
870
cache = self.get_cache()
871
new_modules = 0
872
for module in self.get_all_included_modules():
873
if module not in cache:
874
cache[module] = True
875
new_modules += 1
876
yield module
877
logger.info("Found %d newly included modules", new_modules)
878
if save:
879
self.save_cache()
880
881
def print_newly_included_modules(self):
882
"""
883
Prints all of the modules that appear in the toctrees that
884
don't appear in the cache.
885
"""
886
for module_name in self.get_newly_included_modules():
887
print module_name
888
889
def get_modules(self, filename):
890
"""
891
Given a filename for a ReST file, return an iterator for
892
all of the autogenerated ReST files that it includes.
893
"""
894
#Create the regular expression used to detect an autogenerated file
895
auto_re = re.compile('^\s*(..\/)*(sage(nb)?\/[\w\/]*)\s*$')
896
897
#Read the lines
898
f = open(filename)
899
lines = f.readlines()
900
f.close()
901
902
for line in lines:
903
match = auto_re.match(line)
904
if match:
905
yield match.group(2).replace(os.path.sep, '.')
906
907
def get_module_docstring_title(self, module_name):
908
"""
909
Returns the title of the module from its docstring.
910
"""
911
#Try to import the module
912
try:
913
__import__(module_name)
914
except ImportError as err:
915
logger.error("Warning: Could not import %s %s", module_name, err)
916
return "UNABLE TO IMPORT MODULE"
917
module = sys.modules[module_name]
918
919
#Get the docstring
920
doc = module.__doc__
921
if doc is None:
922
doc = module.doc if hasattr(module, 'doc') else ""
923
924
#Extract the title
925
i = doc.find('\n')
926
if i != -1:
927
return doc[i+1:].lstrip().splitlines()[0]
928
else:
929
return doc
930
931
def auto_rest_filename(self, module_name):
932
"""
933
Returns the name of the file associated to a given module
934
935
EXAMPLES::
936
937
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
938
sage: import builder
939
sage: builder.ReferenceSubBuilder("reference").auto_rest_filename("sage.combinat.partition")
940
'.../doc/en/reference/sage/combinat/partition.rst'
941
"""
942
return self.dir + os.path.sep + module_name.replace('.',os.path.sep) + '.rst'
943
944
def write_auto_rest_file(self, module_name):
945
"""
946
Writes the autogenerated ReST file for module_name.
947
"""
948
if not module_name.startswith('sage'):
949
return
950
filename = self.auto_rest_filename(module_name)
951
mkdir(os.path.dirname(filename))
952
953
outfile = open(filename, 'w')
954
955
title = self.get_module_docstring_title(module_name)
956
957
if title == '':
958
logger.error("Warning: Missing title for %s", module_name)
959
title = "MISSING TITLE"
960
961
# Don't doctest the autogenerated file.
962
outfile.write(".. nodoctest\n\n")
963
# Now write the actual content.
964
outfile.write(".. _%s:\n\n"%module_name)
965
outfile.write(title + '\n')
966
outfile.write('='*len(title) + "\n\n")
967
outfile.write('.. This file has been autogenerated.\n\n')
968
969
global options
970
inherited = ':inherited-members:' if options.inherited else ''
971
972
automodule = '''
973
.. automodule:: %s
974
:members:
975
:undoc-members:
976
:show-inheritance:
977
%s
978
979
'''
980
outfile.write(automodule % (module_name, inherited))
981
982
outfile.close()
983
984
def clean_auto(self):
985
"""
986
Remove the cache file for the autogenerated files as well as
987
the files themselves.
988
"""
989
if os.path.exists(self.cache_filename()):
990
os.unlink(self.cache_filename())
991
logger.debug("Deleted .rst cache file: %s", self.cache_filename())
992
993
import shutil
994
try:
995
shutil.rmtree(os.path.join(self.dir, 'sage'))
996
logger.debug("Deleted auto-generated .rst files in: %s",
997
os.path.join(self.dir, 'sage'))
998
except OSError:
999
pass
1000
1001
def get_unincluded_modules(self):
1002
"""
1003
Returns an iterator for all the modules in the Sage library
1004
which are not included in the reference manual.
1005
"""
1006
#Make a dictionary of the included modules
1007
included_modules = {}
1008
for module_name in self.get_all_included_modules():
1009
included_modules[module_name] = True
1010
1011
base_path = os.path.join(SAGE_SRC, 'sage')
1012
for directory, subdirs, files in os.walk(base_path):
1013
for filename in files:
1014
if not (filename.endswith('.py') or
1015
filename.endswith('.pyx')):
1016
continue
1017
1018
path = os.path.join(directory, filename)
1019
1020
#Create the module name
1021
module_name = path[len(base_path):].replace(os.path.sep, '.')
1022
module_name = 'sage' + module_name
1023
module_name = module_name[:-4] if module_name.endswith('pyx') else module_name[:-3]
1024
1025
#Exclude some ones -- we don't want init the manual
1026
if module_name.endswith('__init__') or module_name.endswith('all'):
1027
continue
1028
1029
if module_name not in included_modules:
1030
yield module_name
1031
1032
def print_unincluded_modules(self):
1033
"""
1034
Prints all of the modules which are not included in the Sage
1035
reference manual.
1036
"""
1037
for module_name in self.get_unincluded_modules():
1038
print module_name
1039
1040
def print_included_modules(self):
1041
"""
1042
Prints all of the modules that are included in the Sage reference
1043
manual.
1044
"""
1045
for module_name in self.get_all_included_modules():
1046
print module_name
1047
1048
1049
def get_builder(name):
1050
"""
1051
Returns an appropriate *Builder object for the document ``name``.
1052
DocBuilder and its subclasses do all the real work in building the
1053
documentation.
1054
"""
1055
if name == 'all':
1056
return AllBuilder()
1057
elif name.endswith('reference'):
1058
return ReferenceBuilder(name)
1059
elif 'reference' in name:
1060
return ReferenceSubBuilder(name)
1061
elif name.endswith('website'):
1062
return WebsiteBuilder(name)
1063
elif name in get_documents() or name in AllBuilder().get_all_documents():
1064
return DocBuilder(name)
1065
else:
1066
print "'%s' is not a recognized document. Type 'sage -docbuild -D' for a list"%name
1067
print "of documents, or 'sage -docbuild --help' for more help."
1068
sys.exit(1)
1069
1070
def format_columns(lst, align='<', cols=None, indent=4, pad=3, width=80):
1071
"""
1072
Utility function that formats a list as a simple table and returns
1073
a Unicode string representation. The number of columns is
1074
computed from the other options, unless it's passed as a keyword
1075
argument. For help on Python's string formatter, see
1076
1077
http://docs.python.org/library/string.html#format-string-syntax
1078
"""
1079
# Can we generalize this (efficiently) to other / multiple inputs
1080
# and generators?
1081
size = max(map(len, lst)) + pad
1082
if cols is None:
1083
import math
1084
cols = math.trunc((width - indent) / size)
1085
s = " " * indent
1086
for i in xrange(len(lst)):
1087
if i != 0 and i % cols == 0:
1088
s += "\n" + " " * indent
1089
s += "{0:{1}{2}}".format(lst[i], align, size)
1090
s += "\n"
1091
return unicode(s)
1092
1093
def help_usage(s=u"", compact=False):
1094
"""
1095
Appends and returns a brief usage message for the Sage
1096
documentation builder. If 'compact' is False, the function adds a
1097
final newline character.
1098
"""
1099
s += "sage -docbuild [OPTIONS] DOCUMENT (FORMAT | COMMAND)"
1100
if not compact:
1101
s += "\n"
1102
return s
1103
1104
def help_description(s=u"", compact=False):
1105
"""
1106
Appends and returns a brief description of the Sage documentation
1107
builder. If 'compact' is False, the function adds a final newline
1108
character.
1109
"""
1110
s += "Build or return information about Sage documentation.\n"
1111
s += " DOCUMENT name of the document to build\n"
1112
s += " FORMAT document output format\n"
1113
s += " COMMAND document-specific command\n"
1114
s += "A DOCUMENT and either a FORMAT or a COMMAND are required,\n"
1115
s += "unless a list of one or more of these is requested."
1116
if not compact:
1117
s += "\n"
1118
return s
1119
1120
def help_examples(s=u""):
1121
"""
1122
Appends and returns some usage examples for the Sage documentation
1123
builder.
1124
"""
1125
s += "Examples:\n"
1126
s += " sage -docbuild -FDC all\n"
1127
s += " sage -docbuild constructions pdf\n"
1128
s += " sage -docbuild reference html -jv3\n"
1129
s += " sage -docbuild --mathjax tutorial html\n"
1130
s += " sage -docbuild reference print_unincluded_modules\n"
1131
s += " sage -docbuild developer -j html --sphinx-opts -q,-aE --verbose 2"
1132
return s
1133
1134
def get_documents():
1135
"""
1136
Returns a list of document names the Sage documentation builder
1137
will accept as command-line arguments.
1138
"""
1139
all_b = AllBuilder()
1140
docs = all_b.get_all_documents()
1141
docs = [(d[3:] if d[0:3] == 'en/' else d) for d in docs]
1142
return docs
1143
1144
def help_documents(s=u""):
1145
"""
1146
Appends and returns a tabular list of documents, including a
1147
shortcut 'all' for all documents, available to the Sage
1148
documentation builder.
1149
"""
1150
docs = get_documents()
1151
s += "DOCUMENTs:\n"
1152
s += format_columns(docs + ['all (!)'])
1153
s += "(!) Builds everything.\n\n"
1154
if 'reference' in docs:
1155
s+= "Other valid document names take the form 'reference/DIR', where\n"
1156
s+= "DIR is a subdirectory of SAGE_DOC/en/reference/.\n"
1157
s+= "This builds just the specified part of the reference manual.\n"
1158
return s
1159
1160
def get_formats():
1161
"""
1162
Returns a list of output formats the Sage documentation builder
1163
will accept on the command-line.
1164
"""
1165
tut_b = DocBuilder('en/tutorial')
1166
formats = tut_b._output_formats()
1167
formats.remove('html')
1168
return ['html', 'pdf'] + formats
1169
1170
def help_formats(s=u""):
1171
"""
1172
Appends and returns a tabular list of output formats available to
1173
the Sage documentation builder.
1174
"""
1175
s += "FORMATs:\n"
1176
s += format_columns(get_formats())
1177
return s
1178
1179
def help_commands(name='all', s=u""):
1180
"""
1181
Appends and returns a tabular list of commands, if any, the Sage
1182
documentation builder can run on the indicated document. The
1183
default is to list all commands for all documents.
1184
"""
1185
# To do: Generate the lists dynamically, using class attributes,
1186
# as with the Builders above.
1187
command_dict = { 'reference' : [
1188
'print_included_modules', 'print_modified_modules (*)',
1189
'print_unincluded_modules', 'print_newly_included_modules (*)',
1190
] }
1191
for doc in command_dict:
1192
if name == 'all' or doc == name:
1193
s += "COMMANDs for the DOCUMENT '" + doc + "':\n"
1194
s += format_columns(command_dict[doc])
1195
s += "(*) Since the last build.\n"
1196
return s
1197
1198
def help_message_long(option, opt_str, value, parser):
1199
"""
1200
Prints an extended help message for the Sage documentation builder
1201
and exits.
1202
"""
1203
help_funcs = [ help_usage, help_description, help_documents,
1204
help_formats, help_commands, parser.format_option_help,
1205
help_examples ]
1206
for f in help_funcs:
1207
print f()
1208
sys.exit(0)
1209
1210
def help_message_short(option=None, opt_str=None, value=None, parser=None,
1211
error=False):
1212
"""
1213
Prints a help message for the Sage documentation builder. The
1214
message includes command-line usage and a list of options. The
1215
message is printed only on the first call. If error is True
1216
during this call, the message is printed only if the user hasn't
1217
requested a list (e.g., documents, formats, commands).
1218
"""
1219
if not hasattr(parser.values, 'printed_help'):
1220
if error == True:
1221
if not hasattr(parser.values, 'printed_list'):
1222
parser.print_help()
1223
else:
1224
parser.print_help()
1225
setattr(parser.values, 'printed_help', 1)
1226
1227
def help_wrapper(option, opt_str, value, parser):
1228
"""
1229
A helper wrapper for command-line options to the Sage
1230
documentation builder that print lists, such as document names,
1231
formats, and document-specific commands.
1232
"""
1233
if option.dest == 'commands':
1234
print help_commands(value),
1235
if option.dest == 'documents':
1236
print help_documents(),
1237
if option.dest == 'formats':
1238
print help_formats(),
1239
setattr(parser.values, 'printed_list', 1)
1240
1241
1242
class IndentedHelpFormatter2(optparse.IndentedHelpFormatter, object):
1243
"""
1244
Custom help formatter class for optparse's OptionParser.
1245
"""
1246
def format_description(self, description):
1247
"""
1248
Returns a formatted description, preserving any original
1249
explicit new line characters.
1250
"""
1251
if description:
1252
lines_in = description.split('\n')
1253
lines_out = [self._format_text(line) for line in lines_in]
1254
return "\n".join(lines_out) + "\n"
1255
else:
1256
return ""
1257
1258
def format_heading(self, heading):
1259
"""
1260
Returns a formatted heading using the superclass' formatter.
1261
If the heading is 'options', up to case, the function converts
1262
it to ALL CAPS. This allows us to match the heading 'OPTIONS' with
1263
the same token in the builder's usage message.
1264
"""
1265
if heading.lower() == 'options':
1266
heading = "OPTIONS"
1267
return super(IndentedHelpFormatter2, self).format_heading(heading)
1268
1269
def setup_parser():
1270
"""
1271
Sets up and returns a command-line OptionParser instance for the
1272
Sage documentation builder.
1273
"""
1274
# Documentation: http://docs.python.org/library/optparse.html
1275
parser = optparse.OptionParser(add_help_option=False,
1276
usage=help_usage(compact=True),
1277
formatter=IndentedHelpFormatter2(),
1278
description=help_description(compact=True))
1279
1280
# Standard options. Note: We use explicit option.dest names
1281
# to avoid ambiguity.
1282
standard = optparse.OptionGroup(parser, "Standard")
1283
standard.add_option("-h", "--help",
1284
action="callback", callback=help_message_short,
1285
help="show a help message and exit")
1286
standard.add_option("-H", "--help-all",
1287
action="callback", callback=help_message_long,
1288
help="show an extended help message and exit")
1289
standard.add_option("-D", "--documents", dest="documents",
1290
action="callback", callback=help_wrapper,
1291
help="list all available DOCUMENTs")
1292
standard.add_option("-F", "--formats", dest="formats",
1293
action="callback", callback=help_wrapper,
1294
help="list all output FORMATs")
1295
standard.add_option("-C", "--commands", dest="commands",
1296
type="string", metavar="DOC",
1297
action="callback", callback=help_wrapper,
1298
help="list all COMMANDs for DOCUMENT DOC; use 'all' to list all")
1299
1300
standard.add_option("-i", "--inherited", dest="inherited",
1301
default=False, action="store_true",
1302
help="include inherited members in reference manual; may be slow, may fail for PDF output")
1303
standard.add_option("-u", "--underscore", dest="underscore",
1304
default=False, action="store_true",
1305
help="include variables prefixed with '_' in reference manual; may be slow, may fail for PDF output")
1306
1307
standard.add_option("-j", "--mathjax", "--jsmath", dest="mathjax",
1308
action="store_true",
1309
help="render math using MathJax; FORMATs: html, json, pickle, web")
1310
standard.add_option("--no-pdf-links", dest="no_pdf_links",
1311
action="store_true",
1312
help="do not include PDF links in DOCUMENT 'website'; FORMATs: html, json, pickle, web")
1313
standard.add_option("--warn-links", dest="warn_links",
1314
default=False, action="store_true",
1315
help="issue a warning whenever a link is not properly resolved; equivalent to '--sphinx-opts -n' (sphinx option: nitpicky)")
1316
standard.add_option("--check-nested", dest="check_nested",
1317
action="store_true",
1318
help="check picklability of nested classes in DOCUMENT 'reference'")
1319
standard.add_option("-N", "--no-colors", dest="color", default=True,
1320
action="store_false",
1321
help="do not color output; does not affect children")
1322
standard.add_option("-q", "--quiet", dest="verbose",
1323
action="store_const", const=0,
1324
help="work quietly; same as --verbose=0")
1325
standard.add_option("-v", "--verbose", dest="verbose",
1326
type="int", default=1, metavar="LEVEL",
1327
action="store",
1328
help="report progress at LEVEL=0 (quiet), 1 (normal), 2 (info), or 3 (debug); does not affect children")
1329
parser.add_option_group(standard)
1330
1331
# Advanced options.
1332
advanced = optparse.OptionGroup(parser, "Advanced",
1333
"Use these options with care.")
1334
advanced.add_option("-S", "--sphinx-opts", dest="sphinx_opts",
1335
type="string", metavar="OPTS",
1336
action="store",
1337
help="pass comma-separated OPTS to sphinx-build")
1338
advanced.add_option("-U", "--update-mtimes", dest="update_mtimes",
1339
default=False, action="store_true",
1340
help="before building reference manual, update modification times for auto-generated ReST files")
1341
parser.add_option_group(advanced)
1342
1343
return parser
1344
1345
def setup_logger(verbose=1, color=True):
1346
"""
1347
Sets up and returns a Python Logger instance for the Sage
1348
documentation builder. The optional argument sets logger's level
1349
and message format.
1350
"""
1351
# Set up colors. Adapted from sphinx.cmdline.
1352
import sphinx.util.console as c
1353
if not color or not sys.stdout.isatty() or not c.color_terminal():
1354
c.nocolor()
1355
1356
# Available colors: black, darkgray, (dark)red, dark(green),
1357
# brown, yellow, (dark)blue, purple, fuchsia, turquoise, teal,
1358
# lightgray, white. Available styles: reset, bold, faint,
1359
# standout, underline, blink.
1360
1361
# Set up log record formats.
1362
format_std = "%(message)s"
1363
formatter = logging.Formatter(format_std)
1364
1365
# format_debug = "%(module)s #%(lineno)s %(funcName)s() %(message)s"
1366
fields = ['%(module)s', '#%(lineno)s', '%(funcName)s()', '%(message)s']
1367
colors = ['darkblue', 'darkred', 'brown', 'reset']
1368
styles = ['reset', 'reset', 'reset', 'reset']
1369
format_debug = ""
1370
for i in xrange(len(fields)):
1371
format_debug += c.colorize(styles[i], c.colorize(colors[i], fields[i]))
1372
if i != len(fields):
1373
format_debug += " "
1374
1375
# Documentation: http://docs.python.org/library/logging.html
1376
logger = logging.getLogger('doc.common.builder')
1377
1378
# Note: There's also Handler.setLevel(). The argument is the
1379
# lowest severity message that the respective logger or handler
1380
# will pass on. The default levels are DEBUG, INFO, WARNING,
1381
# ERROR, and CRITICAL. We use "WARNING" for normal verbosity and
1382
# "ERROR" for quiet operation. It's possible to define custom
1383
# levels. See the documentation for details.
1384
if verbose == 0:
1385
logger.setLevel(logging.ERROR)
1386
if verbose == 1:
1387
logger.setLevel(logging.WARNING)
1388
if verbose == 2:
1389
logger.setLevel(logging.INFO)
1390
if verbose == 3:
1391
logger.setLevel(logging.DEBUG)
1392
formatter = logging.Formatter(format_debug)
1393
1394
handler = logging.StreamHandler()
1395
handler.setFormatter(formatter)
1396
logger.addHandler(handler)
1397
return logger
1398
1399
1400
class IntersphinxCache:
1401
"""
1402
Replace sphinx.ext.intersphinx.fetch_inventory by an in-memory
1403
cached version.
1404
"""
1405
def __init__(self):
1406
self.inventories = {}
1407
self.real_fetch_inventory = sphinx.ext.intersphinx.fetch_inventory
1408
sphinx.ext.intersphinx.fetch_inventory = self.fetch_inventory
1409
1410
def fetch_inventory(self, app, uri, inv):
1411
"""
1412
Return the result of ``sphinx.ext.intersphinx.fetch_inventory()``
1413
from a cache if possible. Otherwise, call
1414
``sphinx.ext.intersphinx.fetch_inventory()`` and cache the result.
1415
"""
1416
t = (uri, inv)
1417
try:
1418
return self.inventories[t]
1419
except KeyError:
1420
i = self.real_fetch_inventory(app, uri, inv)
1421
self.inventories[t] = i
1422
return i
1423
1424
if __name__ == '__main__':
1425
# Parse the command-line.
1426
parser = setup_parser()
1427
options, args = parser.parse_args()
1428
1429
# Get the name and type (target format) of the document we are
1430
# trying to build.
1431
try:
1432
name, type = args
1433
except ValueError:
1434
help_message_short(parser=parser, error=True)
1435
sys.exit(1)
1436
1437
# Set up module-wide logging.
1438
logger = setup_logger(options.verbose, options.color)
1439
1440
# Process selected options.
1441
#
1442
# MathJax: this check usually has no practical effect, since
1443
# SAGE_DOC_MATHJAX is set to "True" by the script sage-env.
1444
# To disable MathJax, set SAGE_DOC_MATHJAX to "no" or "False".
1445
if options.mathjax or (os.environ.get('SAGE_DOC_MATHJAX', 'no') != 'no'
1446
and os.environ.get('SAGE_DOC_MATHJAX', 'no') != 'False'):
1447
os.environ['SAGE_DOC_MATHJAX'] = 'True'
1448
1449
if options.check_nested:
1450
os.environ['SAGE_CHECK_NESTED'] = 'True'
1451
1452
if options.underscore:
1453
os.environ['SAGE_DOC_UNDERSCORE'] = "True"
1454
1455
if options.sphinx_opts:
1456
ALLSPHINXOPTS += options.sphinx_opts.replace(',', ' ') + " "
1457
if options.no_pdf_links:
1458
WEBSITESPHINXOPTS = " -A hide_pdf_links=1 "
1459
if options.warn_links:
1460
ALLSPHINXOPTS += "-n "
1461
1462
# Make sure common/static exists.
1463
mkdir(os.path.join(SAGE_DOC, 'common', 'static'))
1464
1465
import sage.all
1466
1467
# Minimize GAP/libGAP RAM usage when we build the docs
1468
set_gap_memory_pool_size(1) # 1 MB
1469
1470
# Set up Intersphinx cache
1471
C = IntersphinxCache()
1472
1473
# Get the builder and build.
1474
getattr(get_builder(name), type)()
1475
1476