Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagelib
Path: blob/master/doc/common/builder.py
4024 views
1
#!/usr/bin/env python
2
import glob, logging, optparse, os, shutil, subprocess, sys, textwrap
3
4
#We remove the current directory from sys.path right away
5
#so that we import sage from the proper spot
6
try:
7
sys.path.remove(os.path.realpath(os.getcwd()))
8
except:
9
pass
10
11
from sage.misc.cachefunc import cached_method
12
13
# Read options
14
execfile(os.path.join(os.getenv('SAGE_ROOT'), 'devel', 'sage', 'doc', 'common' , 'build_options.py'))
15
16
##########################################
17
# Utility Functions #
18
##########################################
19
def mkdir(path):
20
"""
21
Makes the directory at path if it doesn't exist and returns the
22
string path.
23
24
EXAMPLES::
25
26
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
27
sage: d = tmp_filename(); d
28
'/.../tmp_...'
29
sage: os.path.exists(d)
30
False
31
sage: dd = builder.mkdir(d)
32
sage: d == dd
33
True
34
sage: os.path.exists(d)
35
True
36
"""
37
if not os.path.exists(path):
38
os.makedirs(path)
39
return path
40
41
def copytree(src, dst, symlinks=False, ignore_errors=False):
42
"""
43
Recursively copy a directory tree using copy2().
44
45
The destination directory must not already exist.
46
If exception(s) occur, an Error is raised with a list of reasons.
47
48
If the optional symlinks flag is true, symbolic links in the
49
source tree result in symbolic links in the destination tree; if
50
it is false, the contents of the files pointed to by symbolic
51
links are copied.
52
53
XXX Consider this example code rather than the ultimate tool.
54
55
"""
56
names = os.listdir(src)
57
mkdir(dst)
58
errors = []
59
for name in names:
60
srcname = os.path.join(src, name)
61
dstname = os.path.join(dst, name)
62
try:
63
if symlinks and os.path.islink(srcname):
64
linkto = os.readlink(srcname)
65
os.symlink(linkto, dstname)
66
elif os.path.isdir(srcname):
67
copytree(srcname, dstname, symlinks)
68
else:
69
shutil.copy2(srcname, dstname)
70
# XXX What about devices, sockets etc.?
71
except (IOError, os.error) as why:
72
errors.append((srcname, dstname, str(why)))
73
# catch the Error from the recursive copytree so that we can
74
# continue with other files
75
except shutil.Error as err:
76
errors.extend(err.args[0])
77
try:
78
shutil.copystat(src, dst)
79
except OSError as why:
80
errors.extend((src, dst, str(why)))
81
if errors and not ignore_errors:
82
raise shutil.Error, errors
83
84
85
86
##########################################
87
# Builders #
88
##########################################
89
def builder_helper(type):
90
"""
91
Returns a function which builds the documentation for
92
output type type.
93
"""
94
def f(self):
95
output_dir = self._output_dir(type)
96
os.chdir(self.dir)
97
98
build_command = 'sphinx-build'
99
build_command += ' -b %s -d %s %s %s %s'%(type, self._doctrees_dir(),
100
ALLSPHINXOPTS, self.dir,
101
output_dir)
102
logger.warning(build_command)
103
subprocess.call(build_command, shell=True)
104
105
logger.warning("Build finished. The built documents can be found in %s", output_dir)
106
107
f.is_output_format = True
108
return f
109
110
111
class DocBuilder(object):
112
def __init__(self, name, lang='en'):
113
"""
114
INPUT:
115
116
- ``name`` - the name of a subdirectory in SAGE_DOC, such as
117
'tutorial' or 'bordeaux_2008'
118
119
- ``lang`` - (default "en") the language of the document.
120
"""
121
if '/' in name:
122
lang, name = name.split(os.path.sep)
123
self.name = name
124
self.lang = lang
125
self.dir = os.path.join(SAGE_DOC, lang, name)
126
127
#Make sure the .static and .templates directories are there
128
mkdir(os.path.join(self.dir, "static"))
129
mkdir(os.path.join(self.dir, "templates"))
130
131
def _output_dir(self, type):
132
"""
133
Returns the directory where the output of type type is stored.
134
If the directory does not exist, then it will automatically be
135
created.
136
137
EXAMPLES::
138
139
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
140
sage: b = builder.DocBuilder('tutorial')
141
sage: b._output_dir('html')
142
'.../devel/sage/doc/output/html/en/tutorial'
143
"""
144
return mkdir(os.path.join(SAGE_DOC, "output", type, self.lang, self.name))
145
146
def _doctrees_dir(self):
147
"""
148
Returns the directory where the doctrees are stored. If the
149
directory does not exist, then it will automatically be
150
created.
151
152
EXAMPLES::
153
154
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
155
sage: b = builder.DocBuilder('tutorial')
156
sage: b._doctrees_dir()
157
'.../devel/sage/doc/output/doctrees/en/tutorial'
158
"""
159
return mkdir(os.path.join(SAGE_DOC, "output", 'doctrees', self.lang, self.name))
160
161
def _output_formats(self):
162
"""
163
Returns a list of the possible output formats.
164
165
EXAMPLES::
166
167
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
168
sage: b = builder.DocBuilder('tutorial')
169
sage: b._output_formats()
170
['changes', 'html', 'htmlhelp', 'json', 'latex', 'linkcheck', 'pickle', 'web']
171
172
"""
173
#Go through all the attributes of self and check to
174
#see which ones have an 'is_output_format' attribute. These
175
#are the ones created with builder_helper.
176
output_formats = []
177
for attr in dir(self):
178
if hasattr(getattr(self, attr), 'is_output_format'):
179
output_formats.append(attr)
180
output_formats.sort()
181
return output_formats
182
183
def pdf(self):
184
"""
185
Builds the PDF files for this document. This is done by first
186
(re)-building the LaTeX output, going into that LaTeX
187
directory, and running 'make all-pdf' there.
188
189
EXAMPLES::
190
191
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
192
sage: b = builder.DocBuilder('tutorial')
193
sage: b.pdf() #not tested
194
"""
195
self.latex()
196
os.chdir(self._output_dir('latex'))
197
subprocess.call('make all-pdf', shell=True)
198
199
pdf_dir = self._output_dir('pdf')
200
for pdf_file in glob.glob('*.pdf'):
201
shutil.move(pdf_file, os.path.join(pdf_dir, pdf_file))
202
203
logger.warning("Build finished. The built documents can be found in %s", pdf_dir)
204
205
def clean(self, *args):
206
"""
207
"""
208
import shutil
209
shutil.rmtree(self._doctrees_dir())
210
output_formats = list(args) if args else self._output_formats()
211
for format in output_formats:
212
shutil.rmtree(self._output_dir(format), ignore_errors=True)
213
214
html = builder_helper('html')
215
pickle = builder_helper('pickle')
216
web = pickle
217
json = builder_helper('json')
218
htmlhelp = builder_helper('htmlhelp')
219
latex = builder_helper('latex')
220
changes = builder_helper('changes')
221
linkcheck = builder_helper('linkcheck')
222
223
class AllBuilder(object):
224
"""
225
A class used to build all of the documentation.
226
"""
227
def __getattr__(self, attr):
228
"""
229
For any attributes not explicitly defined, we just go through
230
all of the documents and call their attr. For example,
231
'AllBuilder().json()' will go through all of the documents
232
and call the json() method on their builders.
233
"""
234
from functools import partial
235
return partial(self._wrapper, attr)
236
237
def _wrapper(self, name, *args, **kwds):
238
"""
239
This is the function which goes through all of the documents
240
and does the actual building.
241
"""
242
for document in self.get_all_documents():
243
getattr(get_builder(document), name)(*args, **kwds)
244
245
def get_all_documents(self):
246
"""
247
Returns a list of all of the documents. A document is a directory within one of
248
the language subdirectories of SAGE_DOC specified by the global LANGUAGES
249
variable.
250
251
EXAMPLES::
252
253
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
254
sage: documents = builder.AllBuilder().get_all_documents()
255
sage: 'en/tutorial' in documents
256
True
257
sage: documents[0] == 'en/reference'
258
True
259
"""
260
documents = []
261
for lang in LANGUAGES:
262
for document in os.listdir(os.path.join(SAGE_DOC, lang)):
263
if document not in OMIT:
264
documents.append(os.path.join(lang, document))
265
266
# Ensure that the reference guide is compiled first so that links from
267
# the other document to it are correctly resolved.
268
if 'en/reference' in documents:
269
documents.remove('en/reference')
270
documents.insert(0, 'en/reference')
271
272
return documents
273
274
class WebsiteBuilder(DocBuilder):
275
def html(self):
276
"""
277
After we've finished building the website index page, we copy
278
everything one directory up.
279
"""
280
DocBuilder.html(self)
281
html_output_dir = self._output_dir('html')
282
copytree(html_output_dir,
283
os.path.realpath(os.path.join(html_output_dir, '..')),
284
ignore_errors=False)
285
286
def clean(self):
287
"""
288
When we clean the output for the website index, we need to
289
remove all of the HTML that were placed in the parent
290
directory.
291
"""
292
html_output_dir = self._output_dir('html')
293
parent_dir = os.path.realpath(os.path.join(html_output_dir, '..'))
294
for filename in os.listdir(html_output_dir):
295
parent_filename = os.path.join(parent_dir, filename)
296
if not os.path.exists(parent_filename):
297
continue
298
if os.path.isdir(parent_filename):
299
shutil.rmtree(parent_filename, ignore_errors=True)
300
else:
301
os.unlink(parent_filename)
302
303
DocBuilder.clean(self)
304
305
class ReferenceBuilder(DocBuilder):
306
"""
307
This the class used to build the reference manual. It is
308
resposible for making sure the auto generated ReST files for the
309
Sage library are up to date.
310
311
When building any output, we must first go through and check
312
to see if we need to update any of the autogenerated ReST
313
files. There are two cases where this would happen:
314
315
1. A new module gets added to one of the toctrees.
316
317
2. The actual module gets updated and possibly contains a new
318
title.
319
"""
320
def __init__(self, *args, **kwds):
321
DocBuilder.__init__(self, *args, **kwds)
322
self._wrap_builder_helpers()
323
324
def _wrap_builder_helpers(self):
325
from functools import partial, update_wrapper
326
for attr in dir(self):
327
if hasattr(getattr(self, attr), 'is_output_format'):
328
f = partial(self._wrapper, attr)
329
f.is_output_format = True
330
update_wrapper(f, getattr(self, attr))
331
setattr(self, attr, f)
332
333
def _wrapper(self, build_type, *args, **kwds):
334
"""
335
This is the wrapper around the builder_helper methods that
336
goes through and makes sure things are up to date.
337
"""
338
# Delete the auto-generated .rst files, if the inherited
339
# and/or underscored members options have changed.
340
global options
341
inherit_prev = self.get_cache().get('option_inherited')
342
underscore_prev = self.get_cache().get('option_underscore')
343
if (inherit_prev is None or inherit_prev != options.inherited or
344
underscore_prev is None or underscore_prev != options.underscore):
345
logger.info("Detected change(s) in inherited and/or underscored members option(s).")
346
self.clean_auto()
347
self.get_cache.clear_cache()
348
349
# After "sage -clone", refresh the .rst file mtimes in
350
# environment.pickle.
351
if options.update_mtimes:
352
logger.info("Checking for .rst file mtimes to update...")
353
self.update_mtimes()
354
355
#Update the .rst files for modified Python modules
356
logger.info("Updating .rst files with modified modules...")
357
for module_name in self.get_modified_modules():
358
self.write_auto_rest_file(module_name.replace(os.path.sep, '.'))
359
360
#Write the .rst files for newly included modules
361
logger.info("Writing .rst files for newly-included modules...")
362
for module_name in self.get_newly_included_modules(save=True):
363
self.write_auto_rest_file(module_name)
364
365
#Copy over the custom .rst files from _sage
366
_sage = os.path.join(self.dir, '_sage')
367
if os.path.exists(_sage):
368
logger.info("Copying over custom .rst files from %s ...", _sage)
369
copytree(_sage, os.path.join(self.dir, 'sage'))
370
371
getattr(DocBuilder, build_type)(self, *args, **kwds)
372
373
def cache_filename(self):
374
"""
375
Returns the filename where the pickle of the dictionary of
376
already generated ReST files is stored.
377
"""
378
return os.path.join(self._doctrees_dir(), 'reference.pickle')
379
380
@cached_method
381
def get_cache(self):
382
"""
383
Retrieve the cache of already generated ReST files. If it
384
doesn't exist, then we just return an empty dictionary. If it
385
is corrupted, return an empty dictionary.
386
"""
387
filename = self.cache_filename()
388
if not os.path.exists(filename):
389
return {}
390
import cPickle
391
file = open(self.cache_filename(), 'rb')
392
try:
393
cache = cPickle.load(file)
394
except:
395
logger.debug("Cache file '%s' is corrupted; ignoring it..."% filename)
396
cache = {}
397
else:
398
logger.debug("Loaded .rst file cache: %s", filename)
399
finally:
400
file.close()
401
return cache
402
403
def save_cache(self):
404
"""
405
Save the cache of already generated ReST files.
406
"""
407
cache = self.get_cache()
408
409
global options
410
cache['option_inherited'] = options.inherited
411
cache['option_underscore'] = options.underscore
412
413
import cPickle
414
file = open(self.cache_filename(), 'wb')
415
cPickle.dump(cache, file)
416
file.close()
417
logger.debug("Saved .rst file cache: %s", self.cache_filename())
418
419
def get_sphinx_environment(self):
420
"""
421
Returns the Sphinx environment for this project.
422
"""
423
from sphinx.environment import BuildEnvironment
424
class Foo(object):
425
pass
426
config = Foo()
427
config.values = []
428
429
env_pickle = os.path.join(self._doctrees_dir(), 'environment.pickle')
430
try:
431
env = BuildEnvironment.frompickle(config, env_pickle)
432
logger.debug("Opened Sphinx environment: %s", env_pickle)
433
return env
434
except IOError as err:
435
logger.debug("Failed to open Sphinx environment: %s", err)
436
pass
437
438
def update_mtimes(self):
439
"""
440
Updates the modification times for ReST files in the Sphinx
441
environment for this project.
442
"""
443
env = self.get_sphinx_environment()
444
if env is not None:
445
import time
446
for doc in env.all_docs:
447
env.all_docs[doc] = time.time()
448
logger.info("Updated %d .rst file mtimes", len(env.all_docs))
449
# This is the only place we need to save (as opposed to
450
# load) Sphinx's pickle, so we do it right here.
451
env_pickle = os.path.join(self._doctrees_dir(),
452
'environment.pickle')
453
454
# When cloning a new branch (see
455
# SAGE_LOCAL/bin/sage-clone), we hard link the doc output.
456
# To avoid making unlinked, potentially inconsistent
457
# copies of the environment, we *don't* use
458
# env.topickle(env_pickle), which first writes a temporary
459
# file. We adapt sphinx.environment's
460
# BuildEnvironment.topickle:
461
import cPickle, types
462
463
# remove unpicklable attributes
464
env.set_warnfunc(None)
465
del env.config.values
466
picklefile = open(env_pickle, 'wb')
467
# remove potentially pickling-problematic values from config
468
for key, val in vars(env.config).items():
469
if key.startswith('_') or isinstance(val, (types.ModuleType,
470
types.FunctionType,
471
types.ClassType)):
472
del env.config[key]
473
try:
474
cPickle.dump(env, picklefile, cPickle.HIGHEST_PROTOCOL)
475
finally:
476
picklefile.close()
477
478
logger.debug("Saved Sphinx environment: %s", env_pickle)
479
480
def get_modified_modules(self):
481
"""
482
Returns an iterator for all the modules that have been modified
483
since the docuementation was last built.
484
"""
485
env = self.get_sphinx_environment()
486
if env is None:
487
logger.debug("Stopped check for modified modules.")
488
return
489
try:
490
added, changed, removed = env.get_outdated_files(False)
491
logger.info("Sphinx found %d modified modules", len(changed))
492
except OSError as err:
493
logger.debug("Sphinx failed to determine modified modules: %s", err)
494
self.clean_auto()
495
return
496
for name in changed:
497
if name.startswith('sage'):
498
yield name
499
500
def print_modified_modules(self):
501
"""
502
Prints a list of all the modules that have been modified since
503
the documentation was last built.
504
"""
505
for module_name in self.get_modified_modules():
506
print module_name
507
508
def get_all_rst_files(self, exclude_sage=True):
509
"""
510
Returns an iterator for all rst files which are not
511
autogenerated.
512
"""
513
for directory, subdirs, files in os.walk(self.dir):
514
if exclude_sage and directory.startswith(os.path.join(self.dir, 'sage')):
515
continue
516
for filename in files:
517
if not filename.endswith('.rst'):
518
continue
519
yield os.path.join(directory, filename)
520
521
def get_all_included_modules(self):
522
"""
523
Returns an iterator for all modules which are included in the
524
reference manual.
525
"""
526
for filename in self.get_all_rst_files():
527
for module in self.get_modules(filename):
528
yield module
529
530
def get_newly_included_modules(self, save=False):
531
"""
532
Returns an iterator for all modules that appear in the
533
toctrees that don't appear in the cache.
534
"""
535
cache = self.get_cache()
536
new_modules = 0
537
for module in self.get_all_included_modules():
538
if module not in cache:
539
cache[module] = True
540
new_modules += 1
541
yield module
542
logger.info("Found %d newly included modules", new_modules)
543
if save:
544
self.save_cache()
545
546
def print_newly_included_modules(self):
547
"""
548
Prints all of the modules that appear in the toctrees that
549
don't appear in the cache.
550
"""
551
for module_name in self.get_newly_included_modules():
552
print module_name
553
554
def get_modules(self, filename):
555
"""
556
Given a filename for a ReST file, return an iterator for
557
all of the autogenerated ReST files that it includes.
558
"""
559
#Create the regular expression used to detect an autogenerated file
560
import re
561
auto_re = re.compile('^\s*(..\/)*(sage(nb)?\/[\w\/]*)\s*$')
562
563
#Read the lines
564
f = open(filename)
565
lines = f.readlines()
566
f.close()
567
568
for line in lines:
569
match = auto_re.match(line)
570
if match:
571
yield match.group(2).replace(os.path.sep, '.')
572
573
def get_module_docstring_title(self, module_name):
574
"""
575
Returns the title of the module from its docstring.
576
"""
577
#Try to import the module
578
try:
579
import sage.all
580
__import__(module_name)
581
except ImportError as err:
582
logger.error("Warning: Could not import %s %s", module_name, err)
583
return "UNABLE TO IMPORT MODULE"
584
module = sys.modules[module_name]
585
586
#Get the docstring
587
doc = module.__doc__
588
if doc is None:
589
doc = module.doc if hasattr(module, 'doc') else ""
590
591
#Extract the title
592
i = doc.find('\n')
593
if i != -1:
594
return doc[i+1:].lstrip().splitlines()[0]
595
else:
596
return doc
597
598
def auto_rest_filename(self, module_name):
599
"""
600
Returns the name of the file associated to a given module
601
602
EXAMPLES::
603
604
sage: import os, sys; sys.path.append(os.environ['SAGE_DOC']+'/common/'); import builder
605
sage: import builder
606
sage: builder.ReferenceBuilder("reference").auto_rest_filename("sage.combinat.partition")
607
'.../devel/sage/doc/en/reference/sage/combinat/partition.rst'
608
"""
609
return self.dir + os.path.sep + module_name.replace('.',os.path.sep) + '.rst'
610
611
def write_auto_rest_file(self, module_name):
612
"""
613
Writes the autogenerated ReST file for module_name.
614
"""
615
if not module_name.startswith('sage'):
616
return
617
filename = self.auto_rest_filename(module_name)
618
mkdir(os.path.dirname(filename))
619
620
outfile = open(filename, 'w')
621
622
title = self.get_module_docstring_title(module_name)
623
624
if title == '':
625
logger.error("Warning: Missing title for %s", module_name)
626
title = "MISSING TITLE"
627
628
# Don't doctest the autogenerated file.
629
outfile.write(".. nodoctest\n\n")
630
# Now write the actual content.
631
outfile.write(".. _%s:\n\n"%module_name)
632
outfile.write(title + '\n')
633
outfile.write('='*len(title) + "\n\n")
634
outfile.write('.. This file has been autogenerated.\n\n')
635
636
global options
637
inherited = ':inherited-members:' if options.inherited else ''
638
639
automodule = '''
640
.. automodule:: %s
641
:members:
642
:undoc-members:
643
:show-inheritance:
644
%s
645
646
'''
647
outfile.write(automodule % (module_name, inherited))
648
649
outfile.close()
650
651
def clean_auto(self):
652
"""
653
Remove the cache file for the autogenerated files as well as
654
the files themselves.
655
"""
656
if os.path.exists(self.cache_filename()):
657
os.unlink(self.cache_filename())
658
logger.debug("Deleted .rst cache file: %s", self.cache_filename())
659
660
import shutil
661
try:
662
shutil.rmtree(os.path.join(self.dir, 'sage'))
663
logger.debug("Deleted auto-generated .rst files in: %s",
664
os.path.join(self.dir, 'sage'))
665
except OSError:
666
pass
667
668
def get_unincluded_modules(self):
669
"""
670
Returns an iterator for all the modules in the Sage library
671
which are not included in the reference manual.
672
"""
673
#Make a dictionary of the included modules
674
included_modules = {}
675
for module_name in self.get_all_included_modules():
676
included_modules[module_name] = True
677
678
base_path = os.path.join(os.environ['SAGE_ROOT'], 'devel', 'sage', 'sage')
679
for directory, subdirs, files in os.walk(base_path):
680
for filename in files:
681
if not (filename.endswith('.py') or
682
filename.endswith('.pyx')):
683
continue
684
685
path = os.path.join(directory, filename)
686
687
#Create the module name
688
module_name = path[len(base_path):].replace(os.path.sep, '.')
689
module_name = 'sage' + module_name
690
module_name = module_name[:-4] if module_name.endswith('pyx') else module_name[:-3]
691
692
#Exclude some ones -- we don't want init the manual
693
if module_name.endswith('__init__') or module_name.endswith('all'):
694
continue
695
696
if module_name not in included_modules:
697
yield module_name
698
699
def print_unincluded_modules(self):
700
"""
701
Prints all of the modules which are not included in the Sage
702
reference manual.
703
"""
704
for module_name in self.get_unincluded_modules():
705
print module_name
706
707
def print_included_modules(self):
708
"""
709
Prints all of the modules that are included in the Sage reference
710
manual.
711
"""
712
for module_name in self.get_all_included_modules():
713
print module_name
714
715
716
def get_builder(name):
717
"""
718
Returns a either a AllBuilder or DocBuilder object depending
719
on whether ``name`` is 'all' or not. These are the objects
720
which do all the real work in building the documentation.
721
"""
722
if name == 'all':
723
return AllBuilder()
724
elif name.endswith('reference'):
725
return ReferenceBuilder(name)
726
elif name.endswith('website'):
727
return WebsiteBuilder(name)
728
elif name in get_documents() or name in AllBuilder().get_all_documents():
729
return DocBuilder(name)
730
else:
731
print "'%s' is not a recognized document. Type 'sage -docbuild -D' for a list"%name
732
print "of documents, or 'sage -docbuild --help' for more help."
733
sys.exit(1)
734
735
def format_columns(lst, align='<', cols=None, indent=4, pad=3, width=80):
736
"""
737
Utility function that formats a list as a simple table and returns
738
a Unicode string representation. The number of columns is
739
computed from the other options, unless it's passed as a keyword
740
argument. For help on Python's string formatter, see
741
742
http://docs.python.org/library/string.html#format-string-syntax
743
"""
744
# Can we generalize this (efficiently) to other / multiple inputs
745
# and generators?
746
size = max(map(len, lst)) + pad
747
if cols is None:
748
import math
749
cols = math.trunc((width - indent) / size)
750
s = " " * indent
751
for i in xrange(len(lst)):
752
if i != 0 and i % cols == 0:
753
s += "\n" + " " * indent
754
s += "{0:{1}{2}}".format(lst[i], align, size)
755
s += "\n"
756
return unicode(s)
757
758
def help_usage(s=u"", compact=False):
759
"""
760
Appends and returns a brief usage message for the Sage
761
documentation builder. If 'compact' is False, the function adds a
762
final newline character.
763
"""
764
s += "sage -docbuild [OPTIONS] DOCUMENT (FORMAT | COMMAND)"
765
if not compact:
766
s += "\n"
767
return s
768
769
def help_description(s=u"", compact=False):
770
"""
771
Appends and returns a brief description of the Sage documentation
772
builder. If 'compact' is False, the function adds a final newline
773
character.
774
"""
775
s += "Build or return information about Sage documentation.\n"
776
s += " DOCUMENT name of the document to build\n"
777
s += " FORMAT document output format\n"
778
s += " COMMAND document-specific command\n"
779
s += "A DOCUMENT and either a FORMAT or a COMMAND are required,\n"
780
s += "unless a list of one or more of these is requested."
781
if not compact:
782
s += "\n"
783
return s
784
785
def help_examples(s=u""):
786
"""
787
Appends and returns some usage examples for the Sage documentation
788
builder.
789
"""
790
s += "Examples:\n"
791
s += " sage -docbuild -FDC all\n"
792
s += " sage -docbuild constructions pdf\n"
793
s += " sage -docbuild reference html -jv3\n"
794
s += " sage -docbuild --jsmath tutorial html\n"
795
s += " sage -docbuild reference print_unincluded_modules\n"
796
s += " sage -docbuild developer -j html --sphinx-opts -q,-aE --verbose 2"
797
return s
798
799
def get_documents():
800
"""
801
Returns a list of document names the Sage documentation builder
802
will accept as command-line arguments.
803
"""
804
all_b = AllBuilder()
805
docs = all_b.get_all_documents()
806
docs = [(d[3:] if d[0:3] == 'en/' else d) for d in docs]
807
return docs
808
809
def help_documents(s=u""):
810
"""
811
Appends and returns a tabular list of documents, including a
812
shortcut 'all' for all documents, available to the Sage
813
documentation builder.
814
"""
815
s += "DOCUMENTs:\n"
816
s += format_columns(get_documents() + ['all (!)'])
817
s += "(!) Builds everything.\n"
818
return s
819
820
def get_formats():
821
"""
822
Returns a list of output formats the Sage documentation builder
823
will accept on the command-line.
824
"""
825
tut_b = DocBuilder('en/tutorial')
826
formats = tut_b._output_formats()
827
formats.remove('html')
828
return ['html', 'pdf'] + formats
829
830
def help_formats(s=u""):
831
"""
832
Appends and returns a tabular list of output formats available to
833
the Sage documentation builder.
834
"""
835
s += "FORMATs:\n"
836
s += format_columns(get_formats())
837
return s
838
839
def help_commands(name='all', s=u""):
840
"""
841
Appends and returns a tabular list of commands, if any, the Sage
842
documentation builder can run on the indicated document. The
843
default is to list all commands for all documents.
844
"""
845
# To do: Generate the lists dynamically, using class attributes,
846
# as with the Builders above.
847
command_dict = { 'reference' : [
848
'print_included_modules', 'print_modified_modules (*)',
849
'print_unincluded_modules', 'print_newly_included_modules (*)',
850
] }
851
for doc in command_dict:
852
if name == 'all' or doc == name:
853
s += "COMMANDs for the DOCUMENT '" + doc + "':\n"
854
s += format_columns(command_dict[doc])
855
s += "(*) Since the last build.\n"
856
return s
857
858
def help_message_long(option, opt_str, value, parser):
859
"""
860
Prints an extended help message for the Sage documentation builder
861
and exits.
862
"""
863
help_funcs = [ help_usage, help_description, help_documents,
864
help_formats, help_commands, parser.format_option_help,
865
help_examples ]
866
for f in help_funcs:
867
print f()
868
sys.exit(0)
869
870
def help_message_short(option=None, opt_str=None, value=None, parser=None,
871
error=False):
872
"""
873
Prints a help message for the Sage documentation builder. The
874
message includes command-line usage and a list of options. The
875
message is printed only on the first call. If error is True
876
during this call, the message is printed only if the user hasn't
877
requested a list (e.g., documents, formats, commands).
878
"""
879
if not hasattr(parser.values, 'printed_help'):
880
if error == True:
881
if not hasattr(parser.values, 'printed_list'):
882
parser.print_help()
883
else:
884
parser.print_help()
885
setattr(parser.values, 'printed_help', 1)
886
887
def help_wrapper(option, opt_str, value, parser):
888
"""
889
A helper wrapper for command-line options to the Sage
890
documentation builder that print lists, such as document names,
891
formats, and document-specific commands.
892
"""
893
if option.dest == 'commands':
894
print help_commands(value),
895
if option.dest == 'documents':
896
print help_documents(),
897
if option.dest == 'formats':
898
print help_formats(),
899
setattr(parser.values, 'printed_list', 1)
900
901
902
class IndentedHelpFormatter2(optparse.IndentedHelpFormatter, object):
903
"""
904
Custom help formatter class for optparse's OptionParser.
905
"""
906
def format_description(self, description):
907
"""
908
Returns a formatted description, preserving any original
909
explicit new line characters.
910
"""
911
if description:
912
lines_in = description.split('\n')
913
lines_out = [self._format_text(line) for line in lines_in]
914
return "\n".join(lines_out) + "\n"
915
else:
916
return ""
917
918
def format_heading(self, heading):
919
"""
920
Returns a formatted heading using the superclass' formatter.
921
If the heading is 'options', up to case, the function converts
922
it to ALL CAPS. This allows us to match the heading 'OPTIONS' with
923
the same token in the builder's usage message.
924
"""
925
if heading.lower() == 'options':
926
heading = "OPTIONS"
927
return super(IndentedHelpFormatter2, self).format_heading(heading)
928
929
def setup_parser():
930
"""
931
Sets up and returns a command-line OptionParser instance for the
932
Sage documentation builder.
933
"""
934
# Documentation: http://docs.python.org/library/optparse.html
935
parser = optparse.OptionParser(add_help_option=False,
936
usage=help_usage(compact=True),
937
formatter=IndentedHelpFormatter2(),
938
description=help_description(compact=True))
939
940
# Standard options. Note: We use explicit option.dest names
941
# to avoid ambiguity.
942
standard = optparse.OptionGroup(parser, "Standard")
943
standard.add_option("-h", "--help",
944
action="callback", callback=help_message_short,
945
help="show a help message and exit")
946
standard.add_option("-H", "--help-all",
947
action="callback", callback=help_message_long,
948
help="show an extended help message and exit")
949
standard.add_option("-D", "--documents", dest="documents",
950
action="callback", callback=help_wrapper,
951
help="list all available DOCUMENTs")
952
standard.add_option("-F", "--formats", dest="formats",
953
action="callback", callback=help_wrapper,
954
help="list all output FORMATs")
955
standard.add_option("-C", "--commands", dest="commands",
956
type="string", metavar="DOC",
957
action="callback", callback=help_wrapper,
958
help="list all COMMANDs for DOCUMENT DOC; use 'all' to list all")
959
960
standard.add_option("-i", "--inherited", dest="inherited",
961
default=False, action="store_true",
962
help="include inherited members in reference manual; may be slow, may fail for PDF output")
963
standard.add_option("-u", "--underscore", dest="underscore",
964
default=False, action="store_true",
965
help="include variables prefixed with '_' in reference manual; may be slow, may fail for PDF output")
966
967
standard.add_option("-j", "--jsmath", dest="jsmath",
968
action="store_true",
969
help="render math using jsMath; FORMATs: html, json, pickle, web")
970
standard.add_option("--no-pdf-links", dest="no_pdf_links",
971
action="store_true",
972
help="do not include PDF links in DOCUMENT 'website'; FORMATs: html, json, pickle, web")
973
standard.add_option("--warn-links", dest="warn_links",
974
default=False, action="store_true",
975
help="issue a warning whenever a link is not properly resolved; equivalent to '--sphinx-opts -n' (sphinx option: nitpicky)")
976
standard.add_option("--check-nested", dest="check_nested",
977
action="store_true",
978
help="check picklability of nested classes in DOCUMENT 'reference'")
979
standard.add_option("-N", "--no-colors", dest="color", default=True,
980
action="store_false",
981
help="do not color output; does not affect children")
982
standard.add_option("-q", "--quiet", dest="verbose",
983
action="store_const", const=0,
984
help="work quietly; same as --verbose=0")
985
standard.add_option("-v", "--verbose", dest="verbose",
986
type="int", default=1, metavar="LEVEL",
987
action="store",
988
help="report progress at LEVEL=0 (quiet), 1 (normal), 2 (info), or 3 (debug); does not affect children")
989
parser.add_option_group(standard)
990
991
# Advanced options.
992
advanced = optparse.OptionGroup(parser, "Advanced",
993
"Use these options with care.")
994
advanced.add_option("-S", "--sphinx-opts", dest="sphinx_opts",
995
type="string", metavar="OPTS",
996
action="store",
997
help="pass comma-separated OPTS to sphinx-build")
998
advanced.add_option("-U", "--update-mtimes", dest="update_mtimes",
999
default=False, action="store_true",
1000
help="before building reference manual, update modification times for auto-generated ReST files")
1001
parser.add_option_group(advanced)
1002
1003
return parser
1004
1005
def setup_logger(verbose=1, color=True):
1006
"""
1007
Sets up and returns a Python Logger instance for the Sage
1008
documentation builder. The optional argument sets logger's level
1009
and message format.
1010
"""
1011
# Set up colors. Adapted from sphinx.cmdline.
1012
import sphinx.util.console as c
1013
if not color or not sys.stdout.isatty() or not c.color_terminal():
1014
c.nocolor()
1015
1016
# Available colors: black, darkgray, (dark)red, dark(green),
1017
# brown, yellow, (dark)blue, purple, fuchsia, turquoise, teal,
1018
# lightgray, white. Available styles: reset, bold, faint,
1019
# standout, underline, blink.
1020
1021
# Set up log record formats.
1022
format_std = "%(message)s"
1023
formatter = logging.Formatter(format_std)
1024
1025
# format_debug = "%(module)s #%(lineno)s %(funcName)s() %(message)s"
1026
fields = ['%(module)s', '#%(lineno)s', '%(funcName)s()', '%(message)s']
1027
colors = ['darkblue', 'darkred', 'brown', 'reset']
1028
styles = ['reset', 'reset', 'reset', 'reset']
1029
format_debug = ""
1030
for i in xrange(len(fields)):
1031
format_debug += c.colorize(styles[i], c.colorize(colors[i], fields[i]))
1032
if i != len(fields):
1033
format_debug += " "
1034
1035
# Documentation: http://docs.python.org/library/logging.html
1036
logger = logging.getLogger('doc.common.builder')
1037
1038
# Note: There's also Handler.setLevel(). The argument is the
1039
# lowest severity message that the respective logger or handler
1040
# will pass on. The default levels are DEBUG, INFO, WARNING,
1041
# ERROR, and CRITICAL. We use "WARNING" for normal verbosity and
1042
# "ERROR" for quiet operation. It's possible to define custom
1043
# levels. See the documentation for details.
1044
if verbose == 0:
1045
logger.setLevel(logging.ERROR)
1046
if verbose == 1:
1047
logger.setLevel(logging.WARNING)
1048
if verbose == 2:
1049
logger.setLevel(logging.INFO)
1050
if verbose == 3:
1051
logger.setLevel(logging.DEBUG)
1052
formatter = logging.Formatter(format_debug)
1053
1054
handler = logging.StreamHandler()
1055
handler.setFormatter(formatter)
1056
logger.addHandler(handler)
1057
return logger
1058
1059
1060
if __name__ == '__main__':
1061
# Parse the command-line.
1062
parser = setup_parser()
1063
options, args = parser.parse_args()
1064
1065
# Get the name and type (target format) of the document we are
1066
# trying to build.
1067
try:
1068
name, type = args
1069
except ValueError:
1070
help_message_short(parser=parser, error=True)
1071
sys.exit(1)
1072
1073
# Set up module-wide logging.
1074
logger = setup_logger(options.verbose, options.color)
1075
1076
# Process selected options.
1077
if options.jsmath:
1078
os.environ['SAGE_DOC_JSMATH'] = "True"
1079
1080
if options.check_nested:
1081
os.environ['SAGE_CHECK_NESTED'] = 'True'
1082
1083
if options.underscore:
1084
os.environ['SAGE_DOC_UNDERSCORE'] = "True"
1085
1086
if options.sphinx_opts:
1087
ALLSPHINXOPTS += options.sphinx_opts.replace(',', ' ') + " "
1088
if options.no_pdf_links:
1089
ALLSPHINXOPTS += "-A hide_pdf_links=1 "
1090
if options.warn_links:
1091
ALLSPHINXOPTS += "-n "
1092
1093
1094
# Make sure common/static exists.
1095
mkdir(os.path.join(SAGE_DOC, 'common', 'static'))
1096
1097
# Get the builder and build.
1098
getattr(get_builder(name), type)()
1099
1100