Docstring style for the Virtual Ecosystem package

The code below shows the docstring style that we have adopted for the Virtual Ecosystem, including notes on why we have adopted particular docstring conventions. We use the autodoc extension to sphinx to convert the docstrings in code modules into rendered HTML.

The results of using autodoc on the code below are shown here.

"""This is the documentation for the module. It does not start with a header line
because a header is required at the top of the markdown source page where the API docs
will be inserted using the ``automodule`` declaration, so we do not repeat it here.

That does mean that we need to stop ``flake8`` complaining about a missing blank line
after the first line and a missing full stop at the end of that line, which we can do
using the comment ``# noqa: D205, D415`` after the docstring closes.
"""  # noqa: D205, D415

AN_OBJECT: str = "An object in the module"
"""This is a docstring for a module attribute."""


class MyClass:
    """This is my class.

    This is some text about my class

    Args:
        myarg: An argument used to create an instance of MyClass. Arguments are
            documented here in the class docstring and not in an __init__ docstring.
    """

    a_class_attribute: int = 0
    """A Class attribute.

    Attributes are documented with docstrings underneath the attribute definition. Class
    attributes are grouped with instance attributes in the rendered documentation, so
    should say explictly that they are a class attribute."""

    def __init__(self, myarg: int, name: str) -> None:
        # Note there is no __init__ docstring.

        self.myarg: int = myarg
        """The myarg value used to create the instance.

        All attributes of a class instance should be defined and typed in the __init__
        method, even if they are not used or populated in the __init__ method. This
        ensures that they are picked up and included in the rendered docstrings.
        """

        self.name: str = name
        """The instance name."""

        self.myarg_multiple: int
        """An instance attribute.

        This attribute is not populated in ``__init__`` but must be defined and
        documented here."""

    def __repr__(self) -> str:
        """Create a text representation of the class."""
        return f"MyClass: {self.name}"

    def multiply_me(self, factor: int) -> int:
        """Multiples myarg by a factor.

        Methods must define any arguments and provide Returns and Raises sections if
        appropriate.

        Args:
            factor: An integer factor by which to multiply self.myarg.

        Returns:
            The value of self.myarg multiplied by the provided factor. This result is
            also stored in ``self.myarg_multiple``.

        Raises:
            ValueError: if the provided factor is not an integer.
        """

        if not isinstance(factor, float):
            raise ValueError(f"The factor value {factor} is not an integer.")

        self.myarg_multiple = self.myarg * factor

        return self.myarg_multiple


def multiplier(arg1: int, arg2: int) -> int:
    """Multiply two integers.

    This function calculates the product of two integers. The docstring documents the
    arguments, return value and any exceptions that can be raised.

    Args:
        arg1: The first integer
        arg2: The second integer

    Returns:
        The product of the two integers.

    Raises:
        ValueError: if either argument is not an integer.
    """

    if not (isinstance(arg1, int) and isinstance(arg2, int)):
        raise ValueError("Both arguments must be integers")

    return arg1 * arg2