1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
|
from __future__ import print_function
import inspect
import os
import pkgutil
import re
import shutil
import subprocess
import sys
import textwrap
import warnings
from distutils import log
from distutils.cmd import DistutilsOptionError
import sphinx
from sphinx.setup_command import BuildDoc as SphinxBuildDoc
from ..utils import minversion, AstropyDeprecationWarning
PY3 = sys.version_info[0] >= 3
class AstropyBuildDocs(SphinxBuildDoc):
"""
A version of the ``build_docs`` command that uses the version of Astropy
that is built by the setup ``build`` command, rather than whatever is
installed on the system. To build docs against the installed version, run
``make html`` in the ``astropy/docs`` directory.
This also automatically creates the docs/_static directories--this is
needed because GitHub won't create the _static dir because it has no
tracked files.
"""
description = 'Build Sphinx documentation for Astropy environment'
user_options = SphinxBuildDoc.user_options[:]
user_options.append(
('warnings-returncode', 'w',
'Parses the sphinx output and sets the return code to 1 if there '
'are any warnings. Note that this will cause the sphinx log to '
'only update when it completes, rather than continuously as is '
'normally the case.'))
user_options.append(
('clean-docs', 'l',
'Completely clean previous builds, including '
'automodapi-generated files before building new ones'))
user_options.append(
('no-intersphinx', 'n',
'Skip intersphinx, even if conf.py says to use it'))
user_options.append(
('open-docs-in-browser', 'o',
'Open the docs in a browser (using the webbrowser module) if the '
'build finishes successfully.'))
boolean_options = SphinxBuildDoc.boolean_options[:]
boolean_options.append('warnings-returncode')
boolean_options.append('clean-docs')
boolean_options.append('no-intersphinx')
boolean_options.append('open-docs-in-browser')
_self_iden_rex = re.compile(r"self\.([^\d\W][\w]+)", re.UNICODE)
def initialize_options(self):
SphinxBuildDoc.initialize_options(self)
self.clean_docs = False
self.no_intersphinx = False
self.open_docs_in_browser = False
self.warnings_returncode = False
def finalize_options(self):
# Clear out previous sphinx builds, if requested
if self.clean_docs:
dirstorm = [os.path.join(self.source_dir, 'api'),
os.path.join(self.source_dir, 'generated')]
if self.build_dir is None:
dirstorm.append('docs/_build')
else:
dirstorm.append(self.build_dir)
for d in dirstorm:
if os.path.isdir(d):
log.info('Cleaning directory ' + d)
shutil.rmtree(d)
else:
log.info('Not cleaning directory ' + d + ' because '
'not present or not a directory')
SphinxBuildDoc.finalize_options(self)
def run(self):
# TODO: Break this method up into a few more subroutines and
# document them better
import webbrowser
if PY3:
from urllib.request import pathname2url
else:
from urllib import pathname2url
# This is used at the very end of `run` to decide if sys.exit should
# be called. If it's None, it won't be.
retcode = None
# If possible, create the _static dir
if self.build_dir is not None:
# the _static dir should be in the same place as the _build dir
# for Astropy
basedir, subdir = os.path.split(self.build_dir)
if subdir == '': # the path has a trailing /...
basedir, subdir = os.path.split(basedir)
staticdir = os.path.join(basedir, '_static')
if os.path.isfile(staticdir):
raise DistutilsOptionError(
'Attempted to build_docs in a location where' +
staticdir + 'is a file. Must be a directory.')
self.mkpath(staticdir)
# Now make sure Astropy is built and determine where it was built
build_cmd = self.reinitialize_command('build')
build_cmd.inplace = 0
self.run_command('build')
build_cmd = self.get_finalized_command('build')
build_cmd_path = os.path.abspath(build_cmd.build_lib)
ah_importer = pkgutil.get_importer('astropy_helpers')
ah_path = os.path.abspath(ah_importer.path)
# Now generate the source for and spawn a new process that runs the
# command. This is needed to get the correct imports for the built
# version
runlines, runlineno = inspect.getsourcelines(SphinxBuildDoc.run)
subproccode = textwrap.dedent("""
from sphinx.setup_command import *
os.chdir({srcdir!r})
sys.path.insert(0, {build_cmd_path!r})
sys.path.insert(0, {ah_path!r})
""").format(build_cmd_path=build_cmd_path, ah_path=ah_path,
srcdir=self.source_dir)
# runlines[1:] removes 'def run(self)' on the first line
subproccode += textwrap.dedent(''.join(runlines[1:]))
# All "self.foo" in the subprocess code needs to be replaced by the
# values taken from the current self in *this* process
subproccode = self._self_iden_rex.split(subproccode)
for i in range(1, len(subproccode), 2):
iden = subproccode[i]
val = getattr(self, iden)
if iden.endswith('_dir'):
# Directories should be absolute, because the `chdir` call
# in the new process moves to a different directory
subproccode[i] = repr(os.path.abspath(val))
else:
subproccode[i] = repr(val)
subproccode = ''.join(subproccode)
# This is a quick gross hack, but it ensures that the code grabbed from
# SphinxBuildDoc.run will work in Python 2 if it uses the print
# function
if minversion(sphinx, '1.3'):
subproccode = 'from __future__ import print_function' + subproccode
if self.no_intersphinx:
# the confoverrides variable in sphinx.setup_command.BuildDoc can
# be used to override the conf.py ... but this could well break
# if future versions of sphinx change the internals of BuildDoc,
# so remain vigilant!
subproccode = subproccode.replace(
'confoverrides = {}',
'confoverrides = {\'intersphinx_mapping\':{}}')
log.debug('Starting subprocess of {0} with python code:\n{1}\n'
'[CODE END])'.format(sys.executable, subproccode))
# To return the number of warnings, we need to capture stdout. This
# prevents a continuous updating at the terminal, but there's no
# apparent way around this.
if self.warnings_returncode:
proc = subprocess.Popen([sys.executable, '-c', subproccode],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
retcode = 1
with proc.stdout:
for line in iter(proc.stdout.readline, b''):
line = line.strip(b'\r\n')
print(line.decode('utf-8'))
if 'build succeeded.' == line.decode('utf-8'):
retcode = 0
# Poll to set proc.retcode
proc.wait()
if retcode != 0:
if os.environ.get('TRAVIS', None) == 'true':
# this means we are in the travis build, so customize
# the message appropriately.
msg = ('The build_docs travis build FAILED '
'because sphinx issued documentation '
'warnings (scroll up to see the warnings).')
else: # standard failure message
msg = ('build_docs returning a non-zero exit '
'code because sphinx issued documentation '
'warnings.')
log.warn(msg)
else:
proc = subprocess.Popen([sys.executable], stdin=subprocess.PIPE)
proc.communicate(subproccode.encode('utf-8'))
if proc.returncode == 0:
if self.open_docs_in_browser:
if self.builder == 'html':
absdir = os.path.abspath(self.builder_target_dir)
index_path = os.path.join(absdir, 'index.html')
fileurl = 'file://' + pathname2url(index_path)
webbrowser.open(fileurl)
else:
log.warn('open-docs-in-browser option was given, but '
'the builder is not html! Ignoring.')
else:
log.warn('Sphinx Documentation subprocess failed with return '
'code ' + str(proc.returncode))
retcode = proc.returncode
if retcode is not None:
# this is potentially dangerous in that there might be something
# after the call to `setup` in `setup.py`, and exiting here will
# prevent that from running. But there's no other apparent way
# to signal what the return code should be.
sys.exit(retcode)
class AstropyBuildSphinx(AstropyBuildDocs): # pragma: no cover
description = 'deprecated alias to the build_docs command'
def run(self):
warnings.warn(
'The "build_sphinx" command is now deprecated. Use'
'"build_docs" instead.', AstropyDeprecationWarning)
AstropyBuildDocs.run(self)
|