15
15
import sys
16
16
import os
17
17
import shutil
18
- # import subprocess
18
+ import subprocess
19
19
import argparse
20
- from contextlib import contextmanager
21
20
import webbrowser
22
- import jinja2
23
21
24
22
25
23
DOC_PATH = os .path .dirname (os .path .abspath (__file__ ))
28
26
BUILD_DIRS = ['doctrees' , 'html' , 'latex' , 'plots' , '_static' , '_templates' ]
29
27
30
28
31
- @contextmanager
32
- def _maybe_exclude_notebooks ():
33
- """Skip building the notebooks if pandoc is not installed.
34
-
35
- This assumes that nbsphinx is installed.
36
-
37
- Skip notebook conversion if:
38
- 1. nbconvert isn't installed, or
39
- 2. nbconvert is installed, but pandoc isn't
40
- """
41
- # TODO move to exclude_pattern
42
- base = os .path .dirname (__file__ )
43
- notebooks = [os .path .join (base , 'source' , nb )
44
- for nb in ['style.ipynb' ]]
45
- contents = {}
46
-
47
- def _remove_notebooks ():
48
- for nb in notebooks :
49
- with open (nb , 'rt' ) as f :
50
- contents [nb ] = f .read ()
51
- os .remove (nb )
52
-
53
- try :
54
- import nbconvert
55
- except ImportError :
56
- sys .stderr .write ('Warning: nbconvert not installed. '
57
- 'Skipping notebooks.\n ' )
58
- _remove_notebooks ()
59
- else :
60
- try :
61
- nbconvert .utils .pandoc .get_pandoc_version ()
62
- except nbconvert .utils .pandoc .PandocMissing :
63
- sys .stderr .write ('Warning: Pandoc is not installed. '
64
- 'Skipping notebooks.\n ' )
65
- _remove_notebooks ()
66
-
67
- yield
68
-
69
- for nb , content in contents .items ():
70
- with open (nb , 'wt' ) as f :
71
- f .write (content )
72
-
73
-
74
29
class DocBuilder :
75
- """Class to wrap the different commands of this script.
30
+ """
31
+ Class to wrap the different commands of this script.
76
32
77
33
All public methods of this class can be called as parameters of the
78
34
script.
79
35
"""
80
- def __init__ (self , num_jobs = 1 , include_api = True , single_doc = None ,
81
- verbosity = 0 ):
36
+ def __init__ (self , num_jobs = 0 , include_api = True , single_doc = None ,
37
+ verbosity = 0 , warnings_are_errors = False ):
82
38
self .num_jobs = num_jobs
83
- self .include_api = include_api
84
39
self .verbosity = verbosity
85
- self .single_doc = None
86
- self .single_doc_type = None
87
- if single_doc is not None :
88
- self ._process_single_doc (single_doc )
89
- self .exclude_patterns = self ._exclude_patterns
90
-
91
- self ._generate_index ()
92
- if self .single_doc_type == 'docstring' :
93
- self ._run_os ('sphinx-autogen' , '-o' ,
94
- 'source/generated_single' , 'source/index.rst' )
95
-
96
- @property
97
- def _exclude_patterns (self ):
98
- """Docs source files that will be excluded from building."""
99
- # TODO move maybe_exclude_notebooks here
100
- if self .single_doc is not None :
101
- rst_files = [f for f in os .listdir (SOURCE_PATH )
102
- if ((f .endswith ('.rst' ) or f .endswith ('.ipynb' ))
103
- and (f != 'index.rst' )
104
- and (f != '{0}.rst' .format (self .single_doc )))]
105
- if self .single_doc_type != 'api' :
106
- rst_files += ['generated/*.rst' ]
107
- elif not self .include_api :
108
- rst_files = ['api.rst' , 'generated/*.rst' ]
109
- else :
110
- rst_files = ['generated_single/*.rst' ]
111
-
112
- exclude_patterns = ',' .join (
113
- '{!r}' .format (i ) for i in ['**.ipynb_checkpoints' ] + rst_files )
114
-
115
- return exclude_patterns
40
+ self .warnings_are_errors = warnings_are_errors
41
+
42
+ if single_doc :
43
+ single_doc = self ._process_single_doc (single_doc )
44
+ include_api = False
45
+ os .environ ['SPHINX_PATTERN' ] = single_doc
46
+ elif not include_api :
47
+ os .environ ['SPHINX_PATTERN' ] = '-api'
48
+
49
+ self .single_doc_html = None
50
+ if single_doc and single_doc .endswith ('.rst' ):
51
+ self .single_doc_html = os .path .splitext (single_doc )[0 ] + '.html'
52
+ elif single_doc :
53
+ self .single_doc_html = 'generated/pandas.{}.html' .format (
54
+ single_doc )
116
55
117
56
def _process_single_doc (self , single_doc ):
118
- """Extract self.single_doc (base name) and self.single_doc_type from
119
- passed single_doc kwarg.
120
-
121
57
"""
122
- self .include_api = False
123
-
124
- if single_doc == 'api.rst' or single_doc == 'api' :
125
- self .single_doc_type = 'api'
126
- self .single_doc = 'api'
127
- elif os .path .exists (os .path .join (SOURCE_PATH , single_doc )):
128
- self .single_doc_type = 'rst'
58
+ Make sure the provided value for --single is a path to an existing
59
+ .rst/.ipynb file, or a pandas object that can be imported.
129
60
130
- if 'whatsnew' in single_doc :
131
- basename = single_doc
61
+ For example, categorial.rst or pandas.DataFrame.head. For the latter,
62
+ return the corresponding file path
63
+ (e.g. generated/pandas.DataFrame.head.rst).
64
+ """
65
+ base_name , extension = os .path .splitext (single_doc )
66
+ if extension in ('.rst' , '.ipynb' ):
67
+ if os .path .exists (os .path .join (SOURCE_PATH , single_doc )):
68
+ return single_doc
132
69
else :
133
- basename = os .path .basename (single_doc )
134
- self .single_doc = os .path .splitext (basename )[0 ]
135
- elif os .path .exists (
136
- os .path .join (SOURCE_PATH , '{}.rst' .format (single_doc ))):
137
- self .single_doc_type = 'rst'
138
- self .single_doc = single_doc
139
- elif single_doc is not None :
70
+ raise FileNotFoundError ('File {} not found' .format (single_doc ))
71
+
72
+ elif single_doc .startswith ('pandas.' ):
140
73
try :
141
74
obj = pandas # noqa: F821
142
75
for name in single_doc .split ('.' ):
143
76
obj = getattr (obj , name )
144
77
except AttributeError :
145
- raise ValueError ('Single document not understood, it should '
146
- 'be a file in doc/source/*.rst (e.g. '
147
- '"contributing.rst" or a pandas function or '
148
- 'method (e.g. "pandas.DataFrame.head")' )
78
+ raise ImportError ('Could not import {}' .format (single_doc ))
149
79
else :
150
- self .single_doc_type = 'docstring'
151
- if single_doc .startswith ('pandas.' ):
152
- self .single_doc = single_doc [len ('pandas.' ):]
153
- else :
154
- self .single_doc = single_doc
155
-
156
- def _copy_generated_docstring (self ):
157
- """Copy existing generated (from api.rst) docstring page because
158
- this is more correct in certain cases (where a custom autodoc
159
- template is used).
160
-
161
- """
162
- fname = os .path .join (SOURCE_PATH , 'generated' ,
163
- 'pandas.{}.rst' .format (self .single_doc ))
164
- temp_dir = os .path .join (SOURCE_PATH , 'generated_single' )
165
-
166
- try :
167
- os .makedirs (temp_dir )
168
- except OSError :
169
- pass
170
-
171
- if os .path .exists (fname ):
172
- try :
173
- # copying to make sure sphinx always thinks it is new
174
- # and needs to be re-generated (to pick source code changes)
175
- shutil .copy (fname , temp_dir )
176
- except : # noqa
177
- pass
178
-
179
- def _generate_index (self ):
180
- """Create index.rst file with the specified sections."""
181
- if self .single_doc_type == 'docstring' :
182
- self ._copy_generated_docstring ()
183
-
184
- with open (os .path .join (SOURCE_PATH , 'index.rst.template' )) as f :
185
- t = jinja2 .Template (f .read ())
186
-
187
- with open (os .path .join (SOURCE_PATH , 'index.rst' ), 'w' ) as f :
188
- f .write (t .render (include_api = self .include_api ,
189
- single_doc = self .single_doc ,
190
- single_doc_type = self .single_doc_type ))
191
-
192
- @staticmethod
193
- def _create_build_structure ():
194
- """Create directories required to build documentation."""
195
- for dirname in BUILD_DIRS :
196
- try :
197
- os .makedirs (os .path .join (BUILD_PATH , dirname ))
198
- except OSError :
199
- pass
80
+ return single_doc [len ('pandas.' ):]
81
+ else :
82
+ raise ValueError (('--single={} not understood. Value should be a '
83
+ 'valid path to a .rst or .ipynb file, or a '
84
+ 'valid pandas object (e.g. categorical.rst or '
85
+ 'pandas.DataFrame.head)' ).format (single_doc ))
200
86
201
87
@staticmethod
202
88
def _run_os (* args ):
203
- """Execute a command as a OS terminal.
89
+ """
90
+ Execute a command as a OS terminal.
204
91
205
92
Parameters
206
93
----------
@@ -211,16 +98,11 @@ def _run_os(*args):
211
98
--------
212
99
>>> DocBuilder()._run_os('python', '--version')
213
100
"""
214
- # TODO check_call should be more safe, but it fails with
215
- # exclude patterns, needs investigation
216
- # subprocess.check_call(args, stderr=subprocess.STDOUT)
217
- exit_status = os .system (' ' .join (args ))
218
- if exit_status :
219
- msg = 'Command "{}" finished with exit code {}'
220
- raise RuntimeError (msg .format (' ' .join (args ), exit_status ))
101
+ subprocess .check_call (args , stdout = sys .stdout , stderr = sys .stderr )
221
102
222
103
def _sphinx_build (self , kind ):
223
- """Call sphinx to build documentation.
104
+ """
105
+ Call sphinx to build documentation.
224
106
225
107
Attribute `num_jobs` from the class is used.
226
108
@@ -236,43 +118,44 @@ def _sphinx_build(self, kind):
236
118
raise ValueError ('kind must be html or latex, '
237
119
'not {}' .format (kind ))
238
120
239
- self ._run_os ('sphinx-build' ,
240
- '-j{}' .format (self .num_jobs ),
241
- '-b{}' .format (kind ),
242
- '-{}' .format (
243
- 'v' * self .verbosity ) if self .verbosity else '' ,
244
- '-d"{}"' .format (os .path .join (BUILD_PATH , 'doctrees' )),
245
- '-Dexclude_patterns={}' .format (self .exclude_patterns ),
246
- '"{}"' .format (SOURCE_PATH ),
247
- '"{}"' .format (os .path .join (BUILD_PATH , kind )))
248
-
249
- def _open_browser (self ):
250
- base_url = os .path .join ('file://' , DOC_PATH , 'build' , 'html' )
251
- if self .single_doc_type == 'docstring' :
252
- url = os .path .join (
253
- base_url ,
254
- 'generated_single' , 'pandas.{}.html' .format (self .single_doc ))
255
- else :
256
- url = os .path .join (base_url , '{}.html' .format (self .single_doc ))
121
+ self .clean ()
122
+
123
+ cmd = ['sphinx-build' , '-b' , kind ]
124
+ if self .num_jobs :
125
+ cmd += ['-j' , str (self .num_jobs )]
126
+ if self .warnings_are_errors :
127
+ cmd .append ('-W' )
128
+ if self .verbosity :
129
+ cmd .append ('-{}' .format ('v' * self .verbosity ))
130
+ cmd += ['-d' , os .path .join (BUILD_PATH , 'doctrees' ),
131
+ SOURCE_PATH , os .path .join (BUILD_PATH , kind )]
132
+ cmd = ['sphinx-build' , SOURCE_PATH , os .path .join (BUILD_PATH , kind )]
133
+ self ._run_os (* cmd )
134
+
135
+ def _open_browser (self , single_doc_html ):
136
+ """
137
+ Open a browser tab showing single
138
+ """
139
+ url = os .path .join ('file://' , DOC_PATH , 'build' , 'html' ,
140
+ single_doc_html )
257
141
webbrowser .open (url , new = 2 )
258
142
259
143
def html (self ):
260
- """Build HTML documentation."""
261
- self ._create_build_structure ()
262
- with _maybe_exclude_notebooks ():
263
- self ._sphinx_build ('html' )
264
- zip_fname = os .path .join (BUILD_PATH , 'html' , 'pandas.zip' )
265
- if os .path .exists (zip_fname ):
266
- os .remove (zip_fname )
267
-
268
- if self .single_doc is not None :
269
- self ._open_browser ()
270
- shutil .rmtree (os .path .join (SOURCE_PATH , 'generated_single' ),
271
- ignore_errors = True )
144
+ """
145
+ Build HTML documentation.
146
+ """
147
+ self ._sphinx_build ('html' )
148
+ zip_fname = os .path .join (BUILD_PATH , 'html' , 'pandas.zip' )
149
+ if os .path .exists (zip_fname ):
150
+ os .remove (zip_fname )
151
+
152
+ if self .single_doc_html is not None :
153
+ self ._open_browser (self .single_doc_html )
272
154
273
155
def latex (self , force = False ):
274
- """Build PDF documentation."""
275
- self ._create_build_structure ()
156
+ """
157
+ Build PDF documentation.
158
+ """
276
159
if sys .platform == 'win32' :
277
160
sys .stderr .write ('latex build has not been tested on windows\n ' )
278
161
else :
@@ -289,18 +172,24 @@ def latex(self, force=False):
289
172
self ._run_os ('make' )
290
173
291
174
def latex_forced (self ):
292
- """Build PDF documentation with retries to find missing references."""
175
+ """
176
+ Build PDF documentation with retries to find missing references.
177
+ """
293
178
self .latex (force = True )
294
179
295
180
@staticmethod
296
181
def clean ():
297
- """Clean documentation generated files."""
182
+ """
183
+ Clean documentation generated files.
184
+ """
298
185
shutil .rmtree (BUILD_PATH , ignore_errors = True )
299
186
shutil .rmtree (os .path .join (SOURCE_PATH , 'generated' ),
300
187
ignore_errors = True )
301
188
302
189
def zip_html (self ):
303
- """Compress HTML documentation into a zip file."""
190
+ """
191
+ Compress HTML documentation into a zip file.
192
+ """
304
193
zip_fname = os .path .join (BUILD_PATH , 'html' , 'pandas.zip' )
305
194
if os .path .exists (zip_fname ):
306
195
os .remove (zip_fname )
@@ -326,7 +215,7 @@ def main():
326
215
help = 'command to run: {}' .format (', ' .join (cmds )))
327
216
argparser .add_argument ('--num-jobs' ,
328
217
type = int ,
329
- default = 1 ,
218
+ default = 0 ,
330
219
help = 'number of jobs used by sphinx-build' )
331
220
argparser .add_argument ('--no-api' ,
332
221
default = False ,
@@ -345,6 +234,9 @@ def main():
345
234
argparser .add_argument ('-v' , action = 'count' , dest = 'verbosity' , default = 0 ,
346
235
help = ('increase verbosity (can be repeated), '
347
236
'passed to the sphinx build command' ))
237
+ argparser .add_argument ('--warnings-are-errors' , '-W' ,
238
+ action = 'store_true' ,
239
+ help = 'fail if warnings are raised' )
348
240
args = argparser .parse_args ()
349
241
350
242
if args .command not in cmds :
@@ -364,7 +256,7 @@ def main():
364
256
os .environ ['MPLBACKEND' ] = 'module://matplotlib.backends.backend_agg'
365
257
366
258
builder = DocBuilder (args .num_jobs , not args .no_api , args .single ,
367
- args .verbosity )
259
+ args .verbosity , args . warnings_are_errors )
368
260
getattr (builder , args .command )()
369
261
370
262
0 commit comments