Skip to content

Commit

Permalink
Add API reference (#35)
Browse files Browse the repository at this point in the history
* refactor: readd link to Python tutorial

* feat: generate API reference from code

* refactor: remove image link from images

* feat: add link to source code

* docs: add some requested docs

---------

Co-authored-by: Thomas Duignan <[email protected]>
  • Loading branch information
padix-key and tjduigna authored Aug 29, 2024
1 parent 9b495e4 commit 796ee4c
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 41 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ instance/
# Generated files in Sphinx documentation
docs/_build/
docs/table.html
docs/api/core
docs/api/scores

# PyBuilder
.pybuilder/
Expand Down
9 changes: 0 additions & 9 deletions docs/api.md

This file was deleted.

19 changes: 19 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
sd_hide_title: true
---

# Python API

## API reference

The ``plinder.core`` package provides utilities for interacting with the PLINDER dataset.

It contains an additional ``plinder.core.scores`` sub-package for querying parquet collections.

:::{toctree}
:maxdepth: 1
:hidden:

core/index
scores/index
:::
245 changes: 245 additions & 0 deletions docs/apidoc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# The code in this file is based on the file with the same name in the Biotite project
# licensed under BSD-3-clause license.

import enum
import shutil
import types
from importlib import import_module
from pathlib import Path
from textwrap import dedent
from types import ModuleType
from typing import Any

from sphinx.application import Sphinx

_INDENT = " " * 4


def generate_api_reference(package_name: str, output_dir: Path) -> None:
"""
Create the API reference for the given package and store it in the output directory.
This creates `.rst` files containing `autodoc` directives.
Parameters
----------
package_name : str
The name of the package to document.
output_dir : Path
The directory to store the generated files in.
Notes
-----
Use `.rst` files are `apidoc`and `numpydoc` produce reStructuredText formatted text.
"""
package = import_module(package_name)
class_names = []
function_names = []
for attr_name in package.__all__:
if _is_public_class(package, attr_name):
class_names.append(attr_name)
elif _is_public_function(package, attr_name):
function_names.append(attr_name)

# Remove existing rst files
if output_dir.exists():
shutil.rmtree(output_dir)
# Create rst files
output_dir.mkdir(parents=True, exist_ok=True)
_create_package_page(
output_dir / "index.rst", package_name, class_names, function_names
)
for class_name in class_names:
_create_class_page(
output_dir / f"{package_name}.{class_name}.rst", package_name, class_name
)
for function_name in function_names:
_create_function_page(
output_dir / f"{package_name}.{function_name}.rst",
package_name,
function_name,
)


def _create_package_page(
output_path: Path,
package_name: str,
classes: list[str],
functions: list[str],
):
"""
Create the `.rst` file for the package overview.
Parameters
----------
output_path : Path
The path to the output file.
package_name : str
The name of the package as it would be imported.
classes, functions : list of str
The names of the classes and functions to be documented, that are attributes of
the package, respectively.
"""
attributes = classes + functions
# Enumerate classes and functions
# `eval-rst` directive is necessary, as `autosummary` directive renders rst
summary_string = dedent(
"""
.. autosummary::
:nosignatures:
:toctree:
"""
)
summary_string += "\n".join([_INDENT + attr for attr in attributes])

# Assemble page
file_content = (
dedent(
f"""
``{package_name}``
{"=" * (len(package_name) + 4)}
.. currentmodule:: {package_name}
.. automodule:: {package_name}
.. currentmodule:: {package_name}
"""
)
+ summary_string
)
with open(output_path, "w") as f:
f.write(file_content)


def _create_class_page(output_path, package_name, class_name):
"""
Create the `.rst` file for the given class.
Parameters
----------
output_path : Path
The path to the output file.
package_name : str
The name of the package as it would be imported.
class_name : str
The name of the class to document, that is part of the package.
"""
file_content = dedent(
f"""
:sd_hide_title: true
``{class_name}``
{"=" * (len(class_name) + 4)}
.. autoclass:: {package_name}.{class_name}
:show-inheritance:
:members:
:member-order: bysource
:undoc-members:
:inherited-members:
"""
)
with open(output_path, "w") as f:
f.write(file_content)


def _create_function_page(output_path, package_name, function_name):
"""
Create the `.rst` file for the given function.
Parameters
----------
output_path : Path
The path to the output file.
package_name : str
The name of the package as it would be imported.
function_name : str
The name of the function to document, that is part of the package.
"""
file_content = dedent(
f"""
:sd_hide_title: true
``{function_name}``
{"=" * (len(function_name) + 4)}
.. autofunction:: {package_name}.{function_name}
"""
).strip()
with open(output_path, "w") as f:
f.write(file_content)


def _is_public_class(module: ModuleType, attr_name: str) -> bool:
"""
Check if the attribute is a public class.
"""
return _is_public(attr_name) and isinstance(getattr(module, attr_name), type)


def _is_public_function(module: ModuleType, attr_name: str) -> bool:
"""
Check if the attribute is a public function.
"""
return _is_public(attr_name) and callable(getattr(module, attr_name))


def _is_public(attr_name: str) -> bool:
"""
Check if the attribute is public.
"""
return not attr_name.startswith("_")


def skip_nonrelevant(
app: Sphinx, what: str, name: str, obj: Any, skip: bool, options: dict
) -> bool:
"""
Skip all class members, that are not methods, enum values or inner
classes, since other attributes are already documented in the class
docstring.
Furthermore, skip all class members, that are inherited from
non-PLINDER base classes.
See
https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#event-autodoc-skip-member
for more information.
"""
if skip:
return True
if not _is_relevant_type(obj):
return True
if obj.__module__ is None:
# Some built-in functions have '__module__' set to None
return True
package_name = obj.__module__.split(".")[0]
if package_name != "plinder":
return True
return False


def _is_relevant_type(obj: Any) -> bool:
"""
Check if the given object is an attribute that is worth documenting.
"""
if type(obj).__name__ == "method_descriptor":
# These are some special built-in Python methods
return False
return (
(
# Functions
type(obj)
in [types.FunctionType, types.BuiltinFunctionType, types.MethodType]
)
| (
# Enum instance
isinstance(obj, enum.Enum)
)
| (
# Inner class
isinstance(obj, type)
)
)
18 changes: 14 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import plinder

DOC_PATH = Path(__file__).parent
PACKAGE_PATH = DOC_PATH.parent / "src"
COLUMN_REFERENCE_PATH = DOC_PATH.parent / "column_descriptions"

# Avoid verbose logs in rendered notebooks
Expand All @@ -14,9 +13,13 @@
# Include documentation in PYTHONPATH
# in order to import modules for API doc generation etc.
sys.path.insert(0, str(DOC_PATH))
import apidoc
import tablegen
import viewcode

# Pregeneration of files
apidoc.generate_api_reference("plinder.core", DOC_PATH / "api" / "core")
apidoc.generate_api_reference("plinder.core.scores", DOC_PATH / "api" / "scores")
tablegen.generate_table(COLUMN_REFERENCE_PATH, DOC_PATH / "table.html")

#### Source code link ###
Expand All @@ -31,7 +34,7 @@
"sphinx.ext.autosummary",
"sphinx.ext.doctest",
"sphinx.ext.mathjax",
# "sphinx.ext.linkcode",
"sphinx.ext.linkcode",
"sphinx.ext.todo",
"sphinx_design",
"sphinx_copybutton",
Expand Down Expand Up @@ -60,6 +63,12 @@
]
myst_url_schemes = ("http", "https", "mailto")

numpydoc_show_class_members = False
# Prevent autosummary from using sphinx-autogen, since it would
# overwrite the document structure given by apidoc.json
autosummary_generate = False
linkcode_resolve = viewcode.linkcode_resolve

templates_path = ["templates"]
source_suffix = {
".rst": "restructuredtext",
Expand Down Expand Up @@ -126,10 +135,11 @@
"github_version": "main",
"doc_path": "doc",
}
html_scaled_image_link = False


#### App setup ####


# def setup(app):
# app.connect("autodoc-skip-member", apidoc.skip_nonrelevant)
def setup(app):
app.connect("autodoc-skip-member", apidoc.skip_nonrelevant)
Loading

0 comments on commit 796ee4c

Please sign in to comment.