summaryrefslogtreecommitdiffstats
path: root/sphinx/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/cmd')
-rw-r--r--sphinx/cmd/__init__.py1
-rw-r--r--sphinx/cmd/build.py345
-rw-r--r--sphinx/cmd/make_mode.py181
-rw-r--r--sphinx/cmd/quickstart.py617
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:]))