Skip to content

Commit c8859b5

Browse files
DOC: script to build single docstring page (#19840)
1 parent 52559f5 commit c8859b5

File tree

4 files changed

+161
-78
lines changed

4 files changed

+161
-78
lines changed

doc/make.py

+135-31
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,21 @@
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 webbrowser
2021
import jinja2
2122

23+
import pandas
24+
2225

2326
DOC_PATH = os.path.dirname(os.path.abspath(__file__))
2427
SOURCE_PATH = os.path.join(DOC_PATH, 'source')
2528
BUILD_PATH = os.path.join(DOC_PATH, 'build')
2629
BUILD_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
5233
def _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

242346
if __name__ == '__main__':

doc/source/conf.py

+4-37
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import importlib
1919
import warnings
2020

21-
from pandas.compat import u, PY3
2221

2322
try:
2423
raw_input # Python 2
@@ -86,38 +85,6 @@
8685
if any(re.match("\s*api\s*", l) for l in index_rst_lines):
8786
autosummary_generate = True
8887

89-
files_to_delete = []
90-
for f in os.listdir(os.path.dirname(__file__)):
91-
if (not f.endswith(('.ipynb', '.rst')) or
92-
f.startswith('.') or os.path.basename(f) == 'index.rst'):
93-
continue
94-
95-
_file_basename = os.path.splitext(f)[0]
96-
_regex_to_match = "\s*{}\s*$".format(_file_basename)
97-
if not any(re.match(_regex_to_match, line) for line in index_rst_lines):
98-
files_to_delete.append(f)
99-
100-
if files_to_delete:
101-
print("I'm about to DELETE the following:\n{}\n".format(
102-
list(sorted(files_to_delete))))
103-
sys.stdout.write("WARNING: I'd like to delete those "
104-
"to speed up processing (yes/no)? ")
105-
if PY3:
106-
answer = input()
107-
else:
108-
answer = raw_input()
109-
110-
if answer.lower().strip() in ('y', 'yes'):
111-
for f in files_to_delete:
112-
f = os.path.join(os.path.join(os.path.dirname(__file__), f))
113-
f = os.path.abspath(f)
114-
try:
115-
print("Deleting {}".format(f))
116-
os.unlink(f)
117-
except:
118-
print("Error deleting {}".format(f))
119-
pass
120-
12188
# Add any paths that contain templates here, relative to this directory.
12289
templates_path = ['../_templates']
12390

@@ -131,8 +98,8 @@
13198
master_doc = 'index'
13299

133100
# General information about the project.
134-
project = u('pandas')
135-
copyright = u('2008-2014, the pandas development team')
101+
project = u'pandas'
102+
copyright = u'2008-2014, the pandas development team'
136103

137104
# The version info for the project you're documenting, acts as replacement for
138105
# |version| and |release|, also used in various other places throughout the
@@ -343,8 +310,8 @@
343310
# file, target name, title, author, documentclass [howto/manual]).
344311
latex_documents = [
345312
('index', 'pandas.tex',
346-
u('pandas: powerful Python data analysis toolkit'),
347-
u('Wes McKinney\n\& PyData Development Team'), 'manual'),
313+
u'pandas: powerful Python data analysis toolkit',
314+
u'Wes McKinney\n\& PyData Development Team', 'manual'),
348315
]
349316

350317
# The name of an image file (relative to this directory) to place at the top of

doc/source/contributing.rst

+17-10
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ We'll now kick off a three-step process:
171171
# Create and activate the build environment
172172
conda env create -f ci/environment-dev.yaml
173173
conda activate pandas-dev
174-
174+
175175
# or with older versions of Anaconda:
176176
source activate pandas-dev
177177
@@ -388,14 +388,11 @@ If you want to do a full clean build, do::
388388
python make.py html
389389

390390
You can tell ``make.py`` to compile only a single section of the docs, greatly
391-
reducing the turn-around time for checking your changes. You will be prompted to
392-
delete ``.rst`` files that aren't required. This is okay because the prior
393-
versions of these files can be checked out from git. However, you must make sure
394-
not to commit the file deletions to your Git repository!
391+
reducing the turn-around time for checking your changes.
395392

396393
::
397394

398-
#omit autosummary and API section
395+
# omit autosummary and API section
399396
python make.py clean
400397
python make.py --no-api
401398

@@ -404,10 +401,20 @@ not to commit the file deletions to your Git repository!
404401
python make.py clean
405402
python make.py --single indexing
406403

407-
For comparison, a full documentation build may take 10 minutes, a ``-no-api`` build
408-
may take 3 minutes and a single section may take 15 seconds. Subsequent builds, which
409-
only process portions you have changed, will be faster. Open the following file in a web
410-
browser to see the full documentation you just built::
404+
# compile the reference docs for a single function
405+
python make.py clean
406+
python make.py --single DataFrame.join
407+
408+
For comparison, a full documentation build may take 15 minutes, but a single
409+
section may take 15 seconds. Subsequent builds, which only process portions
410+
you have changed, will be faster.
411+
412+
You can also specify to use multiple cores to speed up the documentation build::
413+
414+
python make.py html --num-jobs 4
415+
416+
Open the following file in a web browser to see the full documentation you
417+
just built::
411418

412419
pandas/docs/build/html/index.html
413420

doc/source/index.rst.template

+5
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 == 'docstring' -%}
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)