Skip to content

Commit 812eb01

Browse files
datapythonistajorisvandenbossche
authored andcommitted
DOC: Implementing single doc building for api docs
1 parent 4e34b09 commit 812eb01

File tree

2 files changed

+92
-150
lines changed

2 files changed

+92
-150
lines changed

doc/make.py

Lines changed: 87 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
import sys
1515
import os
1616
import shutil
17-
import subprocess
17+
# import subprocess
1818
import argparse
1919
from contextlib import contextmanager
20-
import jinja2
21-
import shutil
2220
import webbrowser
21+
import jinja2
22+
import pandas
2323

2424

2525
DOC_PATH = os.path.dirname(os.path.abspath(__file__))
@@ -28,105 +28,6 @@
2828
BUILD_DIRS = ['doctrees', 'html', 'latex', 'plots', '_static', '_templates']
2929

3030

31-
def _generate_index(include_api=True, single_doc=None):
32-
"""Create index.rst file with the specified sections.
33-
34-
Parameters
35-
----------
36-
include_api : bool
37-
Whether API documentation will be built.
38-
single_doc : str or None
39-
If provided, this single documentation page will be generated.
40-
"""
41-
if single_doc is not None:
42-
single_doc = os.path.splitext(os.path.basename(single_doc))[0]
43-
include_api = False
44-
45-
with open(os.path.join(SOURCE_PATH, 'index.rst.template')) as f:
46-
t = jinja2.Template(f.read())
47-
48-
with open(os.path.join(SOURCE_PATH, 'index.rst'), 'w') as f:
49-
f.write(t.render(include_api=include_api,
50-
single_doc=single_doc))
51-
52-
53-
def _generate_exclude_pattern(include_api=True, single_doc=None):
54-
"""
55-
56-
"""
57-
if not include_api:
58-
rst_files = ['api.rst', 'generated/*.rst']
59-
elif single_doc is not None:
60-
rst_files = [f for f in os.listdir(SOURCE_PATH)
61-
if ((f.endswith('.rst') or f.endswith('.ipynb'))
62-
and (f != 'index.rst') and (f != single_doc))]
63-
rst_files += ['generated/*.rst']
64-
else:
65-
rst_files = []
66-
67-
exclude_patterns = ",".join(
68-
['{!r}'.format(i) for i in ['**.ipynb_checkpoints'] + rst_files])
69-
70-
return exclude_patterns
71-
72-
73-
def _generate_temp_docstring_file(methods):
74-
"""
75-
"""
76-
fnames = [os.path.join(SOURCE_PATH, 'generated', '{}.rst'.format(method))
77-
for method in methods]
78-
79-
# # remove the target file to make sure it is updated again (to build
80-
# # latest version)
81-
# try:
82-
# os.remove(fname)
83-
# except OSError:
84-
# pass
85-
#
86-
# # generate docstring pages
87-
# print("Running sphinx-autogen to generate docstring stub pages")
88-
# os.system("sphinx-autogen -o source/generated source/*.rst")
89-
90-
# create the temporary directory in which we will link the target file
91-
try:
92-
os.makedirs(os.path.join(SOURCE_PATH, 'generated_temp'))
93-
except OSError:
94-
pass
95-
96-
for fname in fnames:
97-
if os.path.exists(fname):
98-
# link the target file
99-
try:
100-
# os.symlink(fname, os.path.join(SOURCE_PATH, 'generated_temp',
101-
# '{}.rst'.format(method)),
102-
# target_is_directory=False)
103-
# copying to make sure sphinx always thinks it is new
104-
# and needs to be re-generated (to pick source code changes)
105-
shutil.copy(fname, os.path.join(SOURCE_PATH, 'generated_temp'))
106-
linked = True
107-
except: # noqa
108-
linked = False
109-
else:
110-
linked = False
111-
112-
s = """Built docstrings
113-
================
114-
115-
.. autosummary::
116-
:toctree: generated_temp/
117-
118-
{name}
119-
120-
""".format(name='\n '.join(methods))
121-
122-
with open(os.path.join(SOURCE_PATH, "temp.rst"), 'w') as f:
123-
f.write(s)
124-
125-
if not linked:
126-
print("Running sphinx-autogen on manually created file")
127-
os.system("sphinx-autogen -o source/generated_temp source/temp.rst")
128-
129-
13031
@contextmanager
13132
def _maybe_exclude_notebooks():
13233
"""Skip building the notebooks if pandoc is not installed.
@@ -137,6 +38,7 @@ def _maybe_exclude_notebooks():
13738
1. nbconvert isn't installed, or
13839
2. nbconvert is installed, but pandoc isn't
13940
"""
41+
# TODO move to exclude_pattern
14042
base = os.path.dirname(__file__)
14143
notebooks = [os.path.join(base, 'source', nb)
14244
for nb in ['style.ipynb']]
@@ -175,9 +77,73 @@ class DocBuilder:
17577
All public methods of this class can be called as parameters of the
17678
script.
17779
"""
178-
def __init__(self, num_jobs=1, exclude_patterns=None):
80+
def __init__(self, num_jobs=1, include_api=True, single_doc=None):
17981
self.num_jobs = num_jobs
180-
self.exclude_patterns = exclude_patterns
82+
self.include_api = include_api
83+
self.single_doc = single_doc
84+
self.single_doc_type = self._single_doc_type
85+
self.exclude_patterns = self._exclude_patterns
86+
87+
self._generate_index()
88+
if self.single_doc_type == 'api':
89+
self._run_os('sphinx-autogen', '-o',
90+
'source/generated_single', 'source/index.rst')
91+
92+
@property
93+
def _exclude_patterns(self):
94+
"""Docs source files that will be excluded from building."""
95+
# TODO move maybe_exclude_notebooks here
96+
if self.single_doc is not None:
97+
rst_files = [f for f in os.listdir(SOURCE_PATH)
98+
if ((f.endswith('.rst') or f.endswith('.ipynb'))
99+
and (f != 'index.rst')
100+
and (f != self.single_doc))]
101+
rst_files += ['generated/*.rst']
102+
elif not self.include_api:
103+
rst_files = ['api.rst', 'generated/*.rst']
104+
else:
105+
rst_files = ['generated_single/*.rst']
106+
107+
exclude_patterns = ','.join(
108+
'{!r}'.format(i) for i in ['**.ipynb_checkpoints'] + rst_files)
109+
110+
return exclude_patterns
111+
112+
@property
113+
def _single_doc_type(self):
114+
if self.single_doc:
115+
if os.path.exists(os.path.join(SOURCE_PATH, self.single_doc)):
116+
return 'rst'
117+
try:
118+
obj = pandas
119+
for name in self.single_doc.split('.'):
120+
obj = getattr(obj, name)
121+
except AttributeError:
122+
raise ValueError('Single document not understood, it should '
123+
'be a file in doc/source/*.rst (e.g. '
124+
'"contributing.rst" or a pandas function or '
125+
'method (e.g. "pandas.DataFrame.head")')
126+
else:
127+
return 'api'
128+
129+
def _generate_index(self):
130+
"""Create index.rst file with the specified sections."""
131+
if self.single_doc_type == 'rst':
132+
single_doc = os.path.splitext(os.path.basename(self.single_doc))[0]
133+
self.include_api = False
134+
elif self.single_doc_type == 'api' and \
135+
self.single_doc.startswith('pandas.'):
136+
single_doc = self.single_doc[len('pandas.'):]
137+
else:
138+
single_doc = self.single_doc
139+
140+
with open(os.path.join(SOURCE_PATH, 'index.rst.template')) as f:
141+
t = jinja2.Template(f.read())
142+
143+
with open(os.path.join(SOURCE_PATH, 'index.rst'), 'w') as f:
144+
f.write(t.render(include_api=self.include_api,
145+
single_doc=single_doc,
146+
single_doc_type=self.single_doc_type))
181147

182148
@staticmethod
183149
def _create_build_structure():
@@ -201,7 +167,10 @@ def _run_os(*args):
201167
--------
202168
>>> DocBuilder()._run_os('python', '--version')
203169
"""
204-
subprocess.check_call(args, stderr=subprocess.STDOUT)
170+
# TODO check_call should be more safe, but it fails with
171+
# exclude patterns, needs investigation
172+
# subprocess.check_call(args, stderr=subprocess.STDOUT)
173+
os.system(' '.join(args))
205174

206175
def _sphinx_build(self, kind):
207176
"""Call sphinx to build documentation.
@@ -223,10 +192,16 @@ def _sphinx_build(self, kind):
223192
'-j{}'.format(self.num_jobs),
224193
'-b{}'.format(kind),
225194
'-d{}'.format(os.path.join(BUILD_PATH, 'doctrees')),
226-
# TODO integrate exclude_patterns
195+
'-Dexclude_patterns={}'.format(self.exclude_patterns),
227196
SOURCE_PATH,
228197
os.path.join(BUILD_PATH, kind))
229198

199+
def _open_browser(self):
200+
url = os.path.join(
201+
'file://', DOC_PATH, 'build', 'html',
202+
'generated_single', '{}.html'.format(self.single_doc))
203+
webbrowser.open(url, new=2)
204+
230205
def html(self):
231206
"""Build HTML documentation."""
232207
self._create_build_structure()
@@ -236,6 +211,9 @@ def html(self):
236211
if os.path.exists(zip_fname):
237212
os.remove(zip_fname)
238213

214+
if self.single_doc is not None:
215+
self._open_browser()
216+
239217
def latex(self, force=False):
240218
"""Build PDF documentation."""
241219
self._create_build_structure()
@@ -279,24 +257,6 @@ def zip_html(self):
279257
'-q',
280258
*fnames)
281259

282-
def build_docstring(self):
283-
"""Build single docstring page"""
284-
self._create_build_structure()
285-
286-
args = ('sphinx-build',
287-
'-bhtml',
288-
'-d{}'.format(os.path.join(BUILD_PATH, 'doctrees')),
289-
'-Dexclude_patterns={}'.format(self.exclude_patterns),
290-
SOURCE_PATH,
291-
os.path.join(BUILD_PATH, 'html'),
292-
os.path.join(SOURCE_PATH, 'temp.rst'),
293-
os.path.join(SOURCE_PATH, 'generated_temp/*.rst'),
294-
)
295-
# for some reason it does not work with run_os, but it does if I
296-
# directly call the joined command
297-
# self._run_os(*args)
298-
os.system(" ".join(args))
299-
300260

301261
def main():
302262
cmds = [method for method in dir(DocBuilder) if not method.startswith('_')]
@@ -341,32 +301,9 @@ def main():
341301

342302
os.environ['PYTHONPATH'] = args.python_path
343303

344-
if args.docstring is not None:
345-
shutil.rmtree(os.path.join(BUILD_PATH, 'html', 'generated_temp'),
346-
ignore_errors=True)
347-
_generate_temp_docstring_file(args.docstring)
348-
exclude_patterns = _generate_exclude_pattern(single_doc='temp.rst')
349-
_generate_index(single_doc='temp.rst')
350-
DocBuilder(args.num_jobs, exclude_patterns).build_docstring()
351-
# open generated page in new browser tab
352-
if len(args.docstring) == 1:
353-
url = os.path.join(
354-
"file://", DOC_PATH, "build", "html",
355-
"generated_temp", "{}.html".format(args.docstring[0]))
356-
else:
357-
url = os.path.join(
358-
"file://", DOC_PATH, "build", "html", "temp.html")
359-
webbrowser.open(url, new=2)
360-
# clean-up generated files
361-
os.remove('source/temp.rst')
362-
shutil.rmtree(os.path.join(SOURCE_PATH, 'generated_temp'),
363-
ignore_errors=True)
364-
365-
else:
366-
_generate_index(not args.no_api, args.single)
367-
exclude_patterns = _generate_exclude_pattern(
368-
not args.no_api, args.single)
369-
getattr(DocBuilder(args.num_jobs, exclude_patterns), args.command)()
304+
getattr(DocBuilder(args.num_jobs,
305+
args.no_api,
306+
args.single), args.command)()
370307

371308

372309
if __name__ == '__main__':

doc/source/index.rst.template

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,13 @@ Some other notes
106106
See the package overview for more detail about what's in the library.
107107

108108

109+
{% if single_doc_type == 'api' -%}
110+
.. autosummary::
111+
:toctree: generated_single/
112+
{% else -%}
109113
.. toctree::
110114
:maxdepth: 4
115+
{% endif %}
111116

112117
{% if single_doc -%}
113118
{{ single_doc }}

0 commit comments

Comments
 (0)