-
-
Notifications
You must be signed in to change notification settings - Fork 17.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DOC: script to build single docstring page #19840
Changes from 12 commits
5fd62fc
2766dde
7fe8ac2
13d31b9
1477e63
4e34b09
812eb01
1ca28a0
09c508a
358e08d
2cd4a5d
512d3a6
348cd4f
2acc635
3a57e3e
ebe4a8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,40 +14,21 @@ | |
import sys | ||
import os | ||
import shutil | ||
import subprocess | ||
# import subprocess | ||
import argparse | ||
from contextlib import contextmanager | ||
import webbrowser | ||
import jinja2 | ||
|
||
import pandas | ||
|
||
|
||
DOC_PATH = os.path.dirname(os.path.abspath(__file__)) | ||
SOURCE_PATH = os.path.join(DOC_PATH, 'source') | ||
BUILD_PATH = os.path.join(DOC_PATH, 'build') | ||
BUILD_DIRS = ['doctrees', 'html', 'latex', 'plots', '_static', '_templates'] | ||
|
||
|
||
def _generate_index(include_api, single_doc=None): | ||
"""Create index.rst file with the specified sections. | ||
|
||
Parameters | ||
---------- | ||
include_api : bool | ||
Whether API documentation will be built. | ||
single_doc : str or None | ||
If provided, this single documentation page will be generated. | ||
""" | ||
if single_doc is not None: | ||
single_doc = os.path.splitext(os.path.basename(single_doc))[0] | ||
include_api = False | ||
|
||
with open(os.path.join(SOURCE_PATH, 'index.rst.template')) as f: | ||
t = jinja2.Template(f.read()) | ||
|
||
with open(os.path.join(SOURCE_PATH, 'index.rst'), 'w') as f: | ||
f.write(t.render(include_api=include_api, | ||
single_doc=single_doc)) | ||
|
||
|
||
@contextmanager | ||
def _maybe_exclude_notebooks(): | ||
"""Skip building the notebooks if pandoc is not installed. | ||
|
@@ -58,6 +39,7 @@ def _maybe_exclude_notebooks(): | |
1. nbconvert isn't installed, or | ||
2. nbconvert is installed, but pandoc isn't | ||
""" | ||
# TODO move to exclude_pattern | ||
base = os.path.dirname(__file__) | ||
notebooks = [os.path.join(base, 'source', nb) | ||
for nb in ['style.ipynb']] | ||
|
@@ -96,8 +78,106 @@ class DocBuilder: | |
All public methods of this class can be called as parameters of the | ||
script. | ||
""" | ||
def __init__(self, num_jobs=1): | ||
def __init__(self, num_jobs=1, include_api=True, single_doc=None): | ||
self.num_jobs = num_jobs | ||
self.include_api = include_api | ||
self.single_doc = None | ||
self.single_doc_type = None | ||
if single_doc is not None: | ||
self._process_single_doc(single_doc) | ||
self.exclude_patterns = self._exclude_patterns | ||
|
||
self._generate_index() | ||
if self.single_doc_type == 'docstring': | ||
self._run_os('sphinx-autogen', '-o', | ||
'source/generated_single', 'source/index.rst') | ||
|
||
@property | ||
def _exclude_patterns(self): | ||
"""Docs source files that will be excluded from building.""" | ||
# TODO move maybe_exclude_notebooks here | ||
if self.single_doc is not None: | ||
rst_files = [f for f in os.listdir(SOURCE_PATH) | ||
if ((f.endswith('.rst') or f.endswith('.ipynb')) | ||
and (f != 'index.rst') | ||
and (f != '{0}.rst'.format(self.single_doc)))] | ||
if self.single_doc_type != 'api': | ||
rst_files += ['generated/*.rst'] | ||
elif not self.include_api: | ||
rst_files = ['api.rst', 'generated/*.rst'] | ||
else: | ||
rst_files = ['generated_single/*.rst'] | ||
|
||
exclude_patterns = ','.join( | ||
'{!r}'.format(i) for i in ['**.ipynb_checkpoints'] + rst_files) | ||
|
||
return exclude_patterns | ||
|
||
def _process_single_doc(self, single_doc): | ||
"""Extract self.single_doc (base name) and self.single_doc_type from | ||
passed single_doc kwarg. | ||
|
||
""" | ||
self.include_api = False | ||
|
||
if single_doc == 'api.rst': | ||
self.single_doc_type = 'api' | ||
self.single_doc = 'api' | ||
elif os.path.exists(os.path.join(SOURCE_PATH, single_doc)): | ||
self.single_doc_type = 'rst' | ||
self.single_doc = os.path.splitext(os.path.basename(single_doc))[0] | ||
elif single_doc is not None: | ||
try: | ||
obj = pandas | ||
for name in single_doc.split('.'): | ||
obj = getattr(obj, name) | ||
except AttributeError: | ||
raise ValueError('Single document not understood, it should ' | ||
'be a file in doc/source/*.rst (e.g. ' | ||
'"contributing.rst" or a pandas function or ' | ||
'method (e.g. "pandas.DataFrame.head")') | ||
else: | ||
self.single_doc_type = 'docstring' | ||
if single_doc.startswith('pandas.'): | ||
self.single_doc = single_doc[len('pandas.'):] | ||
else: | ||
self.single_doc = single_doc | ||
|
||
def _copy_generated_docstring(self): | ||
"""Copy existing generated (from api.rst) docstring page because | ||
this is more correct in certain cases (where a custom autodoc | ||
template is used). | ||
|
||
""" | ||
fname = os.path.join(SOURCE_PATH, 'generated', | ||
'pandas.{}.rst'.format(self.single_doc)) | ||
temp_dir = os.path.join(SOURCE_PATH, 'generated_single') | ||
|
||
try: | ||
os.makedirs(temp_dir) | ||
except OSError: | ||
pass | ||
|
||
if os.path.exists(fname): | ||
try: | ||
# copying to make sure sphinx always thinks it is new | ||
# and needs to be re-generated (to pick source code changes) | ||
shutil.copy(fname, temp_dir) | ||
except: # noqa | ||
pass | ||
|
||
def _generate_index(self): | ||
"""Create index.rst file with the specified sections.""" | ||
if self.single_doc_type == 'docstring': | ||
self._copy_generated_docstring() | ||
|
||
with open(os.path.join(SOURCE_PATH, 'index.rst.template')) as f: | ||
t = jinja2.Template(f.read()) | ||
|
||
with open(os.path.join(SOURCE_PATH, 'index.rst'), 'w') as f: | ||
f.write(t.render(include_api=self.include_api, | ||
single_doc=self.single_doc, | ||
single_doc_type=self.single_doc_type)) | ||
|
||
@staticmethod | ||
def _create_build_structure(): | ||
|
@@ -121,7 +201,10 @@ def _run_os(*args): | |
-------- | ||
>>> DocBuilder()._run_os('python', '--version') | ||
""" | ||
subprocess.check_call(args, stderr=subprocess.STDOUT) | ||
# TODO check_call should be more safe, but it fails with | ||
# exclude patterns, needs investigation | ||
# subprocess.check_call(args, stderr=subprocess.STDOUT) | ||
os.system(' '.join(args)) | ||
|
||
def _sphinx_build(self, kind): | ||
"""Call sphinx to build documentation. | ||
|
@@ -142,11 +225,21 @@ def _sphinx_build(self, kind): | |
self._run_os('sphinx-build', | ||
'-j{}'.format(self.num_jobs), | ||
'-b{}'.format(kind), | ||
'-d{}'.format(os.path.join(BUILD_PATH, | ||
'doctrees')), | ||
'-d{}'.format(os.path.join(BUILD_PATH, 'doctrees')), | ||
'-Dexclude_patterns={}'.format(self.exclude_patterns), | ||
SOURCE_PATH, | ||
os.path.join(BUILD_PATH, kind)) | ||
|
||
def _open_browser(self): | ||
base_url = os.path.join('file://', DOC_PATH, 'build', 'html') | ||
if self.single_doc_type == 'docstring': | ||
url = os.path.join( | ||
base_url, | ||
'generated_single', 'pandas.{}.html'.format(self.single_doc)) | ||
else: | ||
url = os.path.join(base_url, '{}.html'.format(self.single_doc)) | ||
webbrowser.open(url, new=2) | ||
|
||
def html(self): | ||
"""Build HTML documentation.""" | ||
self._create_build_structure() | ||
|
@@ -156,6 +249,11 @@ def html(self): | |
if os.path.exists(zip_fname): | ||
os.remove(zip_fname) | ||
|
||
if self.single_doc is not None: | ||
self._open_browser() | ||
shutil.rmtree(os.path.join(SOURCE_PATH, 'generated_single'), | ||
ignore_errors=True) | ||
|
||
def latex(self, force=False): | ||
"""Build PDF documentation.""" | ||
self._create_build_structure() | ||
|
@@ -228,15 +326,24 @@ def main(): | |
type=str, | ||
default=os.path.join(DOC_PATH, '..'), | ||
help='path') | ||
argparser.add_argument('--docstring', | ||
metavar='FILENAME', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How much does it complicate things to do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, not, that wouldn't be to hard I think. Good idea |
||
type=str, | ||
nargs='*', | ||
default=None, | ||
help=('method or function name to compile, ' | ||
'e.g. "DataFrame.join"')) | ||
args = argparser.parse_args() | ||
|
||
if args.command not in cmds: | ||
raise ValueError('Unknown command {}. Available options: {}'.format( | ||
args.command, ', '.join(cmds))) | ||
|
||
os.environ['PYTHONPATH'] = args.python_path | ||
_generate_index(not args.no_api, args.single) | ||
getattr(DocBuilder(args.num_jobs), args.command)() | ||
|
||
getattr(DocBuilder(args.num_jobs, | ||
not args.no_api, | ||
args.single), args.command)() | ||
|
||
|
||
if __name__ == '__main__': | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this argument is not used anymore, is it?