Bazel plugin for Sphinx

The sphinx_bzl Python package is a Sphinx plugin that defines a custom domain (“bzl”) in the Sphinx system. This provides first-class integration with Sphinx and allows code comments to provide rich information and allows manually writing docs for objects that aren't directly representable in bzl source code. For example, the fields of a provider can use :type: to indicate the type of a field, or manually written docs can use the {bzl:target} directive to document a well known target.

Configuring Sphinx

To enable the plugin in Sphinx, depend on @rules_python//sphinxdocs/src/sphinx_bzl and enable it in conf.py:

extensions = [
    "sphinx_bzl.bzl",
]

Brief introduction to Sphinx terminology

To aid understanding how to write docs, lets define a few common terms:

  • Role: A role is the “bzl:obj” part when writing {bzl:obj}`ref` . Roles mark inline text as needing special processing. There's generally two types of processing: creating cross references, or role-specific custom rendering. For example {bzl:obj} will create a cross references, while {bzl:default-value} indicates the default value of an argument.
  • Directive: A directive is indicated with ::: and allows defining an entire object and its parts. For example, to describe a function and its arguments, the :::{bzl:function} directive is used.
  • Directive Option: A directive option is the “type” part when writing :type: within a directive. Directive options are how directives are told the meaning of certain values, such as the type of a provider field. Depending on the object being documented, a directive option may be used instead of special role to indicate semantic values.

Most often, you‘ll be using roles to refer other objects or indicate special values in doc strings. For directives, you’re likely to only use them when manually writing docs to document flags, targets, or other objects that sphinx_stardoc generates for you.

MyST vs RST

By default, Sphinx uses ReStructured Text (RST) syntax for its documents. Unfortunately, RST syntax is very different than the popular Markdown syntax. To bridge the gap, MyST translates Markdown-style syntax into the RST equivalents. This allows easily using Markdown in bzl files.

While MyST isn't required for the core sphinx_bzl plugin to work, this document uses MyST syntax because sphinx_stardoc bzl doc gen rule requires MyST.

The main difference in syntax is:

  • MyST directives use :::{name} with closing ::: instead of .. name:: with indented content.
  • MyST roles use {role:name} instead of :role:name:

Type expressions

Several roles or fields accept type expressions. Type expressions use Python-style annotation syntax to describe data types. For example None | list[str] describes a type of “None or a list of strings”. Each component of the expression is parsed and cross reference to its associated type definition.

Cross references

In brief, to reference bzl objects, use the bzl:obj role and use the Bazel label string you would use to refer to the object in Bazel (using % to denote names within a file). For example, to unambiguously refer to py_binary:

{bzl:obj}`@rules_python//python:py_binary.bzl%py_binary`

The above is pretty long, so shorter names are also supported, and sphinx_bzl will try to find something that matches. Additionally, in .bzl code, the bzl: prefix is set as the default. The above can then be shortened to:

{obj}`py_binary`

The text that is displayed can be customized by putting the reference string in chevrons (<>):

{obj}`the binary rule <py_binary>`

Specific types of objects (rules, functions, providers, etc) can be specified to help disambiguate short names:

{function}`py_binary`  # Refers to the wrapping macro
{rule}`py_binary`  # Refers to the underlying rule

Finally, objects built into Bazel can be explicitly referenced by forcing a lookup outside the local project using {external}. For example, the symbol toolchain is a builtin Bazel function, but it could also be the name of a tag class in the local project. To force looking up the builtin Bazel toolchain rule, {external:bzl:rule} can be used, e.g.:

{external:bzl:obj}`toolchain`

Those are the basics of cross referencing. Sphinx has several additional syntaxes for finding and referencing objects; see the MyST docs for supported syntaxes

Cross reference roles

A cross reference role is the obj portion of {bzl:obj}. It affects what is searched and matched.

:::{note} The documentation renders using RST notation (:foo:role:), not MyST notation ({foo:role}. :::

:::{rst:role} bzl:arg Refer to a function argument. :::

:::{rst:role} bzl:attr Refer to a rule attribute. :::

:::{rst:role} bzl:flag Refer to a flag. :::

:::{rst:role} bzl:obj Refer to any type of Bazel object :::

:::{rst:role} bzl:rule Refer to a rule. :::

:::{rst:role} bzl:target Refer to a target. :::

:::{rst:role} bzl:type Refer to a type or type expression; can also be used in argument documentation.

def func(arg):
    """Do stuff

    Args:
      arg: {type}`int | str` the arg
    """
    print(arg + 1)

:::

Special roles

There are several special roles that can be used to annotate parts of objects, such as the type of arguments or their default values.

:::{note} The documentation renders using RST notation (:foo:role:), not MyST notation ({foo:role}. :::

:::{rst:role} bzl:default-value

Indicate the default value for a function argument or rule attribute. Use it in the Args doc of a function or the doc text of an attribute.

def func(arg=1):
   """Do stuff

   Args:
     foo: {default-value}`1` the arg

my_rule = rule(attrs = {
    "foo": attr.string(doc="{default-value}`bar`)
})

:::

:::{rst:role} bzl:return-type

Indicates the return type for a function. Use it in the Returns doc of a function.

def func():
    """Do stuff

    Returns:
      {return-type}`int`
    """
    return 1

:::

Directives

Most directives are automatically generated by sphinx_stardoc. Here, we only document ones that must be manually written.

To write a directive, a line starts with 3 to 6 colons (:), followed by the directive name in braces ({}), and eventually ended by the same number of colons on their own line. For example:

:::{bzl:target} //my:target

Doc about target
:::

:::{note} The documentation renders using RST notation (.. directive::), not MyST notation. :::

:::{rst:directive} .. bzl:currentfile:: file

This directive indicates the Bazel file that objects defined in the current documentation file are in. This is required for any page that defines Bazel objects. The format of file is Bazel label syntax, e.g. //foo:bar.bzl for bzl files, and //foo:BUILD.bazel for things in BUILD files.

:::

:::{rst:directive} .. bzl:target:: target

Documents a target. It takes no directive options. The format of target can either be a fully qualified label (//foo:bar), or the base target name relative to {bzl:currentfile}.

:::{bzl:target} //foo:target

My docs
:::

:::{rst:directive} .. bzl:flag:: target

Documents a flag. It has the same format as {bzl:target} :::