1515import sys
1616import os
1717import shutil
18- # import subprocess
18+ import subprocess
1919import argparse
20- from contextlib import contextmanager
2120import webbrowser
22- import jinja2
2321
2422
2523DOC_PATH = os .path .dirname (os .path .abspath (__file__ ))
2826BUILD_DIRS = ['doctrees' , 'html' , 'latex' , 'plots' , '_static' , '_templates' ]
2927
3028
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-
7429class DocBuilder :
75- """Class to wrap the different commands of this script.
30+ """
31+ Class to wrap the different commands of this script.
7632
7733 All public methods of this class can be called as parameters of the
7834 script.
7935 """
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 ):
8238 self .num_jobs = num_jobs
83- self .include_api = include_api
8439 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 )
11655
11756 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-
12157 """
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.
12960
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
13269 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.' ):
14073 try :
14174 obj = pandas # noqa: F821
14275 for name in single_doc .split ('.' ):
14376 obj = getattr (obj , name )
14477 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 ))
14979 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 ))
20086
20187 @staticmethod
20288 def _run_os (* args ):
203- """Execute a command as a OS terminal.
89+ """
90+ Execute a command as a OS terminal.
20491
20592 Parameters
20693 ----------
@@ -211,16 +98,11 @@ def _run_os(*args):
21198 --------
21299 >>> DocBuilder()._run_os('python', '--version')
213100 """
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 )
221102
222103 def _sphinx_build (self , kind ):
223- """Call sphinx to build documentation.
104+ """
105+ Call sphinx to build documentation.
224106
225107 Attribute `num_jobs` from the class is used.
226108
@@ -236,43 +118,44 @@ def _sphinx_build(self, kind):
236118 raise ValueError ('kind must be html or latex, '
237119 'not {}' .format (kind ))
238120
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 )
257141 webbrowser .open (url , new = 2 )
258142
259143 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 )
272154
273155 def latex (self , force = False ):
274- """Build PDF documentation."""
275- self ._create_build_structure ()
156+ """
157+ Build PDF documentation.
158+ """
276159 if sys .platform == 'win32' :
277160 sys .stderr .write ('latex build has not been tested on windows\n ' )
278161 else :
@@ -289,18 +172,24 @@ def latex(self, force=False):
289172 self ._run_os ('make' )
290173
291174 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+ """
293178 self .latex (force = True )
294179
295180 @staticmethod
296181 def clean ():
297- """Clean documentation generated files."""
182+ """
183+ Clean documentation generated files.
184+ """
298185 shutil .rmtree (BUILD_PATH , ignore_errors = True )
299186 shutil .rmtree (os .path .join (SOURCE_PATH , 'generated' ),
300187 ignore_errors = True )
301188
302189 def zip_html (self ):
303- """Compress HTML documentation into a zip file."""
190+ """
191+ Compress HTML documentation into a zip file.
192+ """
304193 zip_fname = os .path .join (BUILD_PATH , 'html' , 'pandas.zip' )
305194 if os .path .exists (zip_fname ):
306195 os .remove (zip_fname )
@@ -326,7 +215,7 @@ def main():
326215 help = 'command to run: {}' .format (', ' .join (cmds )))
327216 argparser .add_argument ('--num-jobs' ,
328217 type = int ,
329- default = 1 ,
218+ default = 0 ,
330219 help = 'number of jobs used by sphinx-build' )
331220 argparser .add_argument ('--no-api' ,
332221 default = False ,
@@ -345,6 +234,9 @@ def main():
345234 argparser .add_argument ('-v' , action = 'count' , dest = 'verbosity' , default = 0 ,
346235 help = ('increase verbosity (can be repeated), '
347236 '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' )
348240 args = argparser .parse_args ()
349241
350242 if args .command not in cmds :
@@ -364,7 +256,7 @@ def main():
364256 os .environ ['MPLBACKEND' ] = 'module://matplotlib.backends.backend_agg'
365257
366258 builder = DocBuilder (args .num_jobs , not args .no_api , args .single ,
367- args .verbosity )
259+ args .verbosity , args . warnings_are_errors )
368260 getattr (builder , args .command )()
369261
370262
0 commit comments