diff --git a/doc/make.py b/doc/make.py index e3cb29aa3e086..3d4c0b4f4fbc7 100755 --- a/doc/make.py +++ b/doc/make.py @@ -14,10 +14,12 @@ 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__)) @@ -26,28 +28,6 @@ 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 +38,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 +77,73 @@ 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 = single_doc + self.single_doc_type = self._single_doc_type + self.exclude_patterns = self._exclude_patterns + + self._generate_index() + if self.single_doc_type == 'api': + 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 != self.single_doc))] + 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 + + @property + def _single_doc_type(self): + if self.single_doc: + if os.path.exists(os.path.join(SOURCE_PATH, self.single_doc)): + return 'rst' + try: + obj = pandas + for name in self.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: + return 'api' + + def _generate_index(self): + """Create index.rst file with the specified sections.""" + if self.single_doc_type == 'rst': + single_doc = os.path.splitext(os.path.basename(self.single_doc))[0] + self.include_api = False + elif self.single_doc_type == 'api' and \ + self.single_doc.startswith('pandas.'): + single_doc = self.single_doc[len('pandas.'):] + else: + single_doc = self.single_doc + + 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=single_doc, + single_doc_type=self.single_doc_type)) @staticmethod def _create_build_structure(): @@ -121,7 +167,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 +191,17 @@ 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): + url = os.path.join( + 'file://', DOC_PATH, 'build', 'html', + 'generated_single', '{}.html'.format(self.single_doc)) + webbrowser.open(url, new=2) + def html(self): """Build HTML documentation.""" self._create_build_structure() @@ -156,6 +211,9 @@ def html(self): if os.path.exists(zip_fname): os.remove(zip_fname) + if self.single_doc is not None: + self._open_browser() + def latex(self, force=False): """Build PDF documentation.""" self._create_build_structure() @@ -228,6 +286,13 @@ def main(): type=str, default=os.path.join(DOC_PATH, '..'), help='path') + argparser.add_argument('--docstring', + metavar='FILENAME', + 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: @@ -235,8 +300,10 @@ def main(): 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, + args.no_api, + args.single), args.command)() if __name__ == '__main__': diff --git a/doc/source/conf.py b/doc/source/conf.py index b5fbf096f2626..cf4ac6952dc30 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -86,38 +86,6 @@ if any(re.match("\s*api\s*", l) for l in index_rst_lines): autosummary_generate = True -files_to_delete = [] -for f in os.listdir(os.path.dirname(__file__)): - if (not f.endswith(('.ipynb', '.rst')) or - f.startswith('.') or os.path.basename(f) == 'index.rst'): - continue - - _file_basename = os.path.splitext(f)[0] - _regex_to_match = "\s*{}\s*$".format(_file_basename) - if not any(re.match(_regex_to_match, line) for line in index_rst_lines): - files_to_delete.append(f) - -if files_to_delete: - print("I'm about to DELETE the following:\n{}\n".format( - list(sorted(files_to_delete)))) - sys.stdout.write("WARNING: I'd like to delete those " - "to speed up processing (yes/no)? ") - if PY3: - answer = input() - else: - answer = raw_input() - - if answer.lower().strip() in ('y', 'yes'): - for f in files_to_delete: - f = os.path.join(os.path.join(os.path.dirname(__file__), f)) - f = os.path.abspath(f) - try: - print("Deleting {}".format(f)) - os.unlink(f) - except: - print("Error deleting {}".format(f)) - pass - # Add any paths that contain templates here, relative to this directory. templates_path = ['../_templates'] diff --git a/doc/source/index.rst.template b/doc/source/index.rst.template index eff1227e98994..7d11218564e32 100644 --- a/doc/source/index.rst.template +++ b/doc/source/index.rst.template @@ -106,8 +106,13 @@ Some other notes See the package overview for more detail about what's in the library. +{% if single_doc_type == 'api' -%} +.. autosummary:: + :toctree: generated_single/ +{% else -%} .. toctree:: :maxdepth: 4 +{% endif %} {% if single_doc -%} {{ single_doc }}