Starlark docgen

Using the sphinx_stardoc rule, API documentation can be generated from bzl source code. This rule requires both MyST-based markdown and the sphinx_bzl Sphinx extension are enabled. This allows source code to use Markdown and Sphinx syntax to create rich documentation with cross references, types, and more.

Configuring Sphinx

While the sphinx_stardoc rule doesn't require Sphinx itself, the source it generates requires some additional Sphinx plugins and config settings.

When defining the sphinx_build_binary target, also depend on:

  • @rules_python//sphinxdocs/src/sphinx_bzl:sphinx_bzl
  • myst_parser (e.g. @pypi//myst_parser)
  • typing_extensions (e.g. @pypi//myst_parser)
sphinx_build_binary(
    name = "sphinx-build",
    deps = [
        "@rules_python//sphinxdocs/src/sphinx_bzl",
        "@pypi//myst_parser",
        "@pypi//typing_extensions",
        ...
    ]
)

In conf.py, enable the sphinx_bzl extension, myst_parser extension, and the colon_fence MyST extension.

extensions = [
    "myst_parser",
    "sphinx_bzl.bzl",
]

myst_enable_extensions = [
    "colon_fence",
]

Generating docs from bzl files

To convert the bzl code to Sphinx doc sources, sphinx_stardocs is the primary rule to do so. It takes a list of bzl_library targets or files and generates docs for each. When a bzl_library target is passed, the bzl_library.srcs value can only have a single file.

Example:

sphinx_stardocs(
    name = "my_docs",
    srcs = [
      ":binary_bzl",
      ":library_bzl",
    ]
)

bzl_library(
   name = "binary_bzl",
   srcs = ["binary.bzl"],
   deps = ...
)

bzl_library(
   name = "library_bzl",
   srcs = ["library.bzl"],
   deps = ...
)

User-defined types

While Starlark doesn‘t have user-defined types as a first-class concept, it’s still possible to create such objects using struct and lambdas. For the purposes of documentation, they can be documented by creating a module-level struct with matching fields and also a field named TYPEDEF. When the sphinx_stardoc rule sees a struct with a TYPEDEF field, it generates doc using the {rst:directive}bzl:typedef directive and puts all the struct's fields within the typedef. The net result is the rendered docs look similar to how a class would be documented in other programming languages.

For example, a the Starlark implemenation of a Square object with a area() method would look like:


def _Square_typedef(): """A square with fixed size. :::{field} width :type: int ::: """ def _Square_new(width): """Creates a Square. Args: width: {type}`int` width of square Returns: {type}`Square` """ self = struct( area = lambda *a, **k: _Square_area(self, *a, **k), width = width ) return self def _Square_area(self, ): """Tells the area of the square.""" return self.width * self.width Square = struct( TYPEDEF = _Square_typedef, new = _Square_new, area = _Square_area, )

This will then genereate markdown that looks like:

::::{bzl:typedef} Square
A square with fixed size

:::{bzl:field} width
:type: int
:::
:::{bzl:function} new()
...args etc from _Square_new...
:::
:::{bzl:function} area()
...args etc from _Square_area...
:::
::::

Which renders as:

:::{bzl:currentfile} //example:square.bzl :::

::::{bzl:typedef} Square A square with fixed size

:::{bzl:field} width :type: int ::: :::{bzl:function} new() ... ::: :::{bzl:function} area() ... ::: ::::