diff options
Diffstat (limited to 'sphinx/cmd/build.py')
-rw-r--r-- | sphinx/cmd/build.py | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py new file mode 100644 index 0000000..3ee0ceb --- /dev/null +++ b/sphinx/cmd/build.py @@ -0,0 +1,345 @@ +"""Build documentation from a provided source.""" + +from __future__ import annotations + +import argparse +import bdb +import contextlib +import locale +import multiprocessing +import os +import pdb # NoQA: T100 +import sys +import traceback +from os import path +from typing import TYPE_CHECKING, Any, TextIO + +from docutils.utils import SystemMessage + +import sphinx.locale +from sphinx import __display_version__ +from sphinx.application import Sphinx +from sphinx.errors import SphinxError, SphinxParallelError +from sphinx.locale import __ +from sphinx.util import Tee +from sphinx.util.console import ( # type: ignore[attr-defined] + color_terminal, + nocolor, + red, + terminal_safe, +) +from sphinx.util.docutils import docutils_namespace, patch_docutils +from sphinx.util.exceptions import format_exception_cut_frames, save_traceback +from sphinx.util.osutil import ensuredir + +if TYPE_CHECKING: + from collections.abc import Sequence + + +def handle_exception( + app: Sphinx | None, args: Any, exception: BaseException, stderr: TextIO = sys.stderr, +) -> None: + if isinstance(exception, bdb.BdbQuit): + return + + if args.pdb: + print(red(__('Exception occurred while building, starting debugger:')), + file=stderr) + traceback.print_exc() + pdb.post_mortem(sys.exc_info()[2]) + else: + print(file=stderr) + if args.verbosity or args.traceback: + exc = sys.exc_info()[1] + if isinstance(exc, SphinxParallelError): + exc_format = '(Error in parallel process)\n' + exc.traceback + print(exc_format, file=stderr) + else: + traceback.print_exc(None, stderr) + print(file=stderr) + if isinstance(exception, KeyboardInterrupt): + print(__('Interrupted!'), file=stderr) + elif isinstance(exception, SystemMessage): + print(red(__('reST markup error:')), file=stderr) + print(terminal_safe(exception.args[0]), file=stderr) + elif isinstance(exception, SphinxError): + print(red('%s:' % exception.category), file=stderr) + print(str(exception), file=stderr) + elif isinstance(exception, UnicodeError): + print(red(__('Encoding error:')), file=stderr) + print(terminal_safe(str(exception)), file=stderr) + tbpath = save_traceback(app, exception) + print(red(__('The full traceback has been saved in %s, if you want ' + 'to report the issue to the developers.') % tbpath), + file=stderr) + elif isinstance(exception, RuntimeError) and 'recursion depth' in str(exception): + print(red(__('Recursion error:')), file=stderr) + print(terminal_safe(str(exception)), file=stderr) + print(file=stderr) + print(__('This can happen with very large or deeply nested source ' + 'files. You can carefully increase the default Python ' + 'recursion limit of 1000 in conf.py with e.g.:'), file=stderr) + print(' import sys; sys.setrecursionlimit(1500)', file=stderr) + else: + print(red(__('Exception occurred:')), file=stderr) + print(format_exception_cut_frames().rstrip(), file=stderr) + tbpath = save_traceback(app, exception) + print(red(__('The full traceback has been saved in %s, if you ' + 'want to report the issue to the developers.') % tbpath), + file=stderr) + print(__('Please also report this if it was a user error, so ' + 'that a better error message can be provided next time.'), + file=stderr) + print(__('A bug report can be filed in the tracker at ' + '<https://github.com/sphinx-doc/sphinx/issues>. Thanks!'), + file=stderr) + + +def jobs_argument(value: str) -> int: + """ + Special type to handle 'auto' flags passed to 'sphinx-build' via -j flag. Can + be expanded to handle other special scaling requests, such as setting job count + to cpu_count. + """ + if value == 'auto': + return multiprocessing.cpu_count() + else: + jobs = int(value) + if jobs <= 0: + raise argparse.ArgumentTypeError(__('job number should be a positive number')) + else: + return jobs + + +def get_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTIONS] SOURCEDIR OUTPUTDIR [FILENAMES...]', + epilog=__('For more information, visit <https://www.sphinx-doc.org/>.'), + description=__(""" +Generate documentation from source files. + +sphinx-build generates documentation from the files in SOURCEDIR and places it +in OUTPUTDIR. It looks for 'conf.py' in SOURCEDIR for the configuration +settings. The 'sphinx-quickstart' tool may be used to generate template files, +including 'conf.py' + +sphinx-build can create documentation in different formats. A format is +selected by specifying the builder name on the command line; it defaults to +HTML. Builders can also perform other tasks related to documentation +processing. + +By default, everything that is outdated is built. Output only for selected +files can be built by specifying individual filenames. +""")) + + parser.add_argument('--version', action='version', dest='show_version', + version='%%(prog)s %s' % __display_version__) + + parser.add_argument('sourcedir', + help=__('path to documentation source files')) + parser.add_argument('outputdir', + help=__('path to output directory')) + parser.add_argument('filenames', nargs='*', + help=__('a list of specific files to rebuild. Ignored ' + 'if -a is specified')) + + group = parser.add_argument_group(__('general options')) + group.add_argument('-b', metavar='BUILDER', dest='builder', + default='html', + help=__('builder to use (default: html)')) + group.add_argument('-a', action='store_true', dest='force_all', + help=__('write all files (default: only write new and ' + 'changed files)')) + group.add_argument('-E', action='store_true', dest='freshenv', + help=__("don't use a saved environment, always read " + 'all files')) + group.add_argument('-d', metavar='PATH', dest='doctreedir', + help=__('path for the cached environment and doctree ' + 'files (default: OUTPUTDIR/.doctrees)')) + group.add_argument('-j', '--jobs', metavar='N', default=1, type=jobs_argument, + dest='jobs', + help=__('build in parallel with N processes where ' + 'possible (special value "auto" will set N to cpu-count)')) + group = parser.add_argument_group('build configuration options') + group.add_argument('-c', metavar='PATH', dest='confdir', + help=__('path where configuration file (conf.py) is ' + 'located (default: same as SOURCEDIR)')) + group.add_argument('-C', action='store_true', dest='noconfig', + help=__('use no config file at all, only -D options')) + group.add_argument('-D', metavar='setting=value', action='append', + dest='define', default=[], + help=__('override a setting in configuration file')) + group.add_argument('-A', metavar='name=value', action='append', + dest='htmldefine', default=[], + help=__('pass a value into HTML templates')) + group.add_argument('-t', metavar='TAG', action='append', + dest='tags', default=[], + help=__('define tag: include "only" blocks with TAG')) + group.add_argument('-n', action='store_true', dest='nitpicky', + help=__('nit-picky mode, warn about all missing ' + 'references')) + + group = parser.add_argument_group(__('console output options')) + group.add_argument('-v', action='count', dest='verbosity', default=0, + help=__('increase verbosity (can be repeated)')) + group.add_argument('-q', action='store_true', dest='quiet', + help=__('no output on stdout, just warnings on stderr')) + group.add_argument('-Q', action='store_true', dest='really_quiet', + help=__('no output at all, not even warnings')) + group.add_argument('--color', action='store_const', const='yes', + default='auto', + help=__('do emit colored output (default: auto-detect)')) + group.add_argument('-N', '--no-color', dest='color', action='store_const', + const='no', + help=__('do not emit colored output (default: ' + 'auto-detect)')) + group.add_argument('-w', metavar='FILE', dest='warnfile', + help=__('write warnings (and errors) to given file')) + group.add_argument('-W', action='store_true', dest='warningiserror', + help=__('turn warnings into errors')) + group.add_argument('--keep-going', action='store_true', dest='keep_going', + help=__("with -W, keep going when getting warnings")) + group.add_argument('-T', action='store_true', dest='traceback', + help=__('show full traceback on exception')) + group.add_argument('-P', action='store_true', dest='pdb', + help=__('run Pdb on exception')) + + return parser + + +def make_main(argv: Sequence[str]) -> int: + """Sphinx build "make mode" entry.""" + from sphinx.cmd import make_mode + return make_mode.run_make_mode(argv[1:]) + + +def _parse_arguments(argv: Sequence[str]) -> argparse.Namespace: + parser = get_parser() + args = parser.parse_args(argv) + + if args.noconfig: + args.confdir = None + elif not args.confdir: + args.confdir = args.sourcedir + + if not args.doctreedir: + args.doctreedir = os.path.join(args.outputdir, '.doctrees') + + if args.force_all and args.filenames: + parser.error(__('cannot combine -a option and filenames')) + + if args.color == 'no' or (args.color == 'auto' and not color_terminal()): + nocolor() + + status: TextIO | None = sys.stdout + warning: TextIO | None = sys.stderr + error = sys.stderr + + if args.quiet: + status = None + + if args.really_quiet: + status = warning = None + + if warning and args.warnfile: + try: + warnfile = path.abspath(args.warnfile) + ensuredir(path.dirname(warnfile)) + warnfp = open(args.warnfile, 'w', encoding="utf-8") # NoQA: SIM115 + except Exception as exc: + parser.error(__('cannot open warning file %r: %s') % ( + args.warnfile, exc)) + warning = Tee(warning, warnfp) # type: ignore[assignment] + error = warning + + args.status = status + args.warning = warning + args.error = error + + confoverrides = {} + for val in args.define: + try: + key, val = val.split('=', 1) + except ValueError: + parser.error(__('-D option argument must be in the form name=value')) + confoverrides[key] = val + + for val in args.htmldefine: + try: + key, val = val.split('=') + except ValueError: + parser.error(__('-A option argument must be in the form name=value')) + with contextlib.suppress(ValueError): + val = int(val) + + confoverrides['html_context.%s' % key] = val + + if args.nitpicky: + confoverrides['nitpicky'] = True + + args.confoverrides = confoverrides + + return args + + +def build_main(argv: Sequence[str]) -> int: + """Sphinx build "main" command-line entry.""" + args = _parse_arguments(argv) + + app = None + try: + confdir = args.confdir or args.sourcedir + with patch_docutils(confdir), docutils_namespace(): + app = Sphinx(args.sourcedir, args.confdir, args.outputdir, + args.doctreedir, args.builder, args.confoverrides, args.status, + args.warning, args.freshenv, args.warningiserror, + args.tags, args.verbosity, args.jobs, args.keep_going, + args.pdb) + app.build(args.force_all, args.filenames) + return app.statuscode + except (Exception, KeyboardInterrupt) as exc: + handle_exception(app, args, exc, args.error) + return 2 + + +def _bug_report_info() -> int: + from platform import platform, python_implementation + + import docutils + import jinja2 + import pygments + + print('Please paste all output below into the bug report template\n\n') + print('```text') + print(f'Platform: {sys.platform}; ({platform()})') + print(f'Python version: {sys.version})') + print(f'Python implementation: {python_implementation()}') + print(f'Sphinx version: {sphinx.__display_version__}') + print(f'Docutils version: {docutils.__version__}') + print(f'Jinja2 version: {jinja2.__version__}') + print(f'Pygments version: {pygments.__version__}') + print('```') + return 0 + + +def main(argv: Sequence[str] = (), /) -> int: + locale.setlocale(locale.LC_ALL, '') + sphinx.locale.init_console() + + if not argv: + argv = sys.argv[1:] + + # Allow calling as 'python -m sphinx build …' + if argv[:1] == ['build']: + argv = argv[1:] + + if argv[:1] == ['--bug-report']: + return _bug_report_info() + if argv[:1] == ['-M']: + return make_main(argv) + else: + return build_main(argv) + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) |