Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/src/sage_docbuild/sphinxbuild.py
4052 views
1
# sage.doctest: needs sphinx
2
r"""
3
Sphinx build script
4
5
This is Sage's version of the ``sphinx-build`` script. We redirect ``stdout`` and
6
``stderr`` to our own logger, and remove some unwanted chatter.
7
"""
8
# ****************************************************************************
9
# Copyright (C) 2013-2014 Volker Braun <[email protected]>
10
# 2013-2017 J. H. Palmieri <<[email protected]>
11
# 2013-2017 Jeroen Demeyer <[email protected]>
12
# 2014 Christopher Schwan <[email protected]>
13
# 2014 Nicolas M. Thiéry <[email protected]>
14
# 2015 Marc Mezzarobba <[email protected]>
15
# 2015 André Apitzsch <[email protected]>
16
# 2018 Julian Rüth <[email protected]>
17
#
18
# This program is free software: you can redistribute it and/or modify
19
# it under the terms of the GNU General Public License as published by
20
# the Free Software Foundation, either version 2 of the License, or
21
# (at your option) any later version.
22
# https://www.gnu.org/licenses/
23
# ****************************************************************************
24
import os
25
import sys
26
import re
27
import sphinx
28
import sphinx.cmd.build
29
30
31
# override the fancy multi-line formatting
32
def term_width_line(text):
33
return text + '\n'
34
35
36
sphinx.util.console.term_width_line = term_width_line
37
38
39
class SageSphinxLogger():
40
r"""
41
This implements the file object interface to serve as
42
``sys.stdout``/``sys.stderr`` replacement.
43
"""
44
# https://en.wikipedia.org/wiki/ANSI_escape_code
45
ansi_escape_sequence = re.compile(r'''
46
\x1b # ESC
47
\[ # CSI sequence starts
48
[0-?]* # parameter bytes
49
[ -/]* # intermediate bytes
50
[@-~] # final byte
51
''', re.VERBOSE)
52
ansi_escape_sequence_color = re.compile(r'''
53
\x1b # ESC
54
\[ # CSI sequence starts
55
[0-9;]* # parameter bytes
56
# intermediate bytes
57
m # final byte
58
''', re.VERBOSE)
59
60
prefix_len = 9
61
62
def __init__(self, stream, prefix):
63
self._init_chatter()
64
self._stream = stream
65
self._color = stream.isatty()
66
prefix = prefix[0:self.prefix_len]
67
prefix = ('[{0:' + str(self.prefix_len) + '}]').format(prefix)
68
self._is_stdout = (stream.fileno() == 1)
69
self._is_stderr = (stream.fileno() == 2)
70
if self._is_stdout:
71
color = 'darkgreen'
72
elif self._is_stderr:
73
color = 'red'
74
else:
75
color = 'lightgray'
76
self._prefix = sphinx.util.console.colorize(color, prefix)
77
# When we see an error in the log, we store it here and raise it at the
78
# end of the file (sometimes the lines following the error still
79
# contain valuable information.)
80
self._error = None
81
82
def _init_chatter(self):
83
# We drop any messages from the output that match these regular
84
# expressions. These just bloat the output and do not contain any
85
# information that we care about.
86
self._useless_chatter = (
87
re.compile(r'^$'),
88
re.compile(r'^Running Sphinx'),
89
re.compile(r'^updating environment: 0 added, 0 changed, 0 removed'),
90
re.compile(r'^building \[.*\]: targets for 0 source files that are out of date'),
91
re.compile(r'^building \[.*\]: targets for 0 po files that are out of date'),
92
re.compile(r'^building \[.*\]: targets for 0 mo files that are out of date'),
93
re.compile(r'^build succeeded'), # We still have "Build finished."
94
re.compile(r'^Saved pickle file: citations\.pickle'),
95
re.compile(r'^Compiling|Copying|Merging|Writing'),
96
re.compile(r'^compiling|copying|checking|dumping|executing|generating|linking|loading|looking|pickling|preparing|reading|writing'),
97
re.compile(r'done'),
98
re.compile(r'^WARNING:$'),
99
)
100
101
# We fail whenever a line starts with "WARNING:", however, we ignore
102
# these warnings, as they are not relevant.
103
self._ignored_warnings = (
104
re.compile("WARNING: favicon file 'favicon.ico' does not exist"),
105
re.compile('WARNING: html_static_path entry .* does not exist'),
106
re.compile('WARNING: while setting up extension'),
107
re.compile('WARNING: Any IDs not assiend for figure node'),
108
re.compile('WARNING: .* is not referenced'),
109
re.compile('WARNING: Build finished'),
110
re.compile('WARNING: rST localisation for language .* not found')
111
)
112
# The warning "unknown config value 'multidoc_first_pass'..."
113
# should only appear when building the documentation for a
114
# single file (SingleFileBuilder from __init__.py), and it
115
# needs to be ignored in that case. See #29651.
116
self._ignored_warnings += (re.compile('WARNING: unknown config value \'multidoc_first_pass\''),)
117
self._useless_chatter += self._ignored_warnings
118
119
# replacements: pairs of regular expressions and their replacements,
120
# to be applied to Sphinx output.
121
self.replacements = [(re.compile('build succeeded, [0-9]+ warning[s]?.'),
122
'build succeeded.')]
123
124
if 'inventory' in sys.argv:
125
# When building the inventory, ignore warnings about missing
126
# citations and the search index.
127
ignored = (
128
re.compile('WARNING: citation not found:'),
129
re.compile("WARNING: search index couldn't be loaded, but not all documents will be built: the index will be incomplete.")
130
)
131
self._ignored_warnings += ignored
132
self._useless_chatter += ignored
133
134
# Regular expressions indicating a problem with docbuilding. Raise an
135
# exception if any of these occur.
136
self._error_patterns = (re.compile('Segmentation fault'),
137
re.compile('SEVERE'),
138
re.compile('ERROR'),
139
re.compile('^make.*Error'),
140
re.compile('Exception occurred'),
141
re.compile('Sphinx error'))
142
143
# We want all warnings to actually be errors.
144
# Exceptions:
145
# - warnings upon building the LaTeX documentation
146
# - undefined labels upon the first pass of the compilation: some
147
# cross links may legitimately not yet be resolvable at this point.
148
if 'latex' not in sys.argv:
149
self._error_patterns += (re.compile('WARNING:'),)
150
if 'multidoc_first_pass=1' in sys.argv:
151
ignore = (re.compile('WARNING: undefined label'),)
152
self._ignored_warnings += ignore
153
self._useless_chatter += ignore
154
155
def _filter_out(self, line):
156
if self._error is not None and self._is_stdout:
157
# swallow non-errors after an error occurred
158
return True
159
line = re.sub(self.ansi_escape_sequence, '', line)
160
line = line.strip()
161
for regex in self._useless_chatter:
162
if regex.search(line) is not None:
163
return True
164
return False
165
166
def _check_errors(self, line):
167
r"""
168
Search for errors in line.
169
170
EXAMPLES::
171
172
sage: from sys import stdout
173
sage: from sage_docbuild.sphinxbuild import SageSphinxLogger
174
sage: logger = SageSphinxLogger(stdout, "doctesting")
175
sage: logger._log_line("Segmentation fault!\n") # indirect doctest
176
[doctestin] Segmentation fault!
177
sage: logger.raise_errors()
178
Traceback (most recent call last):
179
...
180
OSError: Segmentation fault!
181
182
"""
183
if self._error is not None:
184
return # we already have found an error
185
for error in self._error_patterns:
186
if error.search(line) is not None:
187
for ignored in self._ignored_warnings:
188
if ignored.search(line) is not None:
189
break
190
else:
191
self._error = line
192
return
193
194
def _log_line(self, line):
195
r"""
196
Write ``line`` to the output stream with some mangling.
197
198
EXAMPLES::
199
200
sage: from sys import stdout
201
sage: from sage_docbuild.sphinxbuild import SageSphinxLogger
202
sage: logger = SageSphinxLogger(stdout, "doctesting")
203
sage: logger._log_line("building documentation…\n")
204
[doctestin] building documentation…
205
206
TESTS:
207
208
Verify that :issue:`25160` has been resolved::
209
210
sage: logger = SageSphinxLogger(stdout, "#25160")
211
sage: import traceback
212
sage: try:
213
....: raise Exception("artificial exception")
214
....: except Exception:
215
....: for line in traceback.format_exc().split('\n'):
216
....: logger._log_line(line)
217
[#25160 ] Traceback (most recent call last):
218
[#25160 ] File ...
219
[#25160 ] raise Exception("artificial exception")
220
[#25160 ] Exception: artificial exception
221
"""
222
skip_this_line = self._filter_out(line)
223
self._check_errors(line)
224
for (old, new) in self.replacements:
225
line = old.sub(new, line)
226
line = self._prefix + ' ' + line.rstrip() + '\n'
227
if not self._color:
228
line = self.ansi_escape_sequence_color.sub('', line)
229
if not skip_this_line:
230
# sphinx does produce messages in the current locals which
231
# could be non-ascii
232
# see https://github.com/sagemath/sage/issues/27706
233
self._stream.write(line if isinstance(line, str) else line.encode('utf8'))
234
self._stream.flush()
235
236
def raise_errors(self):
237
r"""
238
Raise an exceptions if any errors have been found while parsing the
239
Sphinx output.
240
241
EXAMPLES::
242
243
sage: from sys import stdout
244
sage: from sage_docbuild.sphinxbuild import SageSphinxLogger
245
sage: logger = SageSphinxLogger(stdout, "doctesting")
246
sage: logger._log_line("This is a SEVERE error\n")
247
[doctestin] This is a SEVERE error
248
sage: logger.raise_errors()
249
Traceback (most recent call last):
250
...
251
OSError: This is a SEVERE error
252
253
"""
254
if self._error is not None:
255
raise OSError(self._error)
256
257
_line_buffer = ''
258
259
def _write(self, string):
260
self._line_buffer += string
261
lines = self._line_buffer.splitlines()
262
for i, line in enumerate(lines):
263
last = (i == len(lines) - 1)
264
if last and not self._line_buffer.endswith('\n'):
265
self._line_buffer = line
266
return
267
self._log_line(line)
268
self._line_buffer = ''
269
270
# file object interface follows
271
272
closed = False
273
encoding = None
274
mode = 'w'
275
name = '<log>'
276
newlines = None
277
softspace = 0
278
279
def isatty(self):
280
return True
281
282
def close(self):
283
if self._line_buffer != '':
284
self._log_line(self._line_buffer)
285
self._line_buffer = ''
286
287
def flush(self):
288
self._stream.flush()
289
290
def write(self, str):
291
try:
292
self._write(str)
293
except OSError:
294
raise
295
except Exception:
296
import traceback
297
traceback.print_exc(file=self._stream)
298
299
def writelines(self, sequence):
300
for line in sequence:
301
self.write(line)
302
303
304
def runsphinx():
305
output_dir = sys.argv[-1]
306
307
saved_stdout = sys.stdout
308
saved_stderr = sys.stderr
309
310
if not sys.warnoptions:
311
import warnings
312
original_filters = warnings.filters[:]
313
warnings.filterwarnings("ignore", category=DeprecationWarning, module='sphinx.util.inspect')
314
315
try:
316
sys.stdout = SageSphinxLogger(sys.stdout, os.path.basename(output_dir))
317
sys.stderr = SageSphinxLogger(sys.stderr, os.path.basename(output_dir))
318
# Note that this call as of early 2018 leaks memory. So make sure that
319
# you don't call runsphinx() several times in a row. (i.e., you want to
320
# fork() somewhere before this call.)
321
# We don't use subprocess here, as we don't want to re-initialize Sage
322
# for every docbuild as this takes a while.
323
sphinx.cmd.build.main(sys.argv[1:])
324
sys.stderr.raise_errors()
325
sys.stdout.raise_errors()
326
finally:
327
sys.stdout = saved_stdout
328
sys.stderr = saved_stderr
329
sys.stdout.flush()
330
sys.stderr.flush()
331
332
if not sys.warnoptions:
333
warnings.filters = original_filters[:]
334
335