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