diff options
Diffstat (limited to 'sphinx/cmd')
-rw-r--r-- | sphinx/cmd/__init__.py | 1 | ||||
-rw-r--r-- | sphinx/cmd/build.py | 345 | ||||
-rw-r--r-- | sphinx/cmd/make_mode.py | 181 | ||||
-rw-r--r-- | sphinx/cmd/quickstart.py | 617 |
4 files changed, 1144 insertions, 0 deletions
diff --git a/sphinx/cmd/__init__.py b/sphinx/cmd/__init__.py new file mode 100644 index 0000000..4d277c5 --- /dev/null +++ b/sphinx/cmd/__init__.py @@ -0,0 +1 @@ +"""Modules for command line executables.""" 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:])) diff --git a/sphinx/cmd/make_mode.py b/sphinx/cmd/make_mode.py new file mode 100644 index 0000000..8b26d9d --- /dev/null +++ b/sphinx/cmd/make_mode.py @@ -0,0 +1,181 @@ +"""sphinx-build -M command-line handling. + +This replaces the old, platform-dependent and once-generated content +of Makefile / make.bat. + +This is in its own module so that importing it is fast. It should not +import the main Sphinx modules (like sphinx.applications, sphinx.builders). +""" + +from __future__ import annotations + +import os +import subprocess +import sys +from os import path +from typing import TYPE_CHECKING + +import sphinx +from sphinx.cmd.build import build_main +from sphinx.util.console import ( # type: ignore[attr-defined] + blue, + bold, + color_terminal, + nocolor, +) +from sphinx.util.osutil import rmtree + +try: + from contextlib import chdir # type: ignore[attr-defined] +except ImportError: + from sphinx.util.osutil import _chdir as chdir + +if TYPE_CHECKING: + from collections.abc import Sequence + +BUILDERS = [ + ("", "html", "to make standalone HTML files"), + ("", "dirhtml", "to make HTML files named index.html in directories"), + ("", "singlehtml", "to make a single large HTML file"), + ("", "pickle", "to make pickle files"), + ("", "json", "to make JSON files"), + ("", "htmlhelp", "to make HTML files and an HTML help project"), + ("", "qthelp", "to make HTML files and a qthelp project"), + ("", "devhelp", "to make HTML files and a Devhelp project"), + ("", "epub", "to make an epub"), + ("", "latex", "to make LaTeX files, you can set PAPER=a4 or PAPER=letter"), + ("posix", "latexpdf", "to make LaTeX and PDF files (default pdflatex)"), + ("posix", "latexpdfja", "to make LaTeX files and run them through platex/dvipdfmx"), + ("", "text", "to make text files"), + ("", "man", "to make manual pages"), + ("", "texinfo", "to make Texinfo files"), + ("posix", "info", "to make Texinfo files and run them through makeinfo"), + ("", "gettext", "to make PO message catalogs"), + ("", "changes", "to make an overview of all changed/added/deprecated items"), + ("", "xml", "to make Docutils-native XML files"), + ("", "pseudoxml", "to make pseudoxml-XML files for display purposes"), + ("", "linkcheck", "to check all external links for integrity"), + ("", "doctest", "to run all doctests embedded in the documentation " + "(if enabled)"), + ("", "coverage", "to run coverage check of the documentation (if enabled)"), + ("", "clean", "to remove everything in the build directory"), +] + + +class Make: + def __init__(self, srcdir: str, builddir: str, opts: Sequence[str]) -> None: + self.srcdir = srcdir + self.builddir = builddir + self.opts = [*opts] + + def builddir_join(self, *comps: str) -> str: + return path.join(self.builddir, *comps) + + def build_clean(self) -> int: + srcdir = path.abspath(self.srcdir) + builddir = path.abspath(self.builddir) + if not path.exists(self.builddir): + return 0 + elif not path.isdir(self.builddir): + print("Error: %r is not a directory!" % self.builddir) + return 1 + elif srcdir == builddir: + print("Error: %r is same as source directory!" % self.builddir) + return 1 + elif path.commonpath([srcdir, builddir]) == builddir: + print("Error: %r directory contains source directory!" % self.builddir) + return 1 + print("Removing everything under %r..." % self.builddir) + for item in os.listdir(self.builddir): + rmtree(self.builddir_join(item)) + return 0 + + def build_help(self) -> None: + if not color_terminal(): + nocolor() + + print(bold("Sphinx v%s" % sphinx.__display_version__)) + print("Please use `make %s' where %s is one of" % ((blue('target'),) * 2)) + for osname, bname, description in BUILDERS: + if not osname or os.name == osname: + print(f' {blue(bname.ljust(10))} {description}') + + def build_latexpdf(self) -> int: + if self.run_generic_build('latex') > 0: + return 1 + + # Use $MAKE to determine the make command + make_fallback = 'make.bat' if sys.platform == 'win32' else 'make' + makecmd = os.environ.get('MAKE', make_fallback) + if not makecmd.lower().startswith('make'): + raise RuntimeError('Invalid $MAKE command: %r' % makecmd) + try: + with chdir(self.builddir_join('latex')): + return subprocess.call([makecmd, 'all-pdf']) + except OSError: + print('Error: Failed to run: %s' % makecmd) + return 1 + + def build_latexpdfja(self) -> int: + if self.run_generic_build('latex') > 0: + return 1 + + # Use $MAKE to determine the make command + make_fallback = 'make.bat' if sys.platform == 'win32' else 'make' + makecmd = os.environ.get('MAKE', make_fallback) + if not makecmd.lower().startswith('make'): + raise RuntimeError('Invalid $MAKE command: %r' % makecmd) + try: + with chdir(self.builddir_join('latex')): + return subprocess.call([makecmd, 'all-pdf']) + except OSError: + print('Error: Failed to run: %s' % makecmd) + return 1 + + def build_info(self) -> int: + if self.run_generic_build('texinfo') > 0: + return 1 + + # Use $MAKE to determine the make command + makecmd = os.environ.get('MAKE', 'make') + if not makecmd.lower().startswith('make'): + raise RuntimeError('Invalid $MAKE command: %r' % makecmd) + try: + with chdir(self.builddir_join('texinfo')): + return subprocess.call([makecmd, 'info']) + except OSError: + print('Error: Failed to run: %s' % makecmd) + return 1 + + def build_gettext(self) -> int: + dtdir = self.builddir_join('gettext', '.doctrees') + if self.run_generic_build('gettext', doctreedir=dtdir) > 0: + return 1 + return 0 + + def run_generic_build(self, builder: str, doctreedir: str | None = None) -> int: + # compatibility with old Makefile + papersize = os.getenv('PAPER', '') + opts = self.opts + if papersize in ('a4', 'letter'): + opts.extend(['-D', 'latex_elements.papersize=' + papersize + 'paper']) + if doctreedir is None: + doctreedir = self.builddir_join('doctrees') + + args = ['-b', builder, + '-d', doctreedir, + self.srcdir, + self.builddir_join(builder)] + return build_main(args + opts) + + +def run_make_mode(args: Sequence[str]) -> int: + if len(args) < 3: + print('Error: at least 3 arguments (builder, source ' + 'dir, build dir) are required.', file=sys.stderr) + return 1 + make = Make(args[1], args[2], args[3:]) + run_method = 'build_' + args[0] + if hasattr(make, run_method): + return getattr(make, run_method)() + return make.run_generic_build(args[0]) diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py new file mode 100644 index 0000000..89aec45 --- /dev/null +++ b/sphinx/cmd/quickstart.py @@ -0,0 +1,617 @@ +"""Quickly setup documentation source to work with Sphinx.""" + +from __future__ import annotations + +import argparse +import locale +import os +import sys +import time +from os import path +from typing import TYPE_CHECKING, Any, Callable + +# try to import readline, unix specific enhancement +try: + import readline + if TYPE_CHECKING and sys.platform == "win32": # always false, for type checking + raise ImportError + READLINE_AVAILABLE = True + if readline.__doc__ and 'libedit' in readline.__doc__: + readline.parse_and_bind("bind ^I rl_complete") + USE_LIBEDIT = True + else: + readline.parse_and_bind("tab: complete") + USE_LIBEDIT = False +except ImportError: + READLINE_AVAILABLE = False + USE_LIBEDIT = False + +from docutils.utils import column_width + +import sphinx.locale +from sphinx import __display_version__, package_dir +from sphinx.locale import __ +from sphinx.util.console import ( # type: ignore[attr-defined] + bold, + color_terminal, + colorize, + nocolor, + red, +) +from sphinx.util.osutil import ensuredir +from sphinx.util.template import SphinxRenderer + +if TYPE_CHECKING: + from collections.abc import Sequence + +EXTENSIONS = { + 'autodoc': __('automatically insert docstrings from modules'), + 'doctest': __('automatically test code snippets in doctest blocks'), + 'intersphinx': __('link between Sphinx documentation of different projects'), + 'todo': __('write "todo" entries that can be shown or hidden on build'), + 'coverage': __('checks for documentation coverage'), + 'imgmath': __('include math, rendered as PNG or SVG images'), + 'mathjax': __('include math, rendered in the browser by MathJax'), + 'ifconfig': __('conditional inclusion of content based on config values'), + 'viewcode': __('include links to the source code of documented Python objects'), + 'githubpages': __('create .nojekyll file to publish the document on GitHub pages'), +} + +DEFAULTS = { + 'path': '.', + 'sep': False, + 'dot': '_', + 'language': None, + 'suffix': '.rst', + 'master': 'index', + 'makefile': True, + 'batchfile': True, +} + +PROMPT_PREFIX = '> ' + +if sys.platform == 'win32': + # On Windows, show questions as bold because of color scheme of PowerShell (refs: #5294). + COLOR_QUESTION = 'bold' +else: + COLOR_QUESTION = 'purple' + + +# function to get input from terminal -- overridden by the test suite +def term_input(prompt: str) -> str: + if sys.platform == 'win32': + # Important: On windows, readline is not enabled by default. In these + # environment, escape sequences have been broken. To avoid the + # problem, quickstart uses ``print()`` to show prompt. + print(prompt, end='') + return input('') + else: + return input(prompt) + + +class ValidationError(Exception): + """Raised for validation errors.""" + + +def is_path(x: str) -> str: + x = path.expanduser(x) + if not path.isdir(x): + raise ValidationError(__("Please enter a valid path name.")) + return x + + +def is_path_or_empty(x: str) -> str: + if x == '': + return x + return is_path(x) + + +def allow_empty(x: str) -> str: + return x + + +def nonempty(x: str) -> str: + if not x: + raise ValidationError(__("Please enter some text.")) + return x + + +def choice(*l: str) -> Callable[[str], str]: + def val(x: str) -> str: + if x not in l: + raise ValidationError(__('Please enter one of %s.') % ', '.join(l)) + return x + return val + + +def boolean(x: str) -> bool: + if x.upper() not in ('Y', 'YES', 'N', 'NO'): + raise ValidationError(__("Please enter either 'y' or 'n'.")) + return x.upper() in ('Y', 'YES') + + +def suffix(x: str) -> str: + if not (x[0:1] == '.' and len(x) > 1): + raise ValidationError(__("Please enter a file suffix, e.g. '.rst' or '.txt'.")) + return x + + +def ok(x: str) -> str: + return x + + +def do_prompt( + text: str, default: str | None = None, validator: Callable[[str], Any] = nonempty, +) -> str | bool: + while True: + if default is not None: + prompt = PROMPT_PREFIX + f'{text} [{default}]: ' + else: + prompt = PROMPT_PREFIX + text + ': ' + if USE_LIBEDIT: + # Note: libedit has a problem for combination of ``input()`` and escape + # sequence (see #5335). To avoid the problem, all prompts are not colored + # on libedit. + pass + elif READLINE_AVAILABLE: + # pass input_mode=True if readline available + prompt = colorize(COLOR_QUESTION, prompt, input_mode=True) + else: + prompt = colorize(COLOR_QUESTION, prompt, input_mode=False) + x = term_input(prompt).strip() + if default and not x: + x = default + try: + x = validator(x) + except ValidationError as err: + print(red('* ' + str(err))) + continue + break + return x + + +class QuickstartRenderer(SphinxRenderer): + def __init__(self, templatedir: str = '') -> None: + self.templatedir = templatedir + super().__init__() + + def _has_custom_template(self, template_name: str) -> bool: + """Check if custom template file exists. + + Note: Please don't use this function from extensions. + It will be removed in the future without deprecation period. + """ + template = path.join(self.templatedir, path.basename(template_name)) + return bool(self.templatedir) and path.exists(template) + + def render(self, template_name: str, context: dict[str, Any]) -> str: + if self._has_custom_template(template_name): + custom_template = path.join(self.templatedir, path.basename(template_name)) + return self.render_from_file(custom_template, context) + else: + return super().render(template_name, context) + + +def ask_user(d: dict[str, Any]) -> None: + """Ask the user for quickstart values missing from *d*. + + Values are: + + * path: root path + * sep: separate source and build dirs (bool) + * dot: replacement for dot in _templates etc. + * project: project name + * author: author names + * version: version of project + * release: release of project + * language: document language + * suffix: source file suffix + * master: master document name + * extensions: extensions to use (list) + * makefile: make Makefile + * batchfile: make command file + """ + + print(bold(__('Welcome to the Sphinx %s quickstart utility.')) % __display_version__) + print() + print(__('Please enter values for the following settings (just press Enter to\n' + 'accept a default value, if one is given in brackets).')) + + if 'path' in d: + print() + print(bold(__('Selected root path: %s')) % d['path']) + else: + print() + print(__('Enter the root path for documentation.')) + d['path'] = do_prompt(__('Root path for the documentation'), '.', is_path) + + while path.isfile(path.join(d['path'], 'conf.py')) or \ + path.isfile(path.join(d['path'], 'source', 'conf.py')): + print() + print(bold(__('Error: an existing conf.py has been found in the ' + 'selected root path.'))) + print(__('sphinx-quickstart will not overwrite existing Sphinx projects.')) + print() + d['path'] = do_prompt(__('Please enter a new root path (or just Enter to exit)'), + '', is_path_or_empty) + if not d['path']: + raise SystemExit(1) + + if 'sep' not in d: + print() + print(__('You have two options for placing the build directory for Sphinx output.\n' + 'Either, you use a directory "_build" within the root path, or you separate\n' + '"source" and "build" directories within the root path.')) + d['sep'] = do_prompt(__('Separate source and build directories (y/n)'), 'n', boolean) + + if 'dot' not in d: + print() + print(__('Inside the root directory, two more directories will be created; "_templates"\n' # noqa: E501 + 'for custom HTML templates and "_static" for custom stylesheets and other static\n' # noqa: E501 + 'files. You can enter another prefix (such as ".") to replace the underscore.')) # noqa: E501 + d['dot'] = do_prompt(__('Name prefix for templates and static dir'), '_', ok) + + if 'project' not in d: + print() + print(__('The project name will occur in several places in the built documentation.')) + d['project'] = do_prompt(__('Project name')) + if 'author' not in d: + d['author'] = do_prompt(__('Author name(s)')) + + if 'version' not in d: + print() + print(__('Sphinx has the notion of a "version" and a "release" for the\n' + 'software. Each version can have multiple releases. For example, for\n' + 'Python the version is something like 2.5 or 3.0, while the release is\n' + "something like 2.5.1 or 3.0a1. If you don't need this dual structure,\n" + 'just set both to the same value.')) + d['version'] = do_prompt(__('Project version'), '', allow_empty) + if 'release' not in d: + d['release'] = do_prompt(__('Project release'), d['version'], allow_empty) + + if 'language' not in d: + print() + print(__( + 'If the documents are to be written in a language other than English,\n' + 'you can select a language here by its language code. Sphinx will then\n' + 'translate text that it generates into that language.\n' + '\n' + 'For a list of supported codes, see\n' + 'https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language.', + )) + d['language'] = do_prompt(__('Project language'), 'en') + if d['language'] == 'en': + d['language'] = None + + if 'suffix' not in d: + print() + print(__('The file name suffix for source files. Commonly, this is either ".txt"\n' + 'or ".rst". Only files with this suffix are considered documents.')) + d['suffix'] = do_prompt(__('Source file suffix'), '.rst', suffix) + + if 'master' not in d: + print() + print(__('One document is special in that it is considered the top node of the\n' + '"contents tree", that is, it is the root of the hierarchical structure\n' + 'of the documents. Normally, this is "index", but if your "index"\n' + 'document is a custom template, you can also set this to another filename.')) + d['master'] = do_prompt(__('Name of your master document (without suffix)'), 'index') + + while path.isfile(path.join(d['path'], d['master'] + d['suffix'])) or \ + path.isfile(path.join(d['path'], 'source', d['master'] + d['suffix'])): + print() + print(bold(__('Error: the master file %s has already been found in the ' + 'selected root path.') % (d['master'] + d['suffix']))) + print(__('sphinx-quickstart will not overwrite the existing file.')) + print() + d['master'] = do_prompt(__('Please enter a new file name, or rename the ' + 'existing file and press Enter'), d['master']) + + if 'extensions' not in d: + print(__('Indicate which of the following Sphinx extensions should be enabled:')) + d['extensions'] = [] + for name, description in EXTENSIONS.items(): + if do_prompt(f'{name}: {description} (y/n)', 'n', boolean): + d['extensions'].append('sphinx.ext.%s' % name) + + # Handle conflicting options + if {'sphinx.ext.imgmath', 'sphinx.ext.mathjax'}.issubset(d['extensions']): + print(__('Note: imgmath and mathjax cannot be enabled at the same time. ' + 'imgmath has been deselected.')) + d['extensions'].remove('sphinx.ext.imgmath') + + if 'makefile' not in d: + print() + print(__('A Makefile and a Windows command file can be generated for you so that you\n' + "only have to run e.g. `make html' instead of invoking sphinx-build\n" + 'directly.')) + d['makefile'] = do_prompt(__('Create Makefile? (y/n)'), 'y', boolean) + + if 'batchfile' not in d: + d['batchfile'] = do_prompt(__('Create Windows command file? (y/n)'), 'y', boolean) + print() + + +def generate( + d: dict, overwrite: bool = True, silent: bool = False, templatedir: str | None = None, +) -> None: + """Generate project based on values in *d*.""" + template = QuickstartRenderer(templatedir or '') + + if 'mastertoctree' not in d: + d['mastertoctree'] = '' + if 'mastertocmaxdepth' not in d: + d['mastertocmaxdepth'] = 2 + + d['root_doc'] = d['master'] + d['now'] = time.asctime() + d['project_underline'] = column_width(d['project']) * '=' + d.setdefault('extensions', []) + d['copyright'] = time.strftime('%Y') + ', ' + d['author'] + + d["path"] = os.path.abspath(d['path']) + ensuredir(d['path']) + + srcdir = path.join(d['path'], 'source') if d['sep'] else d['path'] + + ensuredir(srcdir) + if d['sep']: + builddir = path.join(d['path'], 'build') + d['exclude_patterns'] = '' + else: + builddir = path.join(srcdir, d['dot'] + 'build') + exclude_patterns = map(repr, [ + d['dot'] + 'build', + 'Thumbs.db', '.DS_Store', + ]) + d['exclude_patterns'] = ', '.join(exclude_patterns) + ensuredir(builddir) + ensuredir(path.join(srcdir, d['dot'] + 'templates')) + ensuredir(path.join(srcdir, d['dot'] + 'static')) + + def write_file(fpath: str, content: str, newline: str | None = None) -> None: + if overwrite or not path.isfile(fpath): + if 'quiet' not in d: + print(__('Creating file %s.') % fpath) + with open(fpath, 'w', encoding='utf-8', newline=newline) as f: + f.write(content) + else: + if 'quiet' not in d: + print(__('File %s already exists, skipping.') % fpath) + + conf_path = os.path.join(templatedir, 'conf.py_t') if templatedir else None + if not conf_path or not path.isfile(conf_path): + conf_path = os.path.join(package_dir, 'templates', 'quickstart', 'conf.py_t') + with open(conf_path, encoding="utf-8") as f: + conf_text = f.read() + + write_file(path.join(srcdir, 'conf.py'), template.render_string(conf_text, d)) + + masterfile = path.join(srcdir, d['master'] + d['suffix']) + if template._has_custom_template('quickstart/master_doc.rst_t'): + msg = ('A custom template `master_doc.rst_t` found. It has been renamed to ' + '`root_doc.rst_t`. Please rename it on your project too.') + print(colorize('red', msg)) + write_file(masterfile, template.render('quickstart/master_doc.rst_t', d)) + else: + write_file(masterfile, template.render('quickstart/root_doc.rst_t', d)) + + if d.get('make_mode') is True: + makefile_template = 'quickstart/Makefile.new_t' + batchfile_template = 'quickstart/make.bat.new_t' + else: + makefile_template = 'quickstart/Makefile_t' + batchfile_template = 'quickstart/make.bat_t' + + if d['makefile'] is True: + d['rsrcdir'] = 'source' if d['sep'] else '.' + d['rbuilddir'] = 'build' if d['sep'] else d['dot'] + 'build' + # use binary mode, to avoid writing \r\n on Windows + write_file(path.join(d['path'], 'Makefile'), + template.render(makefile_template, d), '\n') + + if d['batchfile'] is True: + d['rsrcdir'] = 'source' if d['sep'] else '.' + d['rbuilddir'] = 'build' if d['sep'] else d['dot'] + 'build' + write_file(path.join(d['path'], 'make.bat'), + template.render(batchfile_template, d), '\r\n') + + if silent: + return + print() + print(bold(__('Finished: An initial directory structure has been created.'))) + print() + print(__('You should now populate your master file %s and create other documentation\n' + 'source files. ') % masterfile, end='') + if d['makefile'] or d['batchfile']: + print(__('Use the Makefile to build the docs, like so:\n' + ' make builder')) + else: + print(__('Use the sphinx-build command to build the docs, like so:\n' + ' sphinx-build -b builder %s %s') % (srcdir, builddir)) + print(__('where "builder" is one of the supported builders, ' + 'e.g. html, latex or linkcheck.')) + print() + + +def valid_dir(d: dict) -> bool: + dir = d['path'] + if not path.exists(dir): + return True + if not path.isdir(dir): + return False + + if {'Makefile', 'make.bat'} & set(os.listdir(dir)): + return False + + if d['sep']: + dir = os.path.join('source', dir) + if not path.exists(dir): + return True + if not path.isdir(dir): + return False + + reserved_names = [ + 'conf.py', + d['dot'] + 'static', + d['dot'] + 'templates', + d['master'] + d['suffix'], + ] + if set(reserved_names) & set(os.listdir(dir)): + return False + + return True + + +def get_parser() -> argparse.ArgumentParser: + description = __( + "\n" + "Generate required files for a Sphinx project.\n" + "\n" + "sphinx-quickstart is an interactive tool that asks some questions about your\n" + "project and then generates a complete documentation directory and sample\n" + "Makefile to be used with sphinx-build.\n", + ) + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTIONS] <PROJECT_DIR>', + epilog=__("For more information, visit <https://www.sphinx-doc.org/>."), + description=description) + + parser.add_argument('-q', '--quiet', action='store_true', dest='quiet', + default=None, + help=__('quiet mode')) + parser.add_argument('--version', action='version', dest='show_version', + version='%%(prog)s %s' % __display_version__) + + parser.add_argument('path', metavar='PROJECT_DIR', default='.', nargs='?', + help=__('project root')) + + group = parser.add_argument_group(__('Structure options')) + group.add_argument('--sep', action='store_true', dest='sep', default=None, + help=__('if specified, separate source and build dirs')) + group.add_argument('--no-sep', action='store_false', dest='sep', + help=__('if specified, create build dir under source dir')) + group.add_argument('--dot', metavar='DOT', default='_', + help=__('replacement for dot in _templates etc.')) + + group = parser.add_argument_group(__('Project basic options')) + group.add_argument('-p', '--project', metavar='PROJECT', dest='project', + help=__('project name')) + group.add_argument('-a', '--author', metavar='AUTHOR', dest='author', + help=__('author names')) + group.add_argument('-v', metavar='VERSION', dest='version', default='', + help=__('version of project')) + group.add_argument('-r', '--release', metavar='RELEASE', dest='release', + help=__('release of project')) + group.add_argument('-l', '--language', metavar='LANGUAGE', dest='language', + help=__('document language')) + group.add_argument('--suffix', metavar='SUFFIX', default='.rst', + help=__('source file suffix')) + group.add_argument('--master', metavar='MASTER', default='index', + help=__('master document name')) + group.add_argument('--epub', action='store_true', default=False, + help=__('use epub')) + + group = parser.add_argument_group(__('Extension options')) + for ext in EXTENSIONS: + group.add_argument('--ext-%s' % ext, action='append_const', + const='sphinx.ext.%s' % ext, dest='extensions', + help=__('enable %s extension') % ext) + group.add_argument('--extensions', metavar='EXTENSIONS', dest='extensions', + action='append', help=__('enable arbitrary extensions')) + + group = parser.add_argument_group(__('Makefile and Batchfile creation')) + group.add_argument('--makefile', action='store_true', dest='makefile', default=True, + help=__('create makefile')) + group.add_argument('--no-makefile', action='store_false', dest='makefile', + help=__('do not create makefile')) + group.add_argument('--batchfile', action='store_true', dest='batchfile', default=True, + help=__('create batchfile')) + group.add_argument('--no-batchfile', action='store_false', + dest='batchfile', + help=__('do not create batchfile')) + group.add_argument('-m', '--use-make-mode', action='store_true', + dest='make_mode', default=True, + help=__('use make-mode for Makefile/make.bat')) + group.add_argument('-M', '--no-use-make-mode', action='store_false', + dest='make_mode', + help=__('do not use make-mode for Makefile/make.bat')) + + group = parser.add_argument_group(__('Project templating')) + group.add_argument('-t', '--templatedir', metavar='TEMPLATEDIR', + dest='templatedir', + help=__('template directory for template files')) + group.add_argument('-d', metavar='NAME=VALUE', action='append', + dest='variables', + help=__('define a template variable')) + + return parser + + +def main(argv: Sequence[str] = (), /) -> int: + locale.setlocale(locale.LC_ALL, '') + sphinx.locale.init_console() + + if not color_terminal(): + nocolor() + + # parse options + parser = get_parser() + try: + args = parser.parse_args(argv or sys.argv[1:]) + except SystemExit as err: + return err.code # type: ignore[return-value] + + d = vars(args) + # delete None or False value + d = {k: v for k, v in d.items() if v is not None} + + # handle use of CSV-style extension values + d.setdefault('extensions', []) + for ext in d['extensions'][:]: + if ',' in ext: + d['extensions'].remove(ext) + d['extensions'].extend(ext.split(',')) + + try: + if 'quiet' in d: + if not {'project', 'author'}.issubset(d): + print(__('"quiet" is specified, but any of "project" or ' + '"author" is not specified.')) + return 1 + + if {'quiet', 'project', 'author'}.issubset(d): + # quiet mode with all required params satisfied, use default + d.setdefault('version', '') + d.setdefault('release', d['version']) + d2 = DEFAULTS.copy() + d2.update(d) + d = d2 + + if not valid_dir(d): + print() + print(bold(__('Error: specified path is not a directory, or sphinx' + ' files already exist.'))) + print(__('sphinx-quickstart only generate into a empty directory.' + ' Please specify a new root path.')) + return 1 + else: + ask_user(d) + except (KeyboardInterrupt, EOFError): + print() + print('[Interrupted.]') + return 130 # 128 + SIGINT + + for variable in d.get('variables', []): + try: + name, value = variable.split('=') + d[name] = value + except ValueError: + print(__('Invalid template variable: %s') % variable) + + generate(d, overwrite=False, templatedir=args.templatedir) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) |