1414import sys
1515import os
1616import shutil
17- import subprocess
17+ # import subprocess
1818import argparse
1919from contextlib import contextmanager
20+ import webbrowser
2021import jinja2
2122
23+ import pandas
24+
2225
2326DOC_PATH = os .path .dirname (os .path .abspath (__file__ ))
2427SOURCE_PATH = os .path .join (DOC_PATH , 'source' )
2528BUILD_PATH = os .path .join (DOC_PATH , 'build' )
2629BUILD_DIRS = ['doctrees' , 'html' , 'latex' , 'plots' , '_static' , '_templates' ]
2730
2831
29- def _generate_index (include_api , single_doc = None ):
30- """Create index.rst file with the specified sections.
31-
32- Parameters
33- ----------
34- include_api : bool
35- Whether API documentation will be built.
36- single_doc : str or None
37- If provided, this single documentation page will be generated.
38- """
39- if single_doc is not None :
40- single_doc = os .path .splitext (os .path .basename (single_doc ))[0 ]
41- include_api = False
42-
43- with open (os .path .join (SOURCE_PATH , 'index.rst.template' )) as f :
44- t = jinja2 .Template (f .read ())
45-
46- with open (os .path .join (SOURCE_PATH , 'index.rst' ), 'w' ) as f :
47- f .write (t .render (include_api = include_api ,
48- single_doc = single_doc ))
49-
50-
5132@contextmanager
5233def _maybe_exclude_notebooks ():
5334 """Skip building the notebooks if pandoc is not installed.
@@ -58,6 +39,7 @@ def _maybe_exclude_notebooks():
5839 1. nbconvert isn't installed, or
5940 2. nbconvert is installed, but pandoc isn't
6041 """
42+ # TODO move to exclude_pattern
6143 base = os .path .dirname (__file__ )
6244 notebooks = [os .path .join (base , 'source' , nb )
6345 for nb in ['style.ipynb' ]]
@@ -96,8 +78,110 @@ class DocBuilder:
9678 All public methods of this class can be called as parameters of the
9779 script.
9880 """
99- def __init__ (self , num_jobs = 1 ):
81+ def __init__ (self , num_jobs = 1 , include_api = True , single_doc = None ):
10082 self .num_jobs = num_jobs
83+ self .include_api = include_api
84+ self .single_doc = None
85+ self .single_doc_type = None
86+ if single_doc is not None :
87+ self ._process_single_doc (single_doc )
88+ self .exclude_patterns = self ._exclude_patterns
89+
90+ self ._generate_index ()
91+ if self .single_doc_type == 'docstring' :
92+ self ._run_os ('sphinx-autogen' , '-o' ,
93+ 'source/generated_single' , 'source/index.rst' )
94+
95+ @property
96+ def _exclude_patterns (self ):
97+ """Docs source files that will be excluded from building."""
98+ # TODO move maybe_exclude_notebooks here
99+ if self .single_doc is not None :
100+ rst_files = [f for f in os .listdir (SOURCE_PATH )
101+ if ((f .endswith ('.rst' ) or f .endswith ('.ipynb' ))
102+ and (f != 'index.rst' )
103+ and (f != '{0}.rst' .format (self .single_doc )))]
104+ if self .single_doc_type != 'api' :
105+ rst_files += ['generated/*.rst' ]
106+ elif not self .include_api :
107+ rst_files = ['api.rst' , 'generated/*.rst' ]
108+ else :
109+ rst_files = ['generated_single/*.rst' ]
110+
111+ exclude_patterns = ',' .join (
112+ '{!r}' .format (i ) for i in ['**.ipynb_checkpoints' ] + rst_files )
113+
114+ return exclude_patterns
115+
116+ def _process_single_doc (self , single_doc ):
117+ """Extract self.single_doc (base name) and self.single_doc_type from
118+ passed single_doc kwarg.
119+
120+ """
121+ self .include_api = False
122+
123+ if single_doc == 'api.rst' :
124+ self .single_doc_type = 'api'
125+ self .single_doc = 'api'
126+ elif os .path .exists (os .path .join (SOURCE_PATH , single_doc )):
127+ self .single_doc_type = 'rst'
128+ self .single_doc = os .path .splitext (os .path .basename (single_doc ))[0 ]
129+ elif os .path .exists (
130+ os .path .join (SOURCE_PATH , '{}.rst' .format (single_doc ))):
131+ self .single_doc_type = 'rst'
132+ self .single_doc = single_doc
133+ elif single_doc is not None :
134+ try :
135+ obj = pandas
136+ for name in single_doc .split ('.' ):
137+ obj = getattr (obj , name )
138+ except AttributeError :
139+ raise ValueError ('Single document not understood, it should '
140+ 'be a file in doc/source/*.rst (e.g. '
141+ '"contributing.rst" or a pandas function or '
142+ 'method (e.g. "pandas.DataFrame.head")' )
143+ else :
144+ self .single_doc_type = 'docstring'
145+ if single_doc .startswith ('pandas.' ):
146+ self .single_doc = single_doc [len ('pandas.' ):]
147+ else :
148+ self .single_doc = single_doc
149+
150+ def _copy_generated_docstring (self ):
151+ """Copy existing generated (from api.rst) docstring page because
152+ this is more correct in certain cases (where a custom autodoc
153+ template is used).
154+
155+ """
156+ fname = os .path .join (SOURCE_PATH , 'generated' ,
157+ 'pandas.{}.rst' .format (self .single_doc ))
158+ temp_dir = os .path .join (SOURCE_PATH , 'generated_single' )
159+
160+ try :
161+ os .makedirs (temp_dir )
162+ except OSError :
163+ pass
164+
165+ if os .path .exists (fname ):
166+ try :
167+ # copying to make sure sphinx always thinks it is new
168+ # and needs to be re-generated (to pick source code changes)
169+ shutil .copy (fname , temp_dir )
170+ except : # noqa
171+ pass
172+
173+ def _generate_index (self ):
174+ """Create index.rst file with the specified sections."""
175+ if self .single_doc_type == 'docstring' :
176+ self ._copy_generated_docstring ()
177+
178+ with open (os .path .join (SOURCE_PATH , 'index.rst.template' )) as f :
179+ t = jinja2 .Template (f .read ())
180+
181+ with open (os .path .join (SOURCE_PATH , 'index.rst' ), 'w' ) as f :
182+ f .write (t .render (include_api = self .include_api ,
183+ single_doc = self .single_doc ,
184+ single_doc_type = self .single_doc_type ))
101185
102186 @staticmethod
103187 def _create_build_structure ():
@@ -121,7 +205,10 @@ def _run_os(*args):
121205 --------
122206 >>> DocBuilder()._run_os('python', '--version')
123207 """
124- subprocess .check_call (args , stderr = subprocess .STDOUT )
208+ # TODO check_call should be more safe, but it fails with
209+ # exclude patterns, needs investigation
210+ # subprocess.check_call(args, stderr=subprocess.STDOUT)
211+ os .system (' ' .join (args ))
125212
126213 def _sphinx_build (self , kind ):
127214 """Call sphinx to build documentation.
@@ -142,11 +229,21 @@ def _sphinx_build(self, kind):
142229 self ._run_os ('sphinx-build' ,
143230 '-j{}' .format (self .num_jobs ),
144231 '-b{}' .format (kind ),
145- '-d{}' .format (os .path .join (BUILD_PATH ,
146- 'doctrees' ) ),
232+ '-d{}' .format (os .path .join (BUILD_PATH , 'doctrees' )),
233+ '-Dexclude_patterns={}' . format ( self . exclude_patterns ),
147234 SOURCE_PATH ,
148235 os .path .join (BUILD_PATH , kind ))
149236
237+ def _open_browser (self ):
238+ base_url = os .path .join ('file://' , DOC_PATH , 'build' , 'html' )
239+ if self .single_doc_type == 'docstring' :
240+ url = os .path .join (
241+ base_url ,
242+ 'generated_single' , 'pandas.{}.html' .format (self .single_doc ))
243+ else :
244+ url = os .path .join (base_url , '{}.html' .format (self .single_doc ))
245+ webbrowser .open (url , new = 2 )
246+
150247 def html (self ):
151248 """Build HTML documentation."""
152249 self ._create_build_structure ()
@@ -156,6 +253,11 @@ def html(self):
156253 if os .path .exists (zip_fname ):
157254 os .remove (zip_fname )
158255
256+ if self .single_doc is not None :
257+ self ._open_browser ()
258+ shutil .rmtree (os .path .join (SOURCE_PATH , 'generated_single' ),
259+ ignore_errors = True )
260+
159261 def latex (self , force = False ):
160262 """Build PDF documentation."""
161263 self ._create_build_structure ()
@@ -222,8 +324,8 @@ def main():
222324 metavar = 'FILENAME' ,
223325 type = str ,
224326 default = None ,
225- help = ('filename of section to compile, '
226- 'e.g. "indexing"' ))
327+ help = ('filename of section or method name to '
328+ 'compile, e.g. "indexing", "DataFrame.join "' ))
227329 argparser .add_argument ('--python-path' ,
228330 type = str ,
229331 default = os .path .join (DOC_PATH , '..' ),
@@ -235,8 +337,10 @@ def main():
235337 args .command , ', ' .join (cmds )))
236338
237339 os .environ ['PYTHONPATH' ] = args .python_path
238- _generate_index (not args .no_api , args .single )
239- getattr (DocBuilder (args .num_jobs ), args .command )()
340+
341+ getattr (DocBuilder (args .num_jobs ,
342+ not args .no_api ,
343+ args .single ), args .command )()
240344
241345
242346if __name__ == '__main__' :
0 commit comments