diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
commit | cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a (patch) | |
tree | 18dcde1a8d1f5570a77cd0c361de3b490d02c789 /sphinx/cmd/quickstart.py | |
parent | Initial commit. (diff) | |
download | sphinx-upstream/7.2.6.tar.xz sphinx-upstream/7.2.6.zip |
Adding upstream version 7.2.6.upstream/7.2.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sphinx/cmd/quickstart.py')
-rw-r--r-- | sphinx/cmd/quickstart.py | 617 |
1 files changed, 617 insertions, 0 deletions
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:])) |