1"""
2    sphinx.setup_command
3    ~~~~~~~~~~~~~~~~~~~~
4
5    Setuptools/distutils commands to assist the building of sphinx
6    documentation.
7
8    :author: Sebastian Wiesner
9    :contact: basti.wiesner@gmx.net
10    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
11    :license: BSD, see LICENSE for details.
12"""
13
14import os
15import sys
16from distutils.cmd import Command
17from distutils.errors import DistutilsExecError
18from io import StringIO
19
20from sphinx.application import Sphinx
21from sphinx.cmd.build import handle_exception
22from sphinx.util.console import color_terminal, nocolor
23from sphinx.util.docutils import docutils_namespace, patch_docutils
24from sphinx.util.osutil import abspath
25
26if False:
27    # For type annotation
28    from typing import Any, Dict  # NOQA
29
30
31class BuildDoc(Command):
32    """
33    Distutils command to build Sphinx documentation.
34
35    The Sphinx build can then be triggered from distutils, and some Sphinx
36    options can be set in ``setup.py`` or ``setup.cfg`` instead of Sphinx own
37    configuration file.
38
39    For instance, from `setup.py`::
40
41       # this is only necessary when not using setuptools/distribute
42       from sphinx.setup_command import BuildDoc
43       cmdclass = {'build_sphinx': BuildDoc}
44
45       name = 'My project'
46       version = '1.2'
47       release = '1.2.0'
48       setup(
49           name=name,
50           author='Bernard Montgomery',
51           version=release,
52           cmdclass=cmdclass,
53           # these are optional and override conf.py settings
54           command_options={
55               'build_sphinx': {
56                   'project': ('setup.py', name),
57                   'version': ('setup.py', version),
58                   'release': ('setup.py', release)}},
59       )
60
61    Or add this section in ``setup.cfg``::
62
63       [build_sphinx]
64       project = 'My project'
65       version = 1.2
66       release = 1.2.0
67    """
68
69    description = 'Build Sphinx documentation'
70    user_options = [
71        ('fresh-env', 'E', 'discard saved environment'),
72        ('all-files', 'a', 'build all files'),
73        ('source-dir=', 's', 'Source directory'),
74        ('build-dir=', None, 'Build directory'),
75        ('config-dir=', 'c', 'Location of the configuration directory'),
76        ('builder=', 'b', 'The builder (or builders) to use. Can be a comma- '
77         'or space-separated list. Defaults to "html"'),
78        ('warning-is-error', 'W', 'Turn warning into errors'),
79        ('project=', None, 'The documented project\'s name'),
80        ('version=', None, 'The short X.Y version'),
81        ('release=', None, 'The full version, including alpha/beta/rc tags'),
82        ('today=', None, 'How to format the current date, used as the '
83         'replacement for |today|'),
84        ('link-index', 'i', 'Link index.html to the master doc'),
85        ('copyright', None, 'The copyright string'),
86        ('pdb', None, 'Start pdb on exception'),
87        ('verbosity', 'v', 'increase verbosity (can be repeated)'),
88        ('nitpicky', 'n', 'nit-picky mode, warn about all missing references'),
89        ('keep-going', None, 'With -W, keep going when getting warnings'),
90    ]
91    boolean_options = ['fresh-env', 'all-files', 'warning-is-error',
92                       'link-index', 'nitpicky']
93
94    def initialize_options(self):
95        # type: () -> None
96        self.fresh_env = self.all_files = False
97        self.pdb = False
98        self.source_dir = self.build_dir = None  # type: str
99        self.builder = 'html'
100        self.warning_is_error = False
101        self.project = ''
102        self.version = ''
103        self.release = ''
104        self.today = ''
105        self.config_dir = None  # type: str
106        self.link_index = False
107        self.copyright = ''
108        # Link verbosity to distutils' (which uses 1 by default).
109        self.verbosity = self.distribution.verbose - 1  # type: ignore
110        self.traceback = False
111        self.nitpicky = False
112        self.keep_going = False
113
114    def _guess_source_dir(self):
115        # type: () -> str
116        for guess in ('doc', 'docs'):
117            if not os.path.isdir(guess):
118                continue
119            for root, dirnames, filenames in os.walk(guess):
120                if 'conf.py' in filenames:
121                    return root
122        return os.curdir
123
124    def finalize_options(self):
125        # type: () -> None
126        self.ensure_string_list('builder')
127
128        if self.source_dir is None:
129            self.source_dir = self._guess_source_dir()
130            self.announce('Using source directory %s' % self.source_dir)
131
132        self.ensure_dirname('source_dir')
133
134        if self.config_dir is None:
135            self.config_dir = self.source_dir
136
137        if self.build_dir is None:
138            build = self.get_finalized_command('build')
139            self.build_dir = os.path.join(abspath(build.build_base), 'sphinx')  # type: ignore
140
141        self.doctree_dir = os.path.join(self.build_dir, 'doctrees')
142
143        self.builder_target_dirs = [
144            (builder, os.path.join(self.build_dir, builder))
145            for builder in self.builder]
146
147    def run(self):
148        # type: () -> None
149        if not color_terminal():
150            nocolor()
151        if not self.verbose:  # type: ignore
152            status_stream = StringIO()
153        else:
154            status_stream = sys.stdout  # type: ignore
155        confoverrides = {}  # type: Dict[str, Any]
156        if self.project:
157            confoverrides['project'] = self.project
158        if self.version:
159            confoverrides['version'] = self.version
160        if self.release:
161            confoverrides['release'] = self.release
162        if self.today:
163            confoverrides['today'] = self.today
164        if self.copyright:
165            confoverrides['copyright'] = self.copyright
166        if self.nitpicky:
167            confoverrides['nitpicky'] = self.nitpicky
168
169        for builder, builder_target_dir in self.builder_target_dirs:
170            app = None
171
172            try:
173                confdir = self.config_dir or self.source_dir
174                with patch_docutils(confdir), docutils_namespace():
175                    app = Sphinx(self.source_dir, self.config_dir,
176                                 builder_target_dir, self.doctree_dir,
177                                 builder, confoverrides, status_stream,
178                                 freshenv=self.fresh_env,
179                                 warningiserror=self.warning_is_error,
180                                 verbosity=self.verbosity, keep_going=self.keep_going)
181                    app.build(force_all=self.all_files)
182                    if app.statuscode:
183                        raise DistutilsExecError(
184                            'caused by %s builder.' % app.builder.name)
185            except Exception as exc:
186                handle_exception(app, self, exc, sys.stderr)
187                if not self.pdb:
188                    raise SystemExit(1) from exc
189
190            if not self.link_index:
191                continue
192
193            src = app.config.master_doc + app.builder.out_suffix  # type: ignore
194            dst = app.builder.get_outfilename('index')  # type: ignore
195            os.symlink(src, dst)
196