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