1. How to docstring

Welcome to the How-To on creating documentation for your functions, classes and submodules. Here we explain shortly how the documentation works and give examples for how to write proper docstrings.

The PorePy documentation is created using Sphinx, an engine effectively importing the Python package and extracting docstrings. The docstrings are then used to fill a pre-defined structure and create a HTML-based documentation.

Docstrings are written using a markup language called reStructuredText (short rst). A quick overview can be found at rst-quickref. You can also find vast support for rst elsewhere on the internet.

Though PorePy closely follows the Google Python Style, it introduces minor adaptions for readability and functionality reasons. After familiarizing yourself with rst and the Google style, you can read through the rest of this How-To to get to know the PorePy specifics.

Once you have programmed, tested, documented and merged your contribution into the PorePy source, contact one of the core developers to ensure a proper integration of your documentation into the whole structure.

In the remaining section we demonstrate how to document various aspects of your code in PorePy style.

While we aim for providing complete instructions, we cannot guarantee the coverage of every possible case of the vast Python world. If you are unsure about an issue, don’t hesitate to contact one of the core developers.

Next to the documented signature of functions and classes you will find links [source] to the source code example_docstrings.py, where you can see the raw docstring in all its rst-glory and the respective Python code. This way you will be able to directly compare what certain syntax will in the end look like in the compiled format.

Once your code is properly documented, it will be included using autodoc directives into the global structure of the PorePy documentation. This will be done in a next step. Before that, familiarize yourself with the various directives and their options beforehand.

1.1. Docstring-Basics

  1. Python docstrings are started and ended using a triple """, and are placed below the declaration of an object.

    var_1: int = 1
    """This is a docstring for ``var_1``."""
    
  2. Inline literals are rendered using ``: ``literal`` becomes literal.

  3. Every member (variable, function, class or method) must have a docstring. Though only public members are included by current configurations, it is demanded that all members, including private and special members have a proper docstring.

  4. From the docstring it should be clear what an object does or is, what it takes as arguments and what it returns. Avoid overly scientific or complicated language.

  5. Sphinx provides extensive linking and cross-referencing features. See e.g., Cross-referencing syntax and Python object referencing. The domain :py can be omitted in this project.

  6. Limit the length of every line to 88 characters. PorePy’s PR routines include checks using isort, black, flake8 and mypy, which check exactly that.

    Hint

    Most IDEs support a modification of the code editor such that a vertical line is displayed after the 88th character column.

  7. End every multi-line docstring with a blank line before """, independent of what you are documenting.

1.1.1. Directives

Next to plain text, the content of your documentation can be created using directives, followed by a block of indented text.

Directives are structural blocks inside the text of a documentation. They provide specific formatting for sections dedicated to e.g., function parameters, return values or exmaple code.

For example, the note directive in plain rst code

.. note:: Implementation

    This is a note about the way something is implemented.

will compile into

Note

Implementation

This is a note about the way something is implemented.

Directive breaks are created by resuming non-indented text. They are also indirectly created anytime a new section or directive starts.

Other useful directives are:

.. code-block:: Python
    :linenos:

    # we demonstrate an example usage of Python code,
    #including the option 'linenos' to display line numbers
    print("Hello World!")

.. deprecated:: VERSIONNUMBER
    This is deprecated, use ``X`` instead.

.. todo:: missing functionality

    * This is a a to-do list
    * for missing functionality.

resulting in

1# we demonstrate an example usage of Python code,
2#including the option 'linenos' to display line numbers
3print("Hello World!")

Deprecated since version VERSIONNUMBER: This is deprecated, use X instead.

Todo

missing functionality

  • This is a a to-do list

  • for missing functionality.

Other types of useful directives in Sphinx can be found online.

Warning

Some directives have all kind of options. Options demand to be at the top of the indented text and a blank line between them and the directive’s content.

Additionally, some directives allow text or input after ::, some do not. Some have no options at all and you do not need blank lines. Some need blank lines before and after the directive and its indented block. Etc….

Get familiar with each directive before you use it.

1.2. PorePy style

As mentioned above, the PorePy style is Google Style with minor changes. These changes involve the developer team’s principles of good code and aesthetics. They also involve the project’s style guide, especially naming conventions. For more information check out the official PorePy User Manual. Some general rules are provided below. Examples of acceptable documentation are provided in the subsections after.

Naming conventions

Certain acronyms are reserved and developers are encouraged to use them consistently in their docstrings to provide a uniform appearance. Acronyms are formatted with inline literals using ``x``. These include (among others):

  • sd : single SubDomain

  • mdg : MixedDimensionalGrid

  • p, T, u,… : names of common physical variables

  • num_dofs : number of degrees of freedom

  • nd : ambient dimension of a mdg, corresponding to the highest dimension of subdomains

  • num_points : number of points

  • num_cells : number of cells

Google style directives

Google style directives are aliases for standard directives, which simplify the usage and syntax. Besides the obligatory declaration of parameters, raised errors and return values, the PorePy style encourages the use of other sections to provide notes, example code usage, references, etc.

For a list of all supported Google style directives, see Google style docstring sections.

The compulsory order of directives inside a docstring is:

  1. Examples, Note, …

  2. References, See Also

  3. Parameters

  4. Raises

  5. Returns/ Yields

Other rules:

  • When using Google Style directives do not type additional text between and after the directives References/See Also, Parameters, Raises and Returns/Yields.

  • End every docstring with a blank line before """. This is especially important after the Returns: directive and its indented block.

Note

The Google Style directives are only valid when used inside docstrings, but not so when used in .rst files. Keep this in mind, in case you deal with the .rst structure of the webpage.

Type hints and annotations

PorePy demands a complete provision of annotations using Python typing. This implicates that the type of every variable, attribute, argument and return value is known. Sphinx is able to interpret and create respective intra- and inter-doc linking based on the annotations.

Type hints can be given using (type):, with or without brackets, at multiple occasions. But doing so overrides the configurations of Sphinx, rendering the type annotations useless and making the documentation inconsistent.

Important

As a consequence, we abstain from including type hints in docstrings and keep them light.

Do:

def my_function(argument: type) -> ReturnType:
    """...

    Parameters:
        argument: Description of argument.

    Returns:
        Description of return value.

    """

Do NOT:

def my_function:
    """...

    Parameters:
        argument (type): Description of argument.

    Returns:
        ReturnType: Description of return value.

    """

Do:

variable: str = "A"
"""Description of variable."""

Do NOT:

variable: str = "A"
"""str: Description of variable."""

Linking/ Cross-referencing to other objects

Linking to other objects or members using

  • :obj:,

  • :class:,

  • :func:,

  • :meth and

  • :data:

to provide useful cross-referencing in the text is encouraged. This will help readers to quickly navigate through the docs.

1.2.1. Documenting variables

example_var_1: str

Variables at the module level are documented inline after declaration.

example_var_2: int

Multi-line docstrings are also valid.

You might want to use multiline docstrings when a more detailed description of the variable is required.

In every case, the closing “”” of a multiline docstring must be on a separate line after a blank line.

Do:

var: int = 1
"""Introduction line.

Line 2.

"""

Do not:

var: int = 1
"""Introduction line.

Line 2."""
var: int = 1
"""Introduction line.

Line 2.
"""
example_var_3: int

Do not start the docstring with int:, i.e. with type hints.

This will render the below Type: int, which is unnecessary due to the already present annotation after the variable name. As you can test above, clicking on int will forward you to the right documentation.

Type

int

ExampleArrayLike

This is a custom type alias, including lists and numpy’s ndarray.

1.2.2. Documenting functions

example_function_1(arg1, arg2, arg3)[source]

A short description of the functionality must fit in the first line.

Detailed description can be given next if necessary. The extended description can of course span several lines.

When natural, the description can be broken into several paragraphs that start without indentation.

The description of arguments is started using the Google Style directive Parameters:.

Note

You can find the [source] link in the upper right corner of this function’s signature and inspect the Python and rst source code of this docstring, as well as for every docstring that follows.

The return value must be documented using the Google Style directive Returns:.

Parameters
  • arg1 (int) – Description of arg1.

  • arg2 (str) –

    Description of arg2.

    If the description of a parameter spans more than one line, the lines must be indented.

    You can even use multiple paragraphs for describing a parameter. There are no restrictions in this regard.

  • arg3 (bool) – Do not include type hints in the Parameters directive. Utilize annotations in the signature instead.

Returns

Describe the return value here. Multiple lines can be used and you have to indent the lines.

Do not use type hints here as they are present in the signature using annotations.

Return type

bool

example_function_2(*args, **kwargs)[source]

This function demonstrates linking to objects inside docstrings, the usage of *args and **kwargs, and the Raises: directive.

As evident, you can link objects in-text. You can also link functions like example_function_1(). Note the different usage of :obj: and :class: for objects in third-party packages and objects in PorePy.

If *args or **kwargs are accepted, they must be listed as *args and **kwargs in the Parameters directive.

Note

This function returns None as per signature. It is the default return value of Python and as such does not need documentation. I.e., we can omit the Returns: directive. However, it is obligatory to explicitly specify the None return in the signature.

Parameters
  • *args – Arbitrary arguments. Describe adequately how they are used.

  • **kwargs

    Arbitrary keyword arguments.

    You can use rst-lists to describe admissible keywords:

    • kw_arg1: Some admissible keyword argument

    • kw_arg2: Some other admissible keyword argument

    Note that Sphinx can only interpret the rst markdown language.

Raises
  • ValueError – You can use the the Google Style directive Raises: to describe error sources you have programmed. Here for example we can raise a value error if an unexpected keyword argument was passed.

  • TypeError – You can hint multiple errors. Be aware of necessary indentations when descriptions span multiple lines.

Return type

None

example_function_3(vector, matrix, optional_arg=None, optional_list=list(), optional_vector=np.ndarray([0, 0, 0]), option_1='A', option_2=True, option_3=1e-16)[source]

This function demonstrates how to document special requirements for arguments.

Optional arguments must be type-hinted using typing.Optional.

Warning

Since the latest update for mypy, Optional should only be used with None as the default argument.

Parameters
  • vector (ndarray) –

    shape=(num_cells,)

    This vector’s special requirement is a certain shape.

    Use inline formatting to describe the requirement in the first line, followed by a blank line. Inspect the source to see how this looks like.

    After the blank line, add text to describe the argument as usual

    The required shape is (num_cells,), the number of cells. If unclear to what the number refers, explain.

    Use explicitly shape for numpy arrays to describe as precise as possible which dimensions are allowed.

    Note

    In numpy and scipy it holds (3,) != (3, 1).

  • matrix (spmatrix) –

    shape=(3, num_cells)

    This argument is a sparse matrix with 3 rows and num_cells columns.

    We insist on having whitespaces between comma and next number for readability reasons.

  • optional_arg (Optional[int]) –

    default=None

    This is an optional integer with default value None.

    We use the same format as for other requirements to display its default value.

  • optional_list (list) –

    len=num_cells

    Restrictions to built-int types like lists and tuples can be indicated using len=.

  • optional_vector (ndarray) –

    shape=(3,), default=np.ndarray([0, 0, 0])

    This is an optional vector argument, with both shape restrictions and a default argument. Combine both in the first line, followed by a blank line.

    Whatever extra requirements are added besides shape, the description default= must be put at the end.

  • option_1 (Literal['A', 'B', 'C']) –

    default='A'

    This argument has admissible values and a default value. It can only be a string 'A', 'B' or 'C'.

    For such admissible values use typing.Literal to denote them in the signature.

  • option_2 (bool) –

    default=True

    This is an boolean argument with the default value True.

  • option_3 (float) –

    [0, 1], default=1e-16

    This is an float argument, with values restricted between 0 and 1. The default value is set to 1e-16.

Raises

ValueError – If option is not in {'A', 'B', 'C'}.

Returns

Describe the return value here.

You can also describe its dependence on the value of option here, or alternatively in the extended description at the beginning of the docstring.

Exemplarily we can write here:

  • If option is 'A', return …

  • If option is 'B', return …

Return type

spmatrix

example_function_4(vector, matrix)[source]

This function demonstrates how to document multiple return values in the Returns: directive.

Parameters
Returns

Every returned object needs a description. We can use the same structure as for the parameters - indented block per returned object or value (see source code).

Sphinx is configured such that it includes the return type in a separate field below. You can optionally include additional cross-referencing using :obj and :class:.

spmatrix: (shape=(3, nd))

Though technically the tuple is a single object, it often is of importance what the tuple contains. If the returned object has noteworthy characteristic, explain them in the first line after the bullet header, followed by a blank line (similar to arguments).

ndarray:

Use the definition list structure with indented blocks to describe the content individually. This makes of course only sense if the tuple is of fixed length and/or contains objects of various types.

Return type

tuple[scipy.sparse._base.spmatrix, numpy.ndarray]

example_function_5(array_like, ndarray_like, dtype_like)[source]

This function demonstrates type annotations using custom type aliases, including those from third-party packages.

For Sphinx to display type aliases without parsing their complete expression, a configuration is needed which maps the type alias to its source.

When encountering type aliases, which are not already covered by the configuration of PorePy’s docs, please contact the core developers.

Warning

There is an open issue on Sphinx’ side, which prevents type aliases from being properly linked to their source.

If you discover your type annotations not creating a hyperlink, don’t threat. This will come with future Sphinx updates.

Parameters
  • array_like (ArrayLike) – An argument of type ArrayLike.

  • ndarray_like (NDArray) – An argument of type NDArray.

  • dtype_like (DTypeLike) – An argument of type DTypeLike.

Returns

A value of a custom array type ExampleArrayLike.

Return type

ExampleArrayLike

example_generator(n)[source]

When documenting a generator, use the Yields: directive instead of Returns:.

Their usage is equivalent, indicating only that this is a generator object, not a regular function.

Example

Here we demonstrate the usage of example_generator using the Example: directive:

>>> print([i for i in example_generator(4)])
[0, 1, 2, 3]
Parameters

n (int) – Some upper integer limit.

Yields

numbers until n is reached.

Return type

Generator[int, None, None]

1.2.3. Documenting classes

When documenting classes, three aspects have to be considered.

Documenting Constructor Arguments

In PorePy, the constructor description is to be included in the class-docstring, not in the __init__-docstring! There must be no ``__init__``-docstring at all!

The class-docstring is written directly below the declaration of the class

class ExampleClass:
    """ Class-docstring """

    def __init(self):
    """ __init__ - docstring """

whereas the __init__-docstring is found below the constructor declaration.

Inside class docstrings, you can sue the same directives as for functions. The same rules apply.

Documentation of attributes

Attributes must be documented using docstrings. This holds for instance-level attributes set in the constructor, as well as for class-level attributes. PorePy’s sphinx build is configured such that it will render those docstrings the same way as method docstrings.

Though only public attributes are included by Sphinx’ configuration, private and name-mangled attributes must be documented as well.

In any case, use type annotations in the Python code and do not use type hints in the docstring. This will avoid a second, redundant display of the type (similar for argument and return type hints).

This guideline holds for instance-level attributes, as well as for class-level attributes.

Though attributes can be documented in the class docstring using the directive Attributes:, abstain from doing so. Here the type annotation cannot be exploited in all cases and additional type hints would be necessary in the docstrings.

The following example shows why we do not use Attributes:

"""
Attributes:
    class_attribute_1: This is a class-level attribute documented in the
        class docstring.
        The annotation would be exploited in this case.
    attribute_1 (int): This is an instance-level attribute documented in the
        class docstring.
        Here we would have to use additional type hints because the annotation inside
        the constructor is not exploited by Sphinx.

        If you document any attribute here AND in a custom docstring, Sphinx will raise an
        error.
"""

Documenting methods

  • All rules introduced in section Documenting functions for regular functions, hold for methods, class methods and static methods as well, independent of whether they are public, private or special.

  • Never document the self argument of a method, or the cls argument of a class method.

  • By default, only public methods are included in the documentation. Nevertheless, private methods and special methods need to be documented properly as well. If the developer wants them included in the compiled documentation, he must provide respective options to the audodoc directives, later when integrating his documentation.

Sphinx groups the documentation of attributes, properties, methods, class methods and static methods and sorts them according to the configuration.

Example class documentation

class ExampleClass(arg1, arg2)[source]

Bases: object

A quick purpose of the class is to be provided in the first line.

After the first line, a broader, multi-line description of the class can follow. It can contain multiple paragraphs as well as elements like code snippets.

Example: directives are encouraged to demonstrate the usage of a class.

Note: directives can be used the same way as for functions.

Example

Using the Example: directive, you can provide simplified python syntax using >>> on how to instantiate the class for example:

>>> import example_docstrings as ed
>>> A = ed.ExampleClass()
>>> print(type(A))
<class 'example_docstrings.ExampleClass'>

Use the References: and See Also: directives to provide links to sources or inspirations for your implementation.

For readability reasons, we impose the same rules on the order of directives as for functions. Meaning references/ see also, followed by parameters, raises and return/yield must be placed at the end with no additional text in between.

References

  1. Basic Google Style.

Parameters
  • arg1 (int) – First argument.

  • arg2 (str) – Second argument.

classmethod example_class_method()[source]

This is the docstring of a class method.

Note that the self argument is replaced by the cls argument, but not included in the docs.

Return type

None

example_method(arg)[source]

A short description of the method must fit in the first line.

A more elaborate description can follow afterwards.

Parameters

arg (bool) – The first argument (even though self is technically the first one).

Returns

True.

Return type

bool

static example_static_method()[source]

This is the docstring of a static method.

Note the absence of self or cls in the signature.

Return type

None

attribute_1: int

This is how to properly document an instance-level attribute in the constructor.

If an attribute has certain properties, like specific shape for arrays or a fixed length for lists, document them in the attribute description.

attribute_2: str

This is an instance-level attribute with a redundant field ‘Type:’ caused by type hints in the docstring.

The type should only be given by its annotation. Do not use type hints in docstrings (cannot repeat often enough).

Type

str

attribute_3: dict[str, int]

Some attributes are calculated during construction, possibly in a separate method.

Document nevertheless what the attribute contains and what it is used for etc., but leave the documentation of how it is calculated to the separate method.

attribute_4: ndarray

Some attributes are only available after a specific method is called, without having an initial value.

Document such attributes using type annotations without instantiation as seen in the example below.

class ExampleClass:
    """ Class-docstring """

    def __init(self):

        self.attribute_4: np.ndarray
        """Docstring below type annotation without setting a concrete
        value."""
class_attribute_1: int = 1

This is how to properly document a class-level attribute.

The value 1 is shown in the docs, because it is static upon class type creation and Sphinx realizes that.

class_attribute_2: str = 'A'

This is a class attribute with a redundant field ‘Type:’ caused by type hints in the docstring.

The type should only be given by its annotation. Do not use type hints in docstrings.

Type

str

property readonly_property: str

Document properties only in their getter-methods.

property readwrite_property: str

Properties with both a getter and setter should only be documented in their getter method.

If the setter method contains notable behavior, it should be mentioned here in the getter documentation.

class ChildClass(new_arg, arg1, arg2='A')[source]

Bases: ExampleClass

This is a derived class from ExampleClass.

Sphinx is configured such that all base classes are shown below the class name.

The child classes do not contain the documentation of inherited members.

If a method or attribute is overridden, for example due to the parent method being abstract or the child method doing something additionally, the developer must provide a new docstring in the overridden class.

Exemplarily, this child class overrides ExampleClass.attribute_2 and ExampleClass.example_method(), providing docstrings that say ‘Something changed’.

If the constructor signature changes, document the arguments that changed or are new.

Parameters
  • new_arg (int) – This is a new argument for the child class.

  • arg1 (int) – See ExampleClass.

  • arg2 (str) –

    default='A'

    The second argument defaults to 'A' now.

example_method(arg)[source]

Something changed.

Describe what is being done, if and when a super-call to the parent method is performed and if the return value changes depending on the new computations.

Parameters

arg (bool) – See ExampleClass.

Returns

Returns something dependent the parent method result.

Return type

bool

attribute_2: str

Something changed. Attribute 2 is now set to something else.

1.2.4. Module-level documentation

Module level documentation can be achieved by writing a docstring at the very top of a file. Subpackages i.e., folders containing an __init__ file and multiple modules, have their docstring at the top of the __init__ file. These docstrings usually describes the purpose and the contents of the module/package and can be included in the compiled documentation.

They can be used as an introduction to the content. In Fact this page’s introduction is written in the module-docstring and imported into the compiled documentation. It is then supplemented with additional content like the sections on Docstring-Basics or Directives.

The content and design of module-level docstrings should be discussed with the core developers.