Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/src/sage_docbuild/__main__.py
7364 views
1
# sage.doctest: needs sphinx
2
r"""
3
Sage docbuild main
4
5
This module defines the Sage documentation build command::
6
7
sage --docbuild [OPTIONS] DOCUMENT (FORMAT | COMMAND)
8
9
If ``FORMAT`` is given, it builds ``DOCUMENT`` in ``FORMAT``. If ``COMMAND`` is
10
given, it returns information about ``DOCUMENT``.
11
12
Run ``sage --docbuild`` to get detailed explanations about
13
arguments and options.
14
15
Positional arguments::
16
17
DOCUMENT name of the document to build. It can be either one of
18
the documents listed by -D or 'file=/path/to/FILE' to
19
build documentation for this specific file.
20
FORMAT or COMMAND document output format (or command)
21
22
Standard options::
23
24
-h, --help show a help message and exit
25
-H, --help-all show an extended help message and exit
26
-D, --documents list all available DOCUMENTs
27
-F, --formats list all output FORMATs
28
-C DOC, --commands DOC
29
list all COMMANDs for DOCUMENT DOC; use 'all' to list all
30
-i, --inherited include inherited members in reference manual; may be
31
slow, may fail for PDF output
32
-u, --underscore include variables prefixed with '_' in reference
33
manual; may be slow, may fail for PDF output
34
-j, --mathjax, --jsmath
35
ignored for backwards compatibility
36
--no-plot do not include graphics auto-generated using the '.. plot' markup
37
--include-tests-blocks
38
include TESTS blocks in the reference manual
39
--no-pdf-links do not include PDF links in DOCUMENT 'website';
40
FORMATs: html, json, pickle, web
41
--live-doc make Sage code blocks live for html FORMAT
42
--warn-links issue a warning whenever a link is not properly
43
resolved; equivalent to '--sphinx-opts -n' (sphinx
44
option: nitpicky)
45
--check-nested check picklability of nested classes in DOCUMENT 'reference'
46
--no-prune-empty-dirs
47
do not prune empty directories in the documentation source
48
--use-cdns assume internet connection and use CDNs; in particular,
49
use MathJax CDN
50
-N, --no-colors do not color output; does not affect children
51
-q, --quiet work quietly; same as --verbose=0
52
-v LEVEL, --verbose LEVEL
53
report progress at LEVEL=0 (quiet), 1 (normal), 2
54
(info), or 3 (debug); does not affect children
55
-o DIR, --output DIR if DOCUMENT is a single file ('file=...'), write output
56
to this directory
57
58
Advanced options::
59
60
Use these options with care.
61
62
-S OPTS, --sphinx-opts OPTS
63
pass comma-separated OPTS to sphinx-build; must precede
64
OPTS with '=', as in '-S=-q,-aE' or '-S="-q,-aE"'
65
-U, --update-mtimes before building reference manual, update modification
66
times for auto-generated reST files
67
-k, --keep-going Do not abort on errors but continue as much as possible
68
after an error
69
--all-documents ARG if ARG is 'reference', list all subdocuments of
70
en/reference. If ARG is 'all', list all main documents
71
"""
72
73
import argparse
74
import logging
75
import os
76
import sys
77
from pathlib import Path
78
79
import sphinx.ext.intersphinx
80
81
from . import build_options
82
from .build_options import BuildOptions
83
from .builders import (
84
DocBuilder,
85
get_all_documents,
86
get_all_reference_documents,
87
get_builder,
88
)
89
90
logger = logging.getLogger(__name__)
91
92
93
def format_columns(lst, align='<', cols=None, indent=4, pad=3, width=80):
94
"""
95
Utility function that formats a list as a simple table and returns
96
a Unicode string representation.
97
98
The number of columns is
99
computed from the other options, unless it's passed as a keyword
100
argument. For help on Python's string formatter, see
101
102
https://docs.python.org/library/string.html#format-string-syntax
103
"""
104
# Can we generalize this (efficiently) to other / multiple inputs
105
# and generators?
106
size = max(map(len, lst)) + pad
107
if cols is None:
108
import math
109
cols = math.trunc((width - indent) / size)
110
s = " " * indent
111
for i in range(len(lst)):
112
if i != 0 and i % cols == 0:
113
s += "\n" + " " * indent
114
s += "{0:{1}{2}}".format(lst[i], align, size)
115
s += "\n"
116
return s
117
118
119
def help_usage(s="", compact=False):
120
"""
121
Append and return a brief usage message for the Sage documentation builder.
122
123
If 'compact' is False, the function adds a final newline character.
124
"""
125
s += "sage --docbuild [OPTIONS] DOCUMENT (FORMAT | COMMAND)"
126
if not compact:
127
s += "\n"
128
return s
129
130
131
def help_description(s="", compact=False):
132
"""
133
Append and return a brief description of the Sage documentation builder.
134
135
If 'compact' is ``False``, the function adds a final newline character.
136
"""
137
s += "Build or return information about Sage documentation. "
138
s += "A DOCUMENT and either a FORMAT or a COMMAND are required."
139
if not compact:
140
s += "\n"
141
return s
142
143
144
def help_examples(s=""):
145
"""
146
Append and return some usage examples for the Sage documentation builder.
147
"""
148
s += "Examples:\n"
149
s += " sage --docbuild -C all\n"
150
s += " sage --docbuild constructions pdf\n"
151
s += " sage --docbuild reference html -jv3\n"
152
s += " sage --docbuild reference print_unincluded_modules\n"
153
s += " sage --docbuild developer html --sphinx-opts='-q,-aE' --verbose 2"
154
return s
155
156
157
def help_documents():
158
"""
159
Append and return a tabular list of documents, including a
160
shortcut 'all' for all documents, available to the Sage
161
documentation builder.
162
"""
163
docs = get_documents()
164
s = "DOCUMENTs:\n"
165
s += format_columns(docs)
166
s += "\n"
167
if 'reference' in docs:
168
s += "Other valid document names take the form 'reference/DIR', where\n"
169
s += "DIR is a subdirectory of src/doc/en/reference/.\n"
170
s += "This builds just the specified part of the reference manual.\n"
171
s += "DOCUMENT may also have the form 'file=/path/to/FILE', which builds\n"
172
s += "the documentation for the specified file.\n"
173
return s
174
175
176
def get_formats():
177
"""
178
Return a list of output formats the Sage documentation builder
179
will accept on the command-line.
180
"""
181
tut_b = DocBuilder('en/tutorial', BuildOptions())
182
formats = tut_b._output_formats()
183
formats.remove('html')
184
return ['html', 'pdf'] + formats
185
186
187
def help_formats():
188
"""
189
Append and return a tabular list of output formats available to
190
the Sage documentation builder.
191
"""
192
return "FORMATs:\n" + format_columns(get_formats())
193
194
195
def help_commands(name='all'):
196
"""
197
Append and return a tabular list of commands, if any, the Sage
198
documentation builder can run on the indicated document. The
199
default is to list all commands for all documents.
200
"""
201
# To do: Generate the lists dynamically, using class attributes,
202
# as with the Builders above.
203
s = ""
204
command_dict = {'reference': [
205
'print_included_modules', 'print_modified_modules (*)',
206
'print_unincluded_modules', 'print_new_and_updated_modules (*)']}
207
for doc in command_dict:
208
if name == 'all' or doc == name:
209
s += "COMMANDs for the DOCUMENT '" + doc + "':\n"
210
s += format_columns(command_dict[doc])
211
s += "(*) Since the last build.\n"
212
return s
213
214
215
class help_message_long(argparse.Action):
216
"""
217
Print an extended help message for the Sage documentation builder
218
and exits.
219
"""
220
def __call__(self, parser, namespace, values, option_string=None):
221
help_funcs = [help_usage, help_description, help_documents,
222
help_formats, help_commands]
223
for f in help_funcs:
224
print(f())
225
parser.print_help()
226
print(help_examples())
227
sys.exit(0)
228
229
230
class help_message_short(argparse.Action):
231
"""
232
Print a help message for the Sage documentation builder.
233
234
The message includes command-line usage and a list of options.
235
The message is printed only on the first call. If error is True
236
during this call, the message is printed only if the user hasn't
237
requested a list (e.g., documents, formats, commands).
238
"""
239
def __call__(self, parser, namespace, values, option_string=None):
240
if not hasattr(namespace, 'printed_help'):
241
parser.print_help()
242
setattr(namespace, 'printed_help', 1)
243
sys.exit(0)
244
245
246
class help_wrapper(argparse.Action):
247
"""
248
A helper wrapper for command-line options to the Sage
249
documentation builder that print lists, such as document names,
250
formats, and document-specific commands.
251
"""
252
def __call__(self, parser, namespace, values, option_string=None):
253
if option_string in ['-D', '--documents']:
254
print(help_documents(), end="")
255
if option_string in ['-F', '--formats']:
256
print(help_formats(), end="")
257
if self.dest == 'commands':
258
print(help_commands(values), end="")
259
setattr(namespace, 'printed_list', 1)
260
sys.exit(0)
261
262
263
def setup_parser():
264
"""
265
Set up and return a command-line ArgumentParser instance for the
266
Sage documentation builder.
267
"""
268
# Documentation: https://docs.python.org/library/argparse.html
269
parser = argparse.ArgumentParser(usage=help_usage(compact=True),
270
description=help_description(compact=True),
271
add_help=False)
272
# Standard options. Note: We use explicit option.dest names
273
# to avoid ambiguity.
274
standard = parser.add_argument_group("Standard")
275
standard.add_argument("-h", "--help", nargs=0, action=help_message_short,
276
help="show a help message and exit")
277
standard.add_argument("-H", "--help-all", nargs=0, action=help_message_long,
278
help="show an extended help message and exit")
279
standard.add_argument("-D", "--documents", nargs=0, action=help_wrapper,
280
help="list all available DOCUMENTs")
281
standard.add_argument("-F", "--formats", nargs=0, action=help_wrapper,
282
help="list all output FORMATs")
283
standard.add_argument("-C", "--commands", dest="commands",
284
type=str, metavar="DOC", action=help_wrapper,
285
help="list all COMMANDs for DOCUMENT DOC; use 'all' to list all")
286
standard.add_argument("-i", "--inherited", dest="inherited",
287
action="store_true",
288
help="include inherited members in reference manual; may be slow, may fail for PDF output")
289
standard.add_argument("-u", "--underscore", dest="underscore",
290
action="store_true",
291
help="include variables prefixed with '_' in reference manual; may be slow, may fail for PDF output")
292
standard.add_argument("-j", "--mathjax", "--jsmath", dest="mathjax",
293
action="store_true",
294
help="ignored for backwards compatibility")
295
standard.add_argument("--no-plot", dest="no_plot",
296
action="store_true",
297
help="do not include graphics auto-generated using the '.. plot' markup")
298
standard.add_argument("--include-tests-blocks", dest="skip_tests", default=True,
299
action="store_false",
300
help="include TESTS blocks in the reference manual")
301
standard.add_argument("--no-pdf-links", dest="no_pdf_links",
302
action="store_true",
303
help="do not include PDF links in DOCUMENT 'website'; FORMATs: html, json, pickle, web")
304
standard.add_argument("--live-doc", dest="live_doc",
305
action="store_true",
306
help="make Sage code blocks live for html FORMAT")
307
standard.add_argument("--warn-links", dest="warn_links",
308
action="store_true",
309
help="issue a warning whenever a link is not properly resolved; equivalent to '--sphinx-opts -n' (sphinx option: nitpicky)")
310
standard.add_argument("--check-nested", dest="check_nested",
311
action="store_true",
312
help="check picklability of nested classes in DOCUMENT 'reference'")
313
standard.add_argument("--no-prune-empty-dirs", dest="no_prune_empty_dirs",
314
action="store_true",
315
help="do not prune empty directories in the documentation source")
316
standard.add_argument("--use-cdns", dest="use_cdns", default=False,
317
action="store_true",
318
help="assume internet connection and use CDNs; in particular, use MathJax CDN")
319
standard.add_argument("-N", "--no-colors", dest="color",
320
action="store_false",
321
help="do not color output; does not affect children")
322
standard.add_argument("-q", "--quiet", dest="verbose",
323
action="store_const", const=0,
324
help="work quietly; same as --verbose=0")
325
standard.add_argument("-v", "--verbose", dest="verbose",
326
type=int, default=1, metavar="LEVEL",
327
action="store",
328
help="report progress at LEVEL=0 (quiet), 1 (normal), 2 (info), or 3 (debug); does not affect children")
329
standard.add_argument("-s", "--source", dest="source_dir", type=Path,
330
default=None, metavar="DIR", action="store",
331
help="directory containing the documentation source files")
332
standard.add_argument("-o", "--output", dest="output_dir", default=None,
333
type=Path,
334
metavar="DIR", action="store",
335
help="if DOCUMENT is a single file ('file=...'), write output to this directory")
336
337
# Advanced options.
338
advanced = parser.add_argument_group("Advanced",
339
"Use these options with care.")
340
advanced.add_argument("-S", "--sphinx-opts", dest="sphinx_opts",
341
type=str, metavar="OPTS",
342
action="store",
343
help="pass comma-separated OPTS to sphinx-build; must precede OPTS with '=', as in '-S=-q,-aE' or '-S=\"-q,-aE\"'")
344
advanced.add_argument("-U", "--update-mtimes", dest="update_mtimes",
345
action="store_true",
346
help="before building reference manual, update modification times for auto-generated reST files")
347
advanced.add_argument("-k", "--keep-going", dest="keep_going",
348
action="store_true",
349
help="Do not abort on errors but continue as much as possible after an error")
350
advanced.add_argument("--all-documents", dest="all_documents",
351
type=str, metavar="ARG",
352
choices=['all', 'reference'],
353
help="if ARG is 'reference', list all subdocuments"
354
" of en/reference. If ARG is 'all', list all main"
355
" documents")
356
parser.add_argument("document", nargs='?', type=str, metavar="DOCUMENT",
357
help="name of the document to build. It can be either one of the documents listed by -D or 'file=/path/to/FILE' to build documentation for this specific file.")
358
parser.add_argument("format", nargs='?', type=str,
359
metavar="FORMAT or COMMAND", help='document output format (or command)')
360
return parser
361
362
363
def setup_logger(verbose=1, color=True):
364
r"""
365
Set up a Python Logger instance for the Sage documentation builder.
366
367
The optional argument sets logger's level and message format.
368
369
EXAMPLES::
370
371
sage: from sage_docbuild.__main__ import setup_logger, logger
372
sage: setup_logger()
373
sage: type(logger)
374
<class 'logging.Logger'>
375
"""
376
# Set up colors. Adapted from sphinx.cmdline.
377
import sphinx.util.console as c
378
if not color or not sys.stdout.isatty() or not c.color_terminal():
379
c.nocolor()
380
381
# Available colors: black, darkgray, (dark)red, dark(green),
382
# brown, yellow, (dark)blue, purple, fuchsia, turquoise, teal,
383
# lightgray, white. Available styles: reset, bold, faint,
384
# standout, underline, blink.
385
386
# Set up log record formats.
387
format_std = "%(message)s"
388
formatter = logging.Formatter(format_std)
389
390
# format_debug = "%(module)s #%(lineno)s %(funcName)s() %(message)s"
391
fields = ['%(module)s', '#%(lineno)s', '%(funcName)s()', '%(message)s']
392
colors = ['darkblue', 'darkred', 'brown', 'reset']
393
styles = ['reset', 'reset', 'reset', 'reset']
394
format_debug = ""
395
for i in range(len(fields)):
396
format_debug += c.colorize(styles[i], c.colorize(colors[i], fields[i]))
397
if i != len(fields):
398
format_debug += " "
399
400
# Note: There's also Handler.setLevel(). The argument is the
401
# lowest severity message that the respective logger or handler
402
# will pass on. The default levels are DEBUG, INFO, WARNING,
403
# ERROR, and CRITICAL. We use "WARNING" for normal verbosity and
404
# "ERROR" for quiet operation. It's possible to define custom
405
# levels. See the documentation for details.
406
if verbose == 0:
407
logger.setLevel(logging.ERROR)
408
if verbose == 1:
409
logger.setLevel(logging.WARNING)
410
if verbose == 2:
411
logger.setLevel(logging.INFO)
412
if verbose == 3:
413
logger.setLevel(logging.DEBUG)
414
formatter = logging.Formatter(format_debug)
415
416
handler = logging.StreamHandler()
417
handler.setFormatter(formatter)
418
logger.addHandler(handler)
419
420
421
class IntersphinxCache:
422
"""
423
Replace sphinx.ext.intersphinx.fetch_inventory by an in-memory
424
cached version.
425
"""
426
def __init__(self):
427
self.inventories = {}
428
self.real_fetch_inventory = sphinx.ext.intersphinx.fetch_inventory
429
sphinx.ext.intersphinx.fetch_inventory = self.fetch_inventory
430
431
def fetch_inventory(self, app, uri, inv):
432
"""
433
Return the result of ``sphinx.ext.intersphinx.fetch_inventory()``
434
from a cache if possible. Otherwise, call
435
``sphinx.ext.intersphinx.fetch_inventory()`` and cache the result.
436
"""
437
t = (uri, inv)
438
try:
439
return self.inventories[t]
440
except KeyError:
441
i = self.real_fetch_inventory(app, uri, inv)
442
self.inventories[t] = i
443
return i
444
445
446
def main():
447
# Parse the command-line.
448
parser = setup_parser()
449
args: BuildOptions = parser.parse_args() # type: ignore
450
451
# Check that the docs source directory exists
452
if args.source_dir is None:
453
args.source_dir = Path(os.environ.get('SAGE_DOC_SRC', 'src/doc'))
454
args.source_dir = args.source_dir.absolute()
455
if not args.source_dir.is_dir():
456
parser.error(f"Source directory {args.source_dir} does not exist.")
457
458
if args.all_documents:
459
if args.all_documents == 'reference':
460
docs = get_all_reference_documents(args.source_dir / 'en')
461
elif args.all_documents == 'all':
462
docs = get_all_documents(args.source_dir)
463
else:
464
parser.error(f"Unknown argument {args.all_documents} for --all-documents.")
465
for d in docs:
466
print(d.as_posix())
467
sys.exit(0)
468
469
# Check that the docs output directory exists
470
if args.output_dir is None:
471
args.output_dir = Path(os.environ.get('SAGE_DOC', 'src/doc'))
472
args.output_dir = args.output_dir.absolute()
473
if not args.output_dir.exists():
474
try:
475
args.output_dir.mkdir(parents=True)
476
except Exception as e:
477
parser.error(f"Failed to create output directory {args.output_dir}: {e}")
478
479
# Get the name and type (target format) of the document we are
480
# trying to build.
481
name, typ = args.document, args.format
482
if not name or not typ:
483
parser.print_help()
484
sys.exit(1)
485
486
# Set up module-wide logging.
487
setup_logger(args.verbose, args.color)
488
489
def excepthook(*exc_info):
490
logger.error('Error building the documentation.', exc_info=exc_info)
491
logger.info('''
492
Note: incremental documentation builds sometimes cause spurious
493
error messages. To be certain that these are real errors, run
494
"make doc-clean doc-uninstall" first and try again.''')
495
496
sys.excepthook = excepthook
497
498
# Set up the environment based on the command-line options
499
if args.check_nested:
500
os.environ['SAGE_CHECK_NESTED'] = 'True'
501
if args.underscore:
502
os.environ['SAGE_DOC_UNDERSCORE'] = "True"
503
if args.sphinx_opts:
504
build_options.ALLSPHINXOPTS += args.sphinx_opts.replace(',', ' ') + " "
505
if args.no_pdf_links:
506
build_options.WEBSITESPHINXOPTS = " -A hide_pdf_links=1 "
507
if args.warn_links:
508
build_options.ALLSPHINXOPTS += "-n "
509
if args.no_plot:
510
os.environ['SAGE_SKIP_PLOT_DIRECTIVE'] = 'yes'
511
if args.live_doc:
512
os.environ['SAGE_LIVE_DOC'] = 'yes'
513
if args.skip_tests:
514
os.environ['SAGE_SKIP_TESTS_BLOCKS'] = 'True'
515
if args.use_cdns:
516
os.environ['SAGE_USE_CDNS'] = 'yes'
517
os.environ['SAGE_DOC_SRC'] = str(args.source_dir)
518
os.environ['SAGE_DOC'] = str(args.output_dir)
519
520
build_options.ABORT_ON_ERROR = not args.keep_going
521
522
# Set up Intersphinx cache
523
_ = IntersphinxCache()
524
525
builder = get_builder(name, args)
526
527
if not args.no_prune_empty_dirs:
528
# Delete empty directories. This is needed in particular for empty
529
# directories due to "git checkout" which never deletes empty
530
# directories it leaves behind. See Issue #20010.
531
# Issue #31948: This is not parallelization-safe; use the option
532
# --no-prune-empty-dirs to turn it off
533
for dirpath, dirnames, filenames in os.walk(args.source_dir, topdown=False):
534
if not dirnames + filenames:
535
logger.warning('Deleting empty directory {0}'.format(dirpath))
536
os.rmdir(dirpath)
537
538
import sage.all # TODO: Remove once all modules can be imported independently # noqa: F401
539
540
build = getattr(builder, typ)
541
build()
542
543
544
if __name__ == '__main__':
545
sys.exit(main())
546
547