diff --git a/.cspell.json b/.cspell.json index 1ac1e52..1143633 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,11 +1,5 @@ { "version": "0.2", - "enableFiletypes": [ - "git-commit", - "github-actions-workflow", - "julia", - "jupyter" - ], "flagWords": [ "analyse", "colour", @@ -20,6 +14,7 @@ "transisions" ], "ignorePaths": [ + "**/*.rst_t", "**/.cspell.json", ".editorconfig", ".gitignore", @@ -30,6 +25,8 @@ ], "language": "en-US", "words": [ + "ampform", + "apidoc", "autoapi", "autodoc", "ComPWA", @@ -46,6 +43,14 @@ "refspecific", "reftarget", "reftype", - "rtfd" + "rtfd", + "srcdir", + "templatedir" + ], + "enableFiletypes": [ + "git-commit", + "github-actions-workflow", + "julia", + "jupyter" ] } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f97da1a..2cdd9c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - id: blacken-docs - repo: https://github.com/ComPWA/repo-maintenance - rev: 0.1.6 + rev: 0.1.7 hooks: - id: check-dev-files args: diff --git a/README.md b/README.md index 0f1ee04..ff43d24 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,7 @@ Just install through [PyPI](https://pypi.org) with `pip`: pip install sphinx-api-relink ``` -Next, in your -[Sphinx configuration file](https://www.sphinx-doc.org/en/master/usage/configuration.html) -(`conf.py`), add `"sphinx_api_relink"` to your `extensions`: +Next, in your [Sphinx configuration file](https://www.sphinx-doc.org/en/master/usage/configuration.html) (`conf.py`), add `"sphinx_api_relink"` to your `extensions`: ```python extensions = [ @@ -50,3 +48,24 @@ api_target_types: dict[str, str] = { "RangeDefinition": "obj", } ``` + +## Generate API + +To generate the API for [`sphinx.ext.autodoc`](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html), add this to your `conf.py`: + +```python +api_package_path = "../src/my_package" # relative to conf.py +``` + +The API is generated with the same style used by the ComPWA repositories (see e.g. [ampform.rtfd.io/en/stable/api/ampform.html](https://ampform.readthedocs.io/en/stable/api/ampform.html)). To use the default template, set: + +```python +generate_apidoc_use_compwa_template = False +``` + +Other configuration values (with their defaults): + +```python +generate_apidoc_directory = "api" +generate_apidoc_excludes = ["version.py"] +``` diff --git a/pyproject.toml b/pyproject.toml index 17633bd..f41cdd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,10 @@ license-files = ["LICENSE"] package-dir = {"" = "src"} [tool.setuptools.package-data] -"*" = ["py.typed"] +"*" = [ + "py.typed", + "templates/*.rst_t", +] [tool.setuptools.packages.find] namespaces = false diff --git a/src/sphinx_api_relink/__init__.py b/src/sphinx_api_relink/__init__.py index bf18ae1..02aac6f 100644 --- a/src/sphinx_api_relink/__init__.py +++ b/src/sphinx_api_relink/__init__.py @@ -2,12 +2,15 @@ from __future__ import annotations +import shutil +from pathlib import Path from typing import TYPE_CHECKING, Any import sphinx.domains.python from docutils import nodes from sphinx.addnodes import pending_xref, pending_xref_condition from sphinx.domains.python import parse_reftarget +from sphinx.ext.apidoc import main as sphinx_apidoc if TYPE_CHECKING: from sphinx.application import Sphinx @@ -17,6 +20,11 @@ def setup(app: Sphinx) -> dict[str, Any]: app.add_config_value("api_target_substitutions", default={}, rebuild="env") app.add_config_value("api_target_types", default={}, rebuild="env") + app.add_config_value("generate_apidoc_directory", default="api", rebuild="env") + app.add_config_value("generate_apidoc_excludes", default=None, rebuild="env") + app.add_config_value("generate_apidoc_package_path", default=None, rebuild="env") + app.add_config_value("generate_apidoc_use_compwa_template", True, rebuild="env") + app.connect("config-inited", generate_apidoc) app.connect("config-inited", replace_type_to_xref) return { "parallel_read_safe": True, @@ -24,6 +32,50 @@ def setup(app: Sphinx) -> dict[str, Any]: } +def generate_apidoc(app: Sphinx, _: BuildEnvironment) -> None: + config_key = "generate_apidoc_package_path" + package_path: str | None = getattr(app.config, config_key, None) + if package_path is None: + return + abs_package_path = Path(app.srcdir) / package_path + apidoc_dir = Path(app.srcdir) / app.config.generate_apidoc_directory + _run_sphinx_apidoc( + abs_package_path, + apidoc_dir, + excludes=app.config.generate_apidoc_excludes, + use_compwa_template=app.config.generate_apidoc_use_compwa_template, + ) + + +def _run_sphinx_apidoc( + package_path: Path, + apidoc_dir: str = "api", + excludes: list[str] | None = None, + use_compwa_template: bool = True, +) -> None: + if not package_path.exists(): + msg = f"Package under {package_path} does not exist" + raise FileNotFoundError(msg) + shutil.rmtree(apidoc_dir, ignore_errors=True) + args: list[str] = [str(package_path)] + if excludes is None: + excludes = [] + version_file = "version.py" + if (package_path / version_file).exists(): + excludes.append(version_file) + for file in excludes: + excluded_path = package_path / file + if not excluded_path.exists(): + msg = f"Excluded file {excluded_path} does not exist" + raise FileNotFoundError(msg) + args.append(str(package_path / file)) + args.extend(f"-o {apidoc_dir} --force --no-toc --separate".split()) + if use_compwa_template: + template_dir = Path(__file__).parent / "templates" + args.extend(f"--templatedir {template_dir}".split()) + sphinx_apidoc(args) + + def replace_type_to_xref(app: Sphinx, _: BuildEnvironment) -> None: target_substitutions = _get_target_substitutions(app) ref_targets = { diff --git a/src/sphinx_api_relink/templates/module.rst_t b/src/sphinx_api_relink/templates/module.rst_t new file mode 100644 index 0000000..6d47710 --- /dev/null +++ b/src/sphinx_api_relink/templates/module.rst_t @@ -0,0 +1,12 @@ +{%- if show_headings and not separate %} +{{ basename.split(".")[-1] | e | heading }} + +.. code-block:: python + + import {{ basename }} + +{% endif -%} +.. automodule:: {{ qualname }} +{%- for option in automodule_options %} + :{{ option }}: +{%- endfor %} diff --git a/src/sphinx_api_relink/templates/package.rst_t b/src/sphinx_api_relink/templates/package.rst_t new file mode 100644 index 0000000..cac2570 --- /dev/null +++ b/src/sphinx_api_relink/templates/package.rst_t @@ -0,0 +1,49 @@ +{%- macro automodule(modname, options) -%} +.. automodule:: {{ modname }} +{%- for option in options %} + :{{ option }}: +{%- endfor %} +{%- endmacro %} + +{%- macro toctree(docnames) -%} +.. toctree:: +{% for docname in docnames %} + {{ docname }} +{%- endfor %} +{%- endmacro %} + +{{ pkgname.split(".")[-1] | e | heading }} + +.. code-block:: python + + import {{ pkgname }} + +{%- if modulefirst and not is_namespace %} +{{ automodule(pkgname, automodule_options) }} +{% endif %} + +{%- if not modulefirst and not is_namespace %} + +{{ automodule(pkgname, automodule_options) }} +{% endif %} + +{%- if submodules or subpackages %} +.. rubric:: Submodules and Subpackages +{% endif %} + +{%- if subpackages %} + +{{ toctree(subpackages) }} +{% endif %} +{%- if submodules %} +{% if separatemodules %} +{{ toctree(submodules) }} +{%- else %} +{%- for submodule in submodules %} +{% if show_headings %} +{{- [submodule, "module"] | join(" ") | e | heading(2) }} +{% endif %} +{{ automodule(submodule, automodule_options) }} +{% endfor %} +{%- endif %} +{% endif %} diff --git a/src/sphinx_api_relink/templates/toc.rst_t b/src/sphinx_api_relink/templates/toc.rst_t new file mode 100644 index 0000000..cba06b1 --- /dev/null +++ b/src/sphinx_api_relink/templates/toc.rst_t @@ -0,0 +1,7 @@ +{{ header | heading }} + +.. toctree:: + :maxdepth: {{ maxdepth }} +{% for docname in docnames %} + {{ docname }} +{%- endfor %}