Raw File
# Copyright 2019 GPflow Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
"""Script to autogenerate .rst files for autodocumentation of classes and modules in GPflow.
To be run by the CI system to update docs.
import inspect
from datetime import datetime
from pathlib import Path
from types import ModuleType
from typing import Any, Callable, List, Set, Tuple

RST_LEVEL_SYMBOLS = ["=", "-", "~", '"', "'", "^"]


.. autoclass:: {object_name}


This function uses multiple dispatch, which will depend on the type of argument passed in:


.. code-block:: python

    {dispatch_name}( {args} )
    # dispatch to -> {true_name}(...)

.. autofunction:: {true_name}


.. autofunction:: {object_name}

.. toctree::
   :maxdepth: 1


SPHINX_FILE_STRING = """{headerline}

.. GENERATED BY `generate_rst.py`
.. DATE: {date}

.. automodule:: {node_name}



DATE_STRING = datetime.strftime(datetime.now(), "%d/%m/%y")

def is_documentable_module(m: Any) -> bool:
    """Return `True` if m is module to be documented automatically, `False` otherwise."""
    return inspect.ismodule(m) and "gpflow" in m.__name__ and m.__name__ not in IGNORE_MODULES

def is_documentable_component(m: Any) -> bool:
    """Return `True` if a function or class to be documented automatically, `False` otherwise."""
    if inspect.isfunction(m):
        return "gpflow" in m.__module__ and m.__module__ not in IGNORE_MODULES
    elif inspect.isclass(m):
        return "gpflow" in m.__module__ and m.__module__ not in IGNORE_MODULES
    elif type(m).__name__ == "Dispatcher":
        return True

    return False

def is_documentable(m: Any) -> bool:
    Return `True` if a function, class, or module to be documented automatically, else `False`.
    return is_documentable_component(m) or is_documentable_module(m)

def get_component_rst_string(module: ModuleType, component: Callable[..., Any], level: int) -> str:
    """Get a rst string, to autogenerate documentation for a component (class or function)

    :param module: the module containing the component
    :param component: the component (class or function)
    :param level: the level in nested directory structure
    object_name = f"{module.__name__}.{component.__name__}"

    rst_documentation = ""
    level_underline = RST_LEVEL_SYMBOLS[level] * len(object_name)
    if inspect.isclass(component):
        rst_documentation = SPHINX_CLASS_STRING.format(
            object_name=object_name, var=component.__name__, level=level_underline
    elif inspect.isfunction(component):
        rst_documentation = SPHINX_FUNC_STRING.format(
            object_name=object_name, var=component.__name__, level=level_underline
    elif type(component).__name__ == "Dispatcher":
        rst_documentation = get_multidispatch_string(component, module, level)

    return rst_documentation

def get_multidispatch_string(
    md_component: Callable[..., Any], module: ModuleType, level: int
) -> str:
    """Get the string for a multiple dispatch component. This involves iterating through the
    possible functions and arguments and creating strings for each of these items.

    :param md_component: the multidispatch component (wrapped around functions)
    :param module: the module containing the component
    :param level: the level in nested directory structure
    content_list = []
    dispatch_name = f"{module.__name__}.{md_component.name}"  # type: ignore
    level_underline = RST_LEVEL_SYMBOLS[level] * len(dispatch_name)
    for args, fname in md_component.funcs.items():  # type: ignore

        arg_names = ", ".join([a.__name__ for a in args])
        alias_name = f"{fname.__module__}.{fname.__name__}"

            dispatch_name=dispatch_name, args=arg_names, true_name=alias_name
    content = "\n".join(content_list)
        object_name=dispatch_name, level=level_underline, content=content

def get_module_rst_string(module: ModuleType, level: int) -> str:
    """Get an rst string, used to autogenerate documentation for a module

    :param module: the module containing the component
    :param level: the level in nested directory structure
    level_underline = RST_LEVEL_SYMBOLS[level] * len(module.__name__)
        name=module.__name__, module=module.__name__.split(".")[-1], level=level_underline

def get_public_attributes(node: Any) -> Any:
    """Get the public attributes ('children') of the current node, accessible from this node."""
    return [getattr(node, a) for a in dir(node) if not a.startswith("_")]

def write_to_rst_file(node_name: str, rst_content: str, dest: Path) -> None:
    """Write rst_content to a file, for a certain node.

    :param node_name: name of the node to write to file
    :param rst_content: List of rst strings to write to file
    level_underline = RST_LEVEL_SYMBOLS[0] * len(node_name)
    rst_file = SPHINX_FILE_STRING.format(

    file_dir = dest / f"{node_name.replace('.', '/')}"
    file_dir.mkdir(parents=True, exist_ok=True)
    path = file_dir / "index.rst"
    print("Wrote", path)

def do_visit_module(module: ModuleType, enqueued_items: Set[int]) -> bool:
    """Decide whether to document this module or not, by checking its attributes and deciding
    if there is something worth documenting there

    :param module: module to document
    :param enqueued_items: enqueue the items
    for child in get_public_attributes(module):
        if is_documentable_module(child) and id(child) not in enqueued_items:
            # There is a module we have not visited
            return True
        elif (
            and id(child) not in enqueued_items
            and ("__init__" in child.__module__ or module.__name__ in child.__module__)
            # There is a class or function (or alias of them) we have not visited
            return True
    return False

def traverse_module_bfs(queue: List[Tuple[Any, int]], enqueued_items: Set[int], dest: Path) -> None:
    We will traverse the module in the queue to generate .rst files, that will be used by sphinx.
    We do this to avoid having to add new classes or modules to the documentation.
    We traverse the module breadth-first, and check `id` of modules to prevent double documentation
    of same items. We traverse breadth first so that when an alias has been created:
        ie - gpflow.kernels.Matern52 == gpflow.kernels.stationaries.Matern52
    we take the path closest to the root (in this case: goflow.kernels.Matern52)

    :param queue: The queue which contains the module and the starting depth. Usually: [(gpflow, 0)]
    :param enqueued_items: The set tracks objects already in the queue, with `id`: set([id(gpflow)])
    :param dest: Root directory to write generated documentation to.
    while queue:
        node, level = queue.pop(0)  # currently using a list as a queue (not great)

        if not hasattr(node, "__name__"):

        if is_documentable_module(node):

            rst_components, rst_modules = [], []
            for child in get_public_attributes(node):

                if id(child) in enqueued_items:

                if is_documentable_component(child):
                    rst_components.append(get_component_rst_string(node, child, level))

                elif is_documentable_module(child):
                    if do_visit_module(child, enqueued_items):
                        rst_modules.append(get_module_rst_string(child, level))
                        queue.append((child, level + 1))

            rst_content = "\n".join(rst_components + rst_modules)
            if rst_content:
                write_to_rst_file(node.__name__, rst_content, dest)

def generate_module_rst(module: ModuleType, dest: Path) -> None:
    Generate documentation for a module.

    :param module: Module to document.
    :param dest: Root directory to write generated documentation to.
    traverse_module_bfs([(module, 0)], {id(module)}, dest)
back to top