Skip to content

Documentation standards

Rodrigo Girão Serrão edited this page Jan 31, 2023 · 17 revisions

This document is currently a draft and nothing of what is written here must be taken for a fact or as an actual recommendation.

This document describes the documentation standards followed when documenting the Textual codebase.

This document serves as a set of guidelines aimed at removing the burden of making decisions from the person writing documentation. Above all, use your best judgement and don't be afraid to “go against” these recommendations when it makes sense.

How do I document a...

function / method

  • Short function description in the first line.
  • (optional) Blank line + information about the semantics of the function. In general, omit implementation details.
  • Section Examples if it makes sense to include usage examples if needed.
  • Section Args if there are arguments.
  • Section Raises if the function raises exceptions.
  • Section Returns if the function returns a value.
  • Add type hints to arguments and return value.
Example
class App:
    # ...

    def get_child_by_id(
        self, id: str, expect_type: type[ExpectType] | None = None
    ) -> ExpectType | Widget:
        """Shorthand for self.screen.get_child(id: str).

        Returns the first child (immediate descendent) of this DOMNode
        with the given ID.

        Args:
            id: The ID of the node to search for.
            expect_type: Require the object be of the supplied type, or None for any type.
                Defaults to None.

        Returns:
            The first child of this node with the specified ID.

        Raises:
            NoMatches: if no children could be found for this ID
            WrongType: if the wrong type was found.
        """
        return (
            self.screen.get_child_by_id(id)
            if expect_type is None
            else self.screen.get_child_by_id(id, expect_type)
        )

generator

Document in the same way as a function / method, but use the section Yields instead of Returns.

Example
class DOMQuery(Generic[QueryType]):
    # ...

    def results(
        self, filter_type: type[ExpectType] | None = None
    ) -> Iterator[Widget | ExpectType]:
        """Get query results, optionally filtered by a given type.

        Args:
            filter_type: A Widget class to filter results,
                or None for no filter. Defaults to None.

        Yields:
            An iterator of Widget instances.
        """
        if filter_type is None:
            yield from self
        else:
            for node in self:
                if isinstance(node, filter_type):
                    yield node

class

  • Short description in the first line.
  • (optional) Blank line + information about the semantics of the function. In general, omit implementation details.
  • Section Examples when it makes sense.

Do not:

  • Document the attributes here.
  • Add the section Attributes: here.
Example
class Animator:
    """An object to manage updates to a given attribute over a period of time."""

    def __init__(self, app: App, frames_per_second: int = 60) -> None:

method __init__

Document a method __init__ as you would document any other function / method. Our tooling will show the documentation for cls.__init__ under cls.

class attribute

This applies to attributes

  • Add a docstring under the attribute.
  • Add the appropriate type hint to the attribute even if it is inferrable from the context, otherwise the documentation tooling won't know the type.
  • Consider listing attributes in alphabetical order.
Example
class Button(Widget):
    # ...
    
    ACTIVE_EFFECT_DURATION: float = 0.3
    """Buttons clicked get the class `-active` for these many seconds."""

class instance attribute

This applies to attributes that are initialised in `init.

  • Add a docstring under the attribute.
  • Add the appropriate type hint to the attribute even if it is inferrable from the context, otherwise the documentation tooling won't know the type.
  • Consider listing attributes in alphabetical order.
Example
class Person:
    """Model a human being."""

    def __init__(self, name: str):
        self.name: str = name  # Explicit type hint is for the documentation tooling.
        """The legal name of this person."""

property

Document like a regular attribute. Do not add a section Returns: to properties.

  • Short description under the header.
  • Add a type hint for the return value.
Example
class Color:
    # ...

    @property
    def r(self) -> int:
        """Red channel value."""
        return int(self.hex_value[:2], 16)

widget

In the source code, follow the guidelines to document a class, attributes, and methods. Additionally:

  • Make sure the attribute BINDINGS has an appropriate table (generate the boilerplate by running tools/widget_documentation.py).
  • Make sure the attribute COMPONENT_CLASSES has an appropriate table (generate the boilerplate by running tools/widget_documentation.py).
  • Create the reference page with the template provided in docs/widgets/_template.md.
Example
class MyWidget(Widget):
    """Does X in the UI."""

    BINDINGS: ClassVar[list[BindingType]] = [("space", "handler_name", "description")]
    """
    | Key(s) | Description |
    | :- | :- |
    | space | Do X, Y, and Z. |
    """

    COMPONENT_CLASSES: ClassVar[set[str]] = {"mywidget--cursor"}
    """
    | Class | Description |
    | :- | :- |
    | `mywidget--cursor` | Target the cursor. |
    """

message / event

  • See how to document a class.
    • Include the handler name in the extended docstring.
  • See how to document an __init__ method.
  • See how to document attributes.
Example
class Input(Widget):
    # ...

    class Changed(Message, bubble=True):
        """Emitted when the value changes.

        Can be handled using `on_input_changed` in a subclass of `Input` or in a parent
        widget in the DOM.

        Attributes:
            value: The value that the input was changed to.
            input: The `Input` widget that was changed.
        """

        def __init__(self, sender: Input, value: str) -> None:
            super().__init__(sender)
            self.value: str = value
            self.input: Input = sender

General style guidelines

  • Keep the docstrings as close as possible to the entity being documented.

  • Do not leave an empty blank line at the end of the docstring.

  • Do not write type hints in comments. Our tooling generally picks it up from the actual signatures in the code and that deduplicates things that need to be maintained.

  • Docstrings are always under what they are documenting, regardless of whether you are documenting a class or a variable.

  • Default to alphabetical order if no other ordering is obvious.

  • In long-form comments/descriptions, start new sentences in new lines:

    • they are easier to read;
    • they are easier to maintain; and
    • they generate better diffs.
  • Write full sentences in description columns in tables:

    • upper-case first letter; and
    • full stop at the end.
  • If list items start with upper case, they end with full-stop. Otherwise, they end with semicolon. Keep it homogenous in the same list/document.

  • Use sass for Textual CSS code blocks.

  • Use complete sentences in tables: capitalise the first letter and end with a punctuation mark.

  • Do Not Use Title Case in markdown files.

Special docstring sections

The plugin mkdocstrings recognises some special sections, depending on the style we use for our docstrings (Google style) and depending on the context (are we documenting a class attribute? A function? A property?).

The list below is not exhaustive. Instead, it shows the sections that are more commonly used in the Textual codebase.

Args

Special section to document the arguments of a callable.

Do not:

  • add explicit type hints in the docstring; and
  • write the default value by hand.

Example:

def my_function(x: int = 42, b: bool = False) -> int:
    """
    Args:
        x: Description of what `x` is for. This can extend a bit if needed,
            in which case it must be indented to distinguish from the documentation
            for the other arguments.
        b: A super useful Boolean.
    """

Attributes

We generally do not use this section.

Special section to document the attributes of a class.

See the explanation/example for this section nonetheless.

This creates a summary table with attribute names, types, and descriptions. For attributes to be adequately typed in the Attributes section, they must be typed explicitly (either in the body or in __init__).

Example:

class MyClass:
    """
    Attributes:
        attr1: This attribute gets its type hint from the body of the class.
        attr2: This attribute gets its type hint from the method `__init__`.
            This is an extended description of the attribute.
    """

    attr1: bool

    def __init__(self):
        self.attr1 = True
        self.attr2: int = 42  # The explicit type hint here is for `Attributes` to pick it up.

Examples

Special section to write down usage examples. Only useful when the examples you want to write out make sense in a REPL session.

Example:

def add_with_a_twist(a: int, b: int) -> int:
    """Adds two integers with a twist.

    Examples:
        You can write prose and also snippets from a REPL session.

        >>> add_with_a_twist(10, 5)
        5

        >>> add_with_a_twist(5, 10)
        -5
    """

    return a - b

Raises

Special section to document exceptions the code may raise. This creates a summary table with all the exceptions that the code might raise and the reasons that may lead to those exceptions.

class MyException(Exception):
    pass


def my_division(a: int, b: int) -> float:
    """
    Raises:
        MyException: When the result of the division would be negative.
        ZeroDivisionError: When `b` is zero.
    """

    if a < 0 and b > 0 or a > 0 and b < 0:
        raise MyException("Why?!")
    return a / b

Returns

Special section to document the return value(s) of methods and functions. Do not add explicit type hints to the docstring. (Use Yields for generators.)

import random


def dice_roll() -> int:
    """
    Returns:
        A random integer between 1 and 6, inclusive.
    """
    return random.randint(1, 6)

If the return type is a tuple, each value can be documented separately:

def split_name(full_name: str) -> tuple[str, str, str]:
    """
    Returns:
        The first name of the person.
        All the middle names of the person.
            This may be an empty string if no middle names are present.
        The last name of the person.
            This may be an empty string if there is no last name.
    """
    first, *others = full_name.split(" ", maxsplit=1)
    *middle, last = (others[0] if others else "").rsplit(" ", maxsplit=1)
    return first, (middle[0] if middle else ""), last

References