r"""
Sage docbuild main
This module defines the Sage documentation build command::
sage --docbuild [OPTIONS] DOCUMENT (FORMAT | COMMAND)
If ``FORMAT`` is given, it builds ``DOCUMENT`` in ``FORMAT``. If ``COMMAND`` is
given, it returns information about ``DOCUMENT``.
Run ``sage --docbuild`` to get detailed explanations about
arguments and options.
Positional arguments::
DOCUMENT 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.
FORMAT or COMMAND document output format (or command)
Standard options::
-h, --help show a help message and exit
-H, --help-all show an extended help message and exit
-D, --documents list all available DOCUMENTs
-F, --formats list all output FORMATs
-C DOC, --commands DOC
list all COMMANDs for DOCUMENT DOC; use 'all' to list all
-i, --inherited include inherited members in reference manual; may be
slow, may fail for PDF output
-u, --underscore include variables prefixed with '_' in reference
manual; may be slow, may fail for PDF output
-j, --mathjax, --jsmath
ignored for backwards compatibility
--no-plot do not include graphics auto-generated using the '.. plot' markup
--no-preparsed-examples
do not show preparsed versions of EXAMPLES blocks
--include-tests-blocks
include TESTS blocks in the reference manual
--no-pdf-links do not include PDF links in DOCUMENT 'website';
FORMATs: html, json, pickle, web
--live-doc make Sage code blocks live for html FORMAT
--warn-links issue a warning whenever a link is not properly
resolved; equivalent to '--sphinx-opts -n' (sphinx
option: nitpicky)
--check-nested check picklability of nested classes in DOCUMENT 'reference'
--no-prune-empty-dirs
do not prune empty directories in the documentation source
--use-cdns assume internet connection and use CDNs; in particular,
use MathJax CDN
-N, --no-colors do not color output; does not affect children
-q, --quiet work quietly; same as --verbose=0
-v LEVEL, --verbose LEVEL
report progress at LEVEL=0 (quiet), 1 (normal), 2
(info), or 3 (debug); does not affect children
-o DIR, --output DIR if DOCUMENT is a single file ('file=...'), write output
to this directory
Advanced options::
Use these options with care.
-S OPTS, --sphinx-opts OPTS
pass comma-separated OPTS to sphinx-build; must precede
OPTS with '=', as in '-S=-q,-aE' or '-S="-q,-aE"'
-U, --update-mtimes before building reference manual, update modification
times for auto-generated reST files
-k, --keep-going Do not abort on errors but continue as much as possible
after an error
--all-documents ARG if ARG is 'reference', list all subdocuments of
en/reference. If ARG is 'all', list all main documents
"""
import argparse
import logging
import os
import sys
from pathlib import Path
import sphinx.ext.intersphinx
from . import build_options
from .build_options import BuildOptions
from .builders import (
DocBuilder,
get_all_documents,
get_all_reference_documents,
get_builder,
)
logger = logging.getLogger(__name__)
def format_columns(lst, align='<', cols=None, indent=4, pad=3, width=80):
"""
Utility function that formats a list as a simple table and returns
a Unicode string representation.
The number of columns is
computed from the other options, unless it's passed as a keyword
argument. For help on Python's string formatter, see
https://docs.python.org/library/string.html#format-string-syntax
"""
size = max(map(len, lst)) + pad
if cols is None:
import math
cols = math.trunc((width - indent) / size)
s = " " * indent
for i in range(len(lst)):
if i != 0 and i % cols == 0:
s += "\n" + " " * indent
s += "{0:{1}{2}}".format(lst[i], align, size)
s += "\n"
return s
def help_usage(s="", compact=False):
"""
Append and return a brief usage message for the Sage documentation builder.
If 'compact' is False, the function adds a final newline character.
"""
s += "sage --docbuild [OPTIONS] DOCUMENT (FORMAT | COMMAND)"
if not compact:
s += "\n"
return s
def help_description(s="", compact=False):
"""
Append and return a brief description of the Sage documentation builder.
If 'compact' is ``False``, the function adds a final newline character.
"""
s += "Build or return information about Sage documentation. "
s += "A DOCUMENT and either a FORMAT or a COMMAND are required."
if not compact:
s += "\n"
return s
def help_examples(s=""):
"""
Append and return some usage examples for the Sage documentation builder.
"""
s += "Examples:\n"
s += " sage --docbuild -C all\n"
s += " sage --docbuild constructions pdf\n"
s += " sage --docbuild reference html -jv3\n"
s += " sage --docbuild reference print_unincluded_modules\n"
s += " sage --docbuild developer html --sphinx-opts='-q,-aE' --verbose 2"
return s
def help_documents():
"""
Append and return a tabular list of documents, including a
shortcut 'all' for all documents, available to the Sage
documentation builder.
"""
docs = get_documents()
s = "DOCUMENTs:\n"
s += format_columns(docs)
s += "\n"
if 'reference' in docs:
s += "Other valid document names take the form 'reference/DIR', where\n"
s += "DIR is a subdirectory of src/doc/en/reference/.\n"
s += "This builds just the specified part of the reference manual.\n"
s += "DOCUMENT may also have the form 'file=/path/to/FILE', which builds\n"
s += "the documentation for the specified file.\n"
return s
def get_formats():
"""
Return a list of output formats the Sage documentation builder
will accept on the command-line.
"""
tut_b = DocBuilder('en/tutorial', BuildOptions())
formats = tut_b._output_formats()
formats.remove('html')
return ['html', 'pdf'] + formats
def help_formats():
"""
Append and return a tabular list of output formats available to
the Sage documentation builder.
"""
return "FORMATs:\n" + format_columns(get_formats())
def help_commands(name='all'):
"""
Append and return a tabular list of commands, if any, the Sage
documentation builder can run on the indicated document. The
default is to list all commands for all documents.
"""
s = ""
command_dict = {'reference': [
'print_included_modules', 'print_modified_modules (*)',
'print_unincluded_modules', 'print_new_and_updated_modules (*)']}
for doc in command_dict:
if name == 'all' or doc == name:
s += "COMMANDs for the DOCUMENT '" + doc + "':\n"
s += format_columns(command_dict[doc])
s += "(*) Since the last build.\n"
return s
class help_message_long(argparse.Action):
"""
Print an extended help message for the Sage documentation builder
and exits.
"""
def __call__(self, parser, namespace, values, option_string=None):
help_funcs = [help_usage, help_description, help_documents,
help_formats, help_commands]
for f in help_funcs:
print(f())
parser.print_help()
print(help_examples())
sys.exit(0)
class help_message_short(argparse.Action):
"""
Print a help message for the Sage documentation builder.
The message includes command-line usage and a list of options.
The message is printed only on the first call. If error is True
during this call, the message is printed only if the user hasn't
requested a list (e.g., documents, formats, commands).
"""
def __call__(self, parser, namespace, values, option_string=None):
if not hasattr(namespace, 'printed_help'):
parser.print_help()
setattr(namespace, 'printed_help', 1)
sys.exit(0)
class help_wrapper(argparse.Action):
"""
A helper wrapper for command-line options to the Sage
documentation builder that print lists, such as document names,
formats, and document-specific commands.
"""
def __call__(self, parser, namespace, values, option_string=None):
if option_string in ['-D', '--documents']:
print(help_documents(), end="")
if option_string in ['-F', '--formats']:
print(help_formats(), end="")
if self.dest == 'commands':
print(help_commands(values), end="")
setattr(namespace, 'printed_list', 1)
sys.exit(0)
def setup_parser():
"""
Set up and return a command-line ArgumentParser instance for the
Sage documentation builder.
"""
parser = argparse.ArgumentParser(usage=help_usage(compact=True),
description=help_description(compact=True),
add_help=False)
standard = parser.add_argument_group("Standard")
standard.add_argument("-h", "--help", nargs=0, action=help_message_short,
help="show a help message and exit")
standard.add_argument("-H", "--help-all", nargs=0, action=help_message_long,
help="show an extended help message and exit")
standard.add_argument("-D", "--documents", nargs=0, action=help_wrapper,
help="list all available DOCUMENTs")
standard.add_argument("-F", "--formats", nargs=0, action=help_wrapper,
help="list all output FORMATs")
standard.add_argument("-C", "--commands", dest="commands",
type=str, metavar="DOC", action=help_wrapper,
help="list all COMMANDs for DOCUMENT DOC; use 'all' to list all")
standard.add_argument("-i", "--inherited", dest="inherited",
action="store_true",
help="include inherited members in reference manual; may be slow, may fail for PDF output")
standard.add_argument("-u", "--underscore", dest="underscore",
action="store_true",
help="include variables prefixed with '_' in reference manual; may be slow, may fail for PDF output")
standard.add_argument("-j", "--mathjax", "--jsmath", dest="mathjax",
action="store_true",
help="ignored for backwards compatibility")
standard.add_argument("--no-plot", dest="no_plot",
action="store_true",
help="do not include graphics auto-generated using the '.. plot' markup")
standard.add_argument("--no-preparsed-examples", dest="no_preparsed_examples",
action="store_true",
help="do not show preparsed versions of EXAMPLES blocks")
standard.add_argument("--include-tests-blocks", dest="skip_tests", default=True,
action="store_false",
help="include TESTS blocks in the reference manual")
standard.add_argument("--no-pdf-links", dest="no_pdf_links",
action="store_true",
help="do not include PDF links in DOCUMENT 'website'; FORMATs: html, json, pickle, web")
standard.add_argument("--live-doc", dest="live_doc",
action="store_true",
help="make Sage code blocks live for html FORMAT")
standard.add_argument("--warn-links", dest="warn_links",
action="store_true",
help="issue a warning whenever a link is not properly resolved; equivalent to '--sphinx-opts -n' (sphinx option: nitpicky)")
standard.add_argument("--check-nested", dest="check_nested",
action="store_true",
help="check picklability of nested classes in DOCUMENT 'reference'")
standard.add_argument("--no-prune-empty-dirs", dest="no_prune_empty_dirs",
action="store_true",
help="do not prune empty directories in the documentation source")
standard.add_argument("--use-cdns", dest="use_cdns", default=False,
action="store_true",
help="assume internet connection and use CDNs; in particular, use MathJax CDN")
standard.add_argument("-N", "--no-colors", dest="color",
action="store_false",
help="do not color output; does not affect children")
standard.add_argument("-q", "--quiet", dest="verbose",
action="store_const", const=0,
help="work quietly; same as --verbose=0")
standard.add_argument("-v", "--verbose", dest="verbose",
type=int, default=1, metavar="LEVEL",
action="store",
help="report progress at LEVEL=0 (quiet), 1 (normal), 2 (info), or 3 (debug); does not affect children")
standard.add_argument("-s", "--source", dest="source_dir", type=Path,
default=None, metavar="DIR", action="store",
help="directory containing the documentation source files")
standard.add_argument("-o", "--output", dest="output_dir", default=None,
type=Path,
metavar="DIR", action="store",
help="if DOCUMENT is a single file ('file=...'), write output to this directory")
advanced = parser.add_argument_group("Advanced",
"Use these options with care.")
advanced.add_argument("-S", "--sphinx-opts", dest="sphinx_opts",
type=str, metavar="OPTS",
action="store",
help="pass comma-separated OPTS to sphinx-build; must precede OPTS with '=', as in '-S=-q,-aE' or '-S=\"-q,-aE\"'")
advanced.add_argument("-U", "--update-mtimes", dest="update_mtimes",
action="store_true",
help="before building reference manual, update modification times for auto-generated reST files")
advanced.add_argument("-k", "--keep-going", dest="keep_going",
action="store_true",
help="Do not abort on errors but continue as much as possible after an error")
advanced.add_argument("--all-documents", dest="all_documents",
type=str, metavar="ARG",
choices=['all', 'reference'],
help="if ARG is 'reference', list all subdocuments"
" of en/reference. If ARG is 'all', list all main"
" documents")
parser.add_argument("document", nargs='?', type=str, metavar="DOCUMENT",
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.")
parser.add_argument("format", nargs='?', type=str,
metavar="FORMAT or COMMAND", help='document output format (or command)')
return parser
def setup_logger(verbose=1, color=True):
r"""
Set up a Python Logger instance for the Sage documentation builder.
The optional argument sets logger's level and message format.
EXAMPLES::
sage: from sage_docbuild.__main__ import setup_logger, logger
sage: setup_logger()
sage: type(logger)
<class 'logging.Logger'>
"""
import sphinx.util.console as c
if not color or not sys.stdout.isatty() or not c.color_terminal():
c.nocolor()
format_std = "%(message)s"
formatter = logging.Formatter(format_std)
fields = ['%(module)s', '#%(lineno)s', '%(funcName)s()', '%(message)s']
colors = ['darkblue', 'darkred', 'brown', 'reset']
styles = ['reset', 'reset', 'reset', 'reset']
format_debug = ""
for i in range(len(fields)):
format_debug += c.colorize(styles[i], c.colorize(colors[i], fields[i]))
if i != len(fields):
format_debug += " "
if verbose == 0:
logger.setLevel(logging.ERROR)
if verbose == 1:
logger.setLevel(logging.WARNING)
if verbose == 2:
logger.setLevel(logging.INFO)
if verbose == 3:
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter(format_debug)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
class IntersphinxCache:
"""
Replace sphinx.ext.intersphinx.fetch_inventory by an in-memory
cached version.
"""
def __init__(self):
self.inventories = {}
self.real_fetch_inventory = sphinx.ext.intersphinx.fetch_inventory
sphinx.ext.intersphinx.fetch_inventory = self.fetch_inventory
def fetch_inventory(self, app, uri, inv):
"""
Return the result of ``sphinx.ext.intersphinx.fetch_inventory()``
from a cache if possible. Otherwise, call
``sphinx.ext.intersphinx.fetch_inventory()`` and cache the result.
"""
t = (uri, inv)
try:
return self.inventories[t]
except KeyError:
i = self.real_fetch_inventory(app, uri, inv)
self.inventories[t] = i
return i
def main():
parser = setup_parser()
args: BuildOptions = parser.parse_args()
if args.source_dir is None:
args.source_dir = Path(os.environ.get('SAGE_DOC_SRC', 'src/doc'))
args.source_dir = args.source_dir.absolute()
if not args.source_dir.is_dir():
parser.error(f"Source directory {args.source_dir} does not exist.")
if args.all_documents:
if args.all_documents == 'reference':
docs = get_all_reference_documents(args.source_dir / 'en')
elif args.all_documents == 'all':
docs = get_all_documents(args.source_dir)
else:
parser.error(f"Unknown argument {args.all_documents} for --all-documents.")
for d in docs:
print(d.as_posix())
sys.exit(0)
if args.output_dir is None:
args.output_dir = Path(os.environ.get('SAGE_DOC', 'src/doc'))
args.output_dir = args.output_dir.absolute()
if not args.output_dir.exists():
try:
args.output_dir.mkdir(parents=True)
except Exception as e:
parser.error(f"Failed to create output directory {args.output_dir}: {e}")
name, typ = args.document, args.format
if not name or not typ:
parser.print_help()
sys.exit(1)
setup_logger(args.verbose, args.color)
def excepthook(*exc_info):
logger.error('Error building the documentation.', exc_info=exc_info)
logger.info('''
Note: incremental documentation builds sometimes cause spurious
error messages. To be certain that these are real errors, run
"make doc-clean doc-uninstall" first and try again.''')
sys.excepthook = excepthook
if args.check_nested:
os.environ['SAGE_CHECK_NESTED'] = 'True'
if args.underscore:
os.environ['SAGE_DOC_UNDERSCORE'] = "True"
if args.sphinx_opts:
build_options.ALLSPHINXOPTS += args.sphinx_opts.replace(',', ' ') + " "
if args.no_pdf_links:
build_options.WEBSITESPHINXOPTS = " -A hide_pdf_links=1 "
if args.warn_links:
build_options.ALLSPHINXOPTS += "-n "
if args.no_plot:
os.environ['SAGE_SKIP_PLOT_DIRECTIVE'] = 'yes'
if args.no_preparsed_examples:
os.environ['SAGE_PREPARSED_DOC'] = 'no'
if args.live_doc:
os.environ['SAGE_LIVE_DOC'] = 'yes'
if args.skip_tests:
os.environ['SAGE_SKIP_TESTS_BLOCKS'] = 'True'
if args.use_cdns:
os.environ['SAGE_USE_CDNS'] = 'yes'
os.environ['SAGE_DOC_SRC'] = str(args.source_dir)
os.environ['SAGE_DOC'] = str(args.output_dir)
build_options.ABORT_ON_ERROR = not args.keep_going
_ = IntersphinxCache()
builder = get_builder(name, args)
if not args.no_prune_empty_dirs:
for dirpath, dirnames, filenames in os.walk(args.source_dir, topdown=False):
if not dirnames + filenames:
logger.warning('Deleting empty directory {0}'.format(dirpath))
os.rmdir(dirpath)
import sage.all
build = getattr(builder, typ)
build()
if __name__ == '__main__':
sys.exit(main())