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..783e716 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,11 @@ def get_parser(): help="Recursive search.", action="store_true", ) + parser.add_argument( + "--hidden", + help="shows hidden submodules as well", + action="store_true", + ) parser.add_argument( "-o", "--output", @@ -59,11 +69,32 @@ 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, + hidden: bool = False, + verbose: int = 0, +): + from .tools.sphinx_api import sphinx_api as f + + f(infolder, output, verbose=verbose, hidden=hidden) + + 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..1436c6c --- /dev/null +++ b/sphinx_runpython/tools/sphinx_api.py @@ -0,0 +1,166 @@ +import os +import textwrap +from typing import Dict, List, Optional + + +def _write_doc_folder( + 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. + """ + template = textwrap.dedent( + """ + + + + .. automodule:: + :members: + :no-undoc-members: + """ + ) + + index = textwrap.dedent( + """ + + + """ + ) + + 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)), + ] + 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 + 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 + if not has_module: + has_module = True + rows.append( + textwrap.dedent( + """ + + .. toctree:: + :maxdepth: 1 + :caption: modules + + """ + ) + ) + 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 + """ + 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 :] 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}") + 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: + 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