From d11ab0b3450d49fa7fd56d1bc85614414fcf4504 Mon Sep 17 00:00:00 2001 From: xadupre Date: Sun, 3 Nov 2024 11:18:16 +0100 Subject: [PATCH 1/2] Command line to generate api --- _doc/conf.py | 1 + _unittests/ut_tools/test_sphinx_api.py | 22 +++++ sphinx_runpython/_cmd_helper.py | 30 +++++- sphinx_runpython/tools/sphinx_api.py | 127 +++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 _unittests/ut_tools/test_sphinx_api.py create mode 100644 sphinx_runpython/tools/sphinx_api.py diff --git a/_doc/conf.py b/_doc/conf.py index 44af3b6..a974501 100644 --- a/_doc/conf.py +++ b/_doc/conf.py @@ -106,6 +106,7 @@ } epkg_dictionary = { + "automodule": "https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-automodule", "black": "https://black.readthedocs.io/en/stable/index.html", "dot": "https://en.wikipedia.org/wiki/DOT_(graph_description_language)", "DOT": "https://en.wikipedia.org/wiki/DOT_(graph_description_language)", diff --git a/_unittests/ut_tools/test_sphinx_api.py b/_unittests/ut_tools/test_sphinx_api.py new file mode 100644 index 0000000..c3c8ab5 --- /dev/null +++ b/_unittests/ut_tools/test_sphinx_api.py @@ -0,0 +1,22 @@ +import os +import unittest +from sphinx_runpython.ext_test_case import ExtTestCase +from sphinx_runpython.tools.sphinx_api import sphinx_api + + +class TestSphinxApi(ExtTestCase): + + def test_this_doc_simulate(self): + doc = os.path.join(os.path.dirname(__file__), "..", "..", "sphinx_runpython") + res = sphinx_api(doc, simulate=True, verbose=1) + self.assertEmpty(res) + + def test_this_doc_write(self): + doc = os.path.join(os.path.dirname(__file__), "..", "..", "sphinx_runpython") + output = os.path.join(os.path.dirname(__file__), "temp_this_doc") + res = sphinx_api(doc, simulate=False, verbose=1, output_folder=output) + print(res) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/sphinx_runpython/_cmd_helper.py b/sphinx_runpython/_cmd_helper.py index e2e81b3..1bd1d5f 100644 --- a/sphinx_runpython/_cmd_helper.py +++ b/sphinx_runpython/_cmd_helper.py @@ -1,6 +1,6 @@ import glob import os -from argparse import ArgumentParser +from argparse import ArgumentParser, RawTextHelpFormatter from tempfile import TemporaryDirectory @@ -8,11 +8,16 @@ def get_parser(): parser = ArgumentParser( prog="sphinx-runpython command line", description="A collection of quick tools.", + formatter_class=RawTextHelpFormatter, epilog="", ) parser.add_argument( "command", - help="Command to run, only 'nb2py', 'readme', 'img2pdf' are available", + help="Command to run, only 'nb2py', 'readme', 'img2pdf', 'api' are available\n" + "- nb2py - converts notebooks into python\n" + "- readme - checks readme syntax\n" + "- img2pdf - converts impage to pdf\n" + "- api - generates sphinx documentation api", ) parser.add_argument( "-p", "--path", help="Folder or file which contains the files to process" @@ -23,6 +28,12 @@ def get_parser(): help="Recursive search.", action="store_true", ) + parser.add_argument( + "-r", + "--hidden", + help="shows hidden submodules as well", + action="store_true", + ) parser.add_argument( "-o", "--output", @@ -59,11 +70,26 @@ def nb2py(infolder: str, recursive: bool = False, verbose: int = 0): convert_ipynb_to_gallery(name, outfile=out) +def sphinx_api(infolder: str, output: str, recursive: bool = False, verbose: int = 0): + from .tools import sphinx_api as f + + f(infolder, output, recursive=recursive, verbose=verbose) + + def process_args(args): cmd = args.command if cmd == "nb2py": nb2py(args.path, recursive=args.recursive, verbose=args.verbose) return + if cmd == "api": + sphinx_api( + args.path, + recursive=args.recursive, + verbose=args.verbose, + output=args.output, + hidden=args.hidden, + ) + return if cmd == "img2pdf": from .tools.img_export import images2pdf diff --git a/sphinx_runpython/tools/sphinx_api.py b/sphinx_runpython/tools/sphinx_api.py new file mode 100644 index 0000000..6a8a893 --- /dev/null +++ b/sphinx_runpython/tools/sphinx_api.py @@ -0,0 +1,127 @@ +import os +import textwrap +from typing import Dict, List, Optional + + +def _write_doc_folder( + folder: str, pyfiles: List[str], hidden: bool = False, prefix: str = "" +) -> Dict[str, str]: + """ + Creates all the file in a dictionary. + """ + template = textwrap.dedent( + """ + + + + .. automodule:: + :members: + :no-undoc-members: + """ + ) + + index = textwrap.dedent( + """ + + + + .. toctree:: + :maxdepth: 1 + """ + ) + + submodule = ".".join(os.path.splitext(folder)[0].replace("\\", "/").split("/")) + fullsubmodule = f"{prefix}.{submodule}" if prefix else submodule + rows = [ + index.replace("", submodule) + .replace("", fullsubmodule) + .replace("", "=" * len(fullsubmodule)), + ] + res = {} + for name in sorted(pyfiles): + if not name: + continue + module_name = ".".join(os.path.splitext(name)[0].replace("\\", "/").split("/")) + last = module_name.split(".")[-1] + if not hidden and last[0] == "_" and last != "__init__": + continue + if not module_name or module_name in ("__main__", "__init__"): + continue + key = f"{module_name}.rst" + if module_name.endswith("__init__"): + module_name = ".".join(module_name.split(".")[:-1]) + full_module_name = f"{submodule}.{module_name}" + line = "=" * len(full_module_name) + text = ( + template.replace("", module_name) + .replace("", full_module_name) + .replace("", line) + ) + res[key] = text + rows.append(f" {last}") + + rows.append( + textwrap.dedent( + f""" + + .. automodule:: {submodule} + :members: + :no-undoc-members: + """ + ) + ) + res["index.rst"] = "\n".join(rows) + return res + + +def sphinx_api( + folder: str, + output_folder: Optional[str] = None, + simulate: bool = False, + hidden: bool = False, + verbose: int = 0, +): + """ + Creates simple pages to document a package. + Relies on :epkg:`automodule`. + + :param folder: folder to document + :param output_folder: where to write the result + :param simulate: prints out what the function will do + :param hidden: document file starting with `_` + :param verbose: verbosity + :return: list of written file + """ + root, package_name = os.path.split(folder) + files = [] + if verbose: + print(f"[sphinx_api] start creating API for {folder!r}") + for racine, dossiers, fichiers in os.walk(folder): + pyfiles = [f for f in fichiers if f.endswith(".py")] + if not pyfiles: + continue + mname = racine[len(root) + 1 :] + if verbose: + print(f"[sphinx_api] open {mname!r}") + content = _write_doc_folder(mname, pyfiles, hidden=hidden, prefix="") + if verbose: + print(f"[sphinx_api] close {mname!r}") + if simulate: + print(f"--+ {mname}") + for k, v in content.items(): + print(f" | {k}") + else: + assert output_folder, "output_folder is empty" + subfolder = os.path.join(output_folder, *mname.split("/")[1:]) + if verbose: + print(f"[sphinx_api] create {subfolder!r}") + if not os.path.exists(subfolder): + os.makedirs(subfolder) + for k, v in content.items(): + n = os.path.join(subfolder, k) + if verbose: + print(f"[sphinx_api] write {n!r}") + with open(n, "w") as f: + f.write(v) + files.append(n) + return files From 372d5ec1eefe0cf8f6abcf78abd1c334ffd6097c Mon Sep 17 00:00:00 2001 From: xadupre Date: Sun, 3 Nov 2024 11:43:42 +0100 Subject: [PATCH 2/2] fix a couple of bugs --- sphinx_runpython/_cmd_helper.py | 13 ++++--- sphinx_runpython/tools/sphinx_api.py | 51 ++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/sphinx_runpython/_cmd_helper.py b/sphinx_runpython/_cmd_helper.py index 1bd1d5f..783e716 100644 --- a/sphinx_runpython/_cmd_helper.py +++ b/sphinx_runpython/_cmd_helper.py @@ -29,7 +29,6 @@ def get_parser(): action="store_true", ) parser.add_argument( - "-r", "--hidden", help="shows hidden submodules as well", action="store_true", @@ -70,10 +69,16 @@ def nb2py(infolder: str, recursive: bool = False, verbose: int = 0): convert_ipynb_to_gallery(name, outfile=out) -def sphinx_api(infolder: str, output: str, recursive: bool = False, verbose: int = 0): - from .tools import sphinx_api as f +def sphinx_api( + infolder: str, + output: str, + recursive: bool = False, + hidden: bool = False, + verbose: int = 0, +): + from .tools.sphinx_api import sphinx_api as f - f(infolder, output, recursive=recursive, verbose=verbose) + f(infolder, output, verbose=verbose, hidden=hidden) def process_args(args): diff --git a/sphinx_runpython/tools/sphinx_api.py b/sphinx_runpython/tools/sphinx_api.py index 6a8a893..1436c6c 100644 --- a/sphinx_runpython/tools/sphinx_api.py +++ b/sphinx_runpython/tools/sphinx_api.py @@ -4,7 +4,11 @@ def _write_doc_folder( - folder: str, pyfiles: List[str], hidden: bool = False, prefix: str = "" + folder: str, + pyfiles: List[str], + hidden: bool = False, + prefix: str = "", + subfolders: Optional[List[str]] = None, ) -> Dict[str, str]: """ Creates all the file in a dictionary. @@ -24,9 +28,6 @@ def _write_doc_folder( """ - - .. toctree:: - :maxdepth: 1 """ ) @@ -37,7 +38,21 @@ def _write_doc_folder( .replace("", fullsubmodule) .replace("", "=" * len(fullsubmodule)), ] + if subfolders: + rows.append( + textwrap.dedent( + """ + .. toctree:: + :maxdepth: 1 + :caption: submodules + + """ + ) + ) + for sub in subfolders: + rows.append(f" {sub}/index") res = {} + has_module = False for name in sorted(pyfiles): if not name: continue @@ -58,6 +73,19 @@ def _write_doc_folder( .replace("", line) ) res[key] = text + if not has_module: + has_module = True + rows.append( + textwrap.dedent( + """ + + .. toctree:: + :maxdepth: 1 + :caption: modules + + """ + ) + ) rows.append(f" {last}") rows.append( @@ -92,18 +120,29 @@ def sphinx_api( :param verbose: verbosity :return: list of written file """ + folder = folder.rstrip("/\\") root, package_name = os.path.split(folder) files = [] if verbose: print(f"[sphinx_api] start creating API for {folder!r}") + for racine, dossiers, fichiers in os.walk(folder): pyfiles = [f for f in fichiers if f.endswith(".py")] if not pyfiles: continue - mname = racine[len(root) + 1 :] + mname = racine[len(root) + 1 :] if root else racine + selected = [ + d + for d in dossiers + if os.path.exists(os.path.join(racine, d, "__init__.py")) + ] if verbose: print(f"[sphinx_api] open {mname!r}") - content = _write_doc_folder(mname, pyfiles, hidden=hidden, prefix="") + if selected: + print(f"[sphinx_api] submodules {selected!r}") + content = _write_doc_folder( + mname, pyfiles, hidden=hidden, prefix="", subfolders=selected + ) if verbose: print(f"[sphinx_api] close {mname!r}") if simulate: