1 2import os 3import pkgutil 4import re 5import shutil 6import subprocess 7import sys 8from distutils.version import LooseVersion 9 10from distutils import log 11 12from sphinx.setup_command import BuildDoc as SphinxBuildDoc 13 14SUBPROCESS_TEMPLATE = """ 15import os 16import sys 17 18{build_main} 19 20os.chdir({srcdir!r}) 21 22{sys_path_inserts} 23 24for builder in {builders!r}: 25 retcode = build_main(argv={argv!r} + ['-b', builder, '.', os.path.join({output_dir!r}, builder)]) 26 if retcode != 0: 27 sys.exit(retcode) 28""" 29 30 31def ensure_sphinx_astropy_installed(): 32 """ 33 Make sure that sphinx-astropy is available. 34 """ 35 36 try: 37 from sphinx_astropy import __version__ as sphinx_astropy_version # noqa 38 except ImportError: 39 sphinx_astropy_version = None 40 41 if (sphinx_astropy_version is None 42 or LooseVersion(sphinx_astropy_version) < LooseVersion('1.2')): 43 raise ImportError("sphinx-astropy 1.2 or later needs to be installed to build " 44 "the documentation.") 45 46 47class AstropyBuildDocs(SphinxBuildDoc): 48 """ 49 A version of the ``build_docs`` command that uses the version of Astropy 50 that is built by the setup ``build`` command, rather than whatever is 51 installed on the system. To build docs against the installed version, run 52 ``make html`` in the ``astropy/docs`` directory. 53 """ 54 55 description = 'Build Sphinx documentation for Astropy environment' 56 user_options = SphinxBuildDoc.user_options[:] 57 user_options.append( 58 ('warnings-returncode', 'w', 59 'Parses the sphinx output and sets the return code to 1 if there ' 60 'are any warnings. Note that this will cause the sphinx log to ' 61 'only update when it completes, rather than continuously as is ' 62 'normally the case.')) 63 user_options.append( 64 ('clean-docs', 'l', 65 'Completely clean previous builds, including ' 66 'automodapi-generated files before building new ones')) 67 user_options.append( 68 ('no-intersphinx', 'n', 69 'Skip intersphinx, even if conf.py says to use it')) 70 user_options.append( 71 ('open-docs-in-browser', 'o', 72 'Open the docs in a browser (using the webbrowser module) if the ' 73 'build finishes successfully.')) 74 user_options.append( 75 ('parallel=', 'j', 76 'Build the docs in parallel on the specified number of ' 77 'processes. If "auto", all the cores on the machine will be ' 78 'used.')) 79 80 boolean_options = SphinxBuildDoc.boolean_options[:] 81 boolean_options.append('warnings-returncode') 82 boolean_options.append('clean-docs') 83 boolean_options.append('no-intersphinx') 84 boolean_options.append('open-docs-in-browser') 85 86 _self_iden_rex = re.compile(r"self\.([^\d\W][\w]+)", re.UNICODE) 87 88 def initialize_options(self): 89 SphinxBuildDoc.initialize_options(self) 90 self.clean_docs = False 91 self.no_intersphinx = False 92 self.open_docs_in_browser = False 93 self.warnings_returncode = False 94 self.traceback = False 95 self.parallel = None 96 97 def finalize_options(self): 98 99 # This has to happen before we call the parent class's finalize_options 100 if self.build_dir is None: 101 self.build_dir = 'docs/_build' 102 103 SphinxBuildDoc.finalize_options(self) 104 105 # Clear out previous sphinx builds, if requested 106 if self.clean_docs: 107 108 dirstorm = [os.path.join(self.source_dir, 'api'), 109 os.path.join(self.source_dir, 'generated')] 110 111 dirstorm.append(self.build_dir) 112 113 for d in dirstorm: 114 if os.path.isdir(d): 115 log.info('Cleaning directory ' + d) 116 shutil.rmtree(d) 117 else: 118 log.info('Not cleaning directory ' + d + ' because ' 119 'not present or not a directory') 120 121 def run(self): 122 123 # TODO: Break this method up into a few more subroutines and 124 # document them better 125 import webbrowser 126 127 from urllib.request import pathname2url 128 129 # This is used at the very end of `run` to decide if sys.exit should 130 # be called. If it's None, it won't be. 131 retcode = None 132 133 # Now make sure Astropy is built and determine where it was built 134 build_cmd = self.reinitialize_command('build') 135 build_cmd.inplace = 0 136 self.run_command('build') 137 build_cmd = self.get_finalized_command('build') 138 build_cmd_path = os.path.abspath(build_cmd.build_lib) 139 140 ah_importer = pkgutil.get_importer('astropy_helpers') 141 if ah_importer is None: 142 ah_path = '.' 143 else: 144 ah_path = os.path.abspath(ah_importer.path) 145 146 build_main = 'from sphinx.cmd.build import build_main' 147 148 # We need to make sure sphinx-astropy is installed 149 ensure_sphinx_astropy_installed() 150 151 sys_path_inserts = [build_cmd_path, ah_path] 152 sys_path_inserts = os.linesep.join(['sys.path.insert(0, {0!r})'.format(path) for path in sys_path_inserts]) 153 154 argv = [] 155 156 if self.warnings_returncode: 157 argv.append('-W') 158 159 if self.no_intersphinx: 160 argv.extend(['-D', 'disable_intersphinx=1']) 161 162 # We now need to adjust the flags based on the parent class's options 163 164 if self.fresh_env: 165 argv.append('-E') 166 167 if self.all_files: 168 argv.append('-a') 169 170 if getattr(self, 'pdb', False): 171 argv.append('-P') 172 173 if getattr(self, 'nitpicky', False): 174 argv.append('-n') 175 176 if self.traceback: 177 argv.append('-T') 178 179 # The default verbosity level is 1, so in that case we just don't add a flag 180 if self.verbose == 0: 181 argv.append('-q') 182 elif self.verbose > 1: 183 argv.append('-v') 184 185 if self.parallel is not None: 186 argv.append(f'-j={self.parallel}') 187 188 if isinstance(self.builder, str): 189 builders = [self.builder] 190 else: 191 builders = self.builder 192 193 subproccode = SUBPROCESS_TEMPLATE.format(build_main=build_main, 194 srcdir=self.source_dir, 195 sys_path_inserts=sys_path_inserts, 196 builders=builders, 197 argv=argv, 198 output_dir=os.path.abspath(self.build_dir)) 199 200 log.debug('Starting subprocess of {0} with python code:\n{1}\n' 201 '[CODE END])'.format(sys.executable, subproccode)) 202 203 proc = subprocess.Popen([sys.executable], stdin=subprocess.PIPE) 204 proc.communicate(subproccode.encode('utf-8')) 205 if proc.returncode != 0: 206 retcode = proc.returncode 207 208 if retcode is None: 209 if self.open_docs_in_browser: 210 if self.builder == 'html': 211 absdir = os.path.abspath(self.builder_target_dir) 212 index_path = os.path.join(absdir, 'index.html') 213 fileurl = 'file://' + pathname2url(index_path) 214 webbrowser.open(fileurl) 215 else: 216 log.warn('open-docs-in-browser option was given, but ' 217 'the builder is not html! Ignoring.') 218 219 # Here we explicitly check proc.returncode since we only want to output 220 # this for cases where the return code really wasn't 0. 221 if proc.returncode: 222 log.warn('Sphinx Documentation subprocess failed with return ' 223 'code ' + str(proc.returncode)) 224 225 if retcode is not None: 226 # this is potentially dangerous in that there might be something 227 # after the call to `setup` in `setup.py`, and exiting here will 228 # prevent that from running. But there's no other apparent way 229 # to signal what the return code should be. 230 sys.exit(retcode) 231 232 233class AstropyBuildSphinx(AstropyBuildDocs): # pragma: no cover 234 def run(self): 235 AstropyBuildDocs.run(self) 236