4. Examples¶
This section covers cases that are not necessarily contained in the reference documentation, such as
factories
properties
decorators
coroutines
classes within classes
constants in classes
type aliases in classes
4.1. Factories¶
This section illustrates the use of the Factory section. In this context,
Factory does not refer to the well-known design pattern, but to the
practical notion of a function that creates an instance of a class and
encapsulates the constructor.
In Python, several patterns exist for defining factory functions. In the sample program, we focus on the following:
module-level functions with explicit signatures,
class methods with explicit signatures,
functions or class methods using runtime polymorphism.
The use of @singledispatch is intentionally omitted.
The inventory of factory functions in this example is intentionally redundant for demonstration purposes. In practice, the choice among these patterns depends on personal style and design preferences.
The class Vec2d represents two-dimensional vectors.
We would like to create a vector in the following ways:
without arguments, yielding the zero vector,
by passing two
float-valued componentsxandy,by passing another two-dimensional vector in order to create a copy.
The corresponding implementation is shown below:
from __future__ import annotations
from typing import Any
class Vec2d:
def __init__(self,x: float = 0.0,y: float = 0.0):
self._c = [x,y]
@classmethod
def from_zero(cls) -> Vec2d:
return Vec2d()
@classmethod
def from_comp(cls,x: float,y: float) -> Vec2d:
return Vec2d(x,y)
@classmethod
def from_vec2d(cls,v: Vec2d) -> Vec2d:
return Vec2d(v._c[0],v._c[1])
@classmethod
def gen(cls,*arg: Any) -> Vec2d:
return vec2d(*arg)
def __str__(self) -> str:
return str(self._c)
def make_vec2d_from_zero() -> Vec2d:
return Vec2d()
def make_vec2d_from_comp(x: float,y: float) -> Vec2d:
return Vec2d(x,y)
def make_vec2d_from_vec2d(v: Vec2d) -> Vec2d:
return Vec2d(v._c[0],v._c[1])
def vec2d(*arg: Any) -> Vec2d:
match arg:
case ():
return Vec2d()
case (float() as x, float() as y):
return Vec2d(x, y)
case (Vec2d() as other,):
return Vec2d(other._c[0], other._c[1])
case _:
raise ValueError(f"Unsupported input: {arg}")
if __name__ == "__main__":
print(Vec2d.gen())
print(Vec2d.gen(2.0,3.0))
print(Vec2d.gen(Vec2d.gen(5.0,7.0)))
print(vec2d())
print(vec2d(2.0,3.0))
print(vec2d(vec2d(5.0,7.0)))
There are eight factory functions, all of which form part of the class API.
The Waterloo docstring format captures this relationship by means of a
Factory section, as shown in the following excerpt:
class Vec2d:
"""
Preamble:
profile:
class
normative_sections:
Contract, Factory, Public_methods
Contract:
general:
|Must| represent a two-dimensional vector with |type|`float`-valued components.
constructor:
Do not use directly; use the factory functions instead.
Factory:
Vec2d.from_zero:
Null vector
Vec2d.from_comp:
Vector from components (x, y)
Vec2d.from_vec2d:
Vector by copy
Vec2d.gen:
All of the above via runtime polymorphism
make_vec2d_from_zero:
Null vector
make_vec2d_from_comp:
Vector from components (x, y)
make_vec2d_from_vec2d:
Vector by copy
vec2d:
All of the above via runtime polymorphism
Public_methods:
from_zero, from_comp, from_vec2d,
gen
"""
Validation ensures that each factory entry listed in this section is resolvable. In the generated HTML artifact, the labels of the factory entries are rendered as links to the documentation boxes of the referenced objects.
|
|
|
|
|
|
|
|
Class |
Vec2d |
|---|
Public Methods in class Vec2d
|
@classmethod
test_docitem_factory_vec2d.Vec2d.gen(
*arg: Any
) -> Vec2d
|
|
|
|
|
|
|
|
|
|
|
Method |
Vec2d.gen |
|---|
|
test_docitem_factory_vec2d.vec2d(
*arg: Any
) -> Vec2d
|
|
|
|
|
|
|
|
|
|
|
Function |
vec2d |
|---|
4.2. Properties¶
The following example shows how properties can be documented.
A property is conceptually a member variable of a class and therefore
it is listed under Public_variables.
Internally, the accessor methods fget, fset,
and fdel are extracted from the property object and rendered
as regular methods. Their docstrings provide the normative specification
of the property’s behavior, while the property itself defines the public API surface.
|
|
|
|
|
|
Class |
X |
|---|
|
@property
test_docitem_method_property.X.value(
) -> int
|
|
|
|
|
|
|
|
|
|
<empty> |
Method |
X.value |
|---|
|
@value.setter
test_docitem_method_property.X.value(
v: int
) -> None
|
|
|
|
|
|
|
|
|
|
<empty> |
Method |
X.value |
|---|
4.3. Decorators¶
In this section we test the graphic representation of function and method signatures
in presence of decorators, such as @classmethod and @staticmethod.
Consider the following test snippet:
#!/usr/bin/env python3
from __future__ import annotations
from types import ModuleType
from typing import Any,Callable,NoReturn,cast
import functools
from abc import ABC, abstractmethod
class B:
@abstractmethod
def f_abstractmethod(self) -> NoReturn:
r"""
Preamble:
profile:
function
normative_sections:
Contract, Parameters, Returns, Raises
Contract:
general:
|Must| always raise an exception because\
it is an abstract method.
Parameters:
Returns:
Does not return.
Raises:
NotImplementedError:
|Must| always raise. Method |must| be\
implemented by derived class.
"""
raise NotImplementedError
class X:
def f_method(self,q: int) -> None:
print(f"{self.__class__.__name__}.f_method")
@classmethod
def f_classmethod(cls,q: float) -> None:
print(f"{cls.__name__}.f_classmethod")
@staticmethod
def f_staticmethod(q: bool) -> None:
"""
Preamble:
profile:
function
normative_sections:
Contract, Parameters, Returns, Raises
Contract:
general:
|Must| print its name
Parameters:
q:
A dummy parameter.
Returns:
|None|
Raises:
"""
print("f_staticmethod")
def trace(fn: Callable[...,Any]) -> Any:
@functools.wraps(fn)
def wrapper(*args, **kwargs): #type: ignore[no-untyped-def]
print("call", fn.__name__)
return fn(*args, **kwargs)
return wrapper
@trace
@functools.lru_cache(maxsize=128)
def fib(n: int) -> int:
"""
Preamble:
profile:
function
normative_sections:
Contract, Parameters, Returns, Raises
Contract:
general:
|Must| compute the |var|`n`-th Fibonacci number.
|Must| cache intermediate results.
Parameters:
n:
The |var|`n` in "compute the |var|`n`-th Fibonacci number".
Returns:
The Fibonacci number
Raises:
BaseException:
|May| propagate from |mod| functools.
"""
if n < 2:
return n
return cast(int,fib(n - 1) + fib(n - 2))
if __name__ == "__main__":
X().f_method(123)
X.f_classmethod(4.56)
X.f_staticmethod(True)
In the document source we have:
.. wtrl_method_signature:: X.f_method
.. wtrl_method_signature_block:: X.f_method
.. wtrl_method_signature:: X.f_classmethod
.. wtrl_method_signature_block:: X.f_classmethod
.. wtrl_method_signature:: X.f_staticmethod
.. wtrl_method_signature_block:: X.f_staticmethod
The output for methods without decorator is:
f_method(q: int) -> NoneFor class method we have:
@classmethod f_classmethod(q: float) -> NoneAnd for static methods:
@staticmethod f_staticmethod(q: bool) -> NoneAn example with more than one decorator. The inline representation is not really helpful, and we recommend to switch to the block representation.
@trace @functools.lru_cache(maxsize=128) fib(n: int) -> int4.4. Coroutines¶
Let’s take a look at how coroutines are rendered. The important keyword is
async. The inline signature looks like this:
In the block representation, we get:
Apart from that, coroutines behave like other methods and functions in terms of documentation. The code segment
async def make_coffee() -> str:
"""
Preamble:
profile:
function
normative_sections:
Contract, Parameters, Returns, Raises
Contract:
general:
|Must| represent a non-blocking asynchronous task.
|Must| take 3 seconds.
Parameters:
Returns:
|Must| return a string telling what has been prepared.
Raises:
asyncio.CancelledError:
|May| throw if the task running the coroutine is cancelled from outside.
"""
print(" [Coffee] Coffee machine started...")
await asyncio.sleep(3)
print(" [Coffee] Coffee is ready!")
return "Hot coffee"
is rendered as:
|
async test_docitem_coroutine.make_coffee(
) -> str
|
|
|
|
|
|
|
|
|
|
|
Function |
make_coffee |
|---|
For generators we get:
async get_marmalade() -> AsyncGenerator[str, None]and
from source code:
async def get_marmalade() -> AsyncGenerator[str,None]:
"""
Preamble:
profile:
function
normative_sections:
Contract, Parameters, Returns, Raises
Contract:
general:
|Must| represent a non-blocking asynchronous task.
|Must| serve cherry marmalade after a second.
|Must| serve strawberry marmalade after two seconds.
Parameters:
Returns:
|Must| produce an asynchronous generator yielding strings of marmalade types.
Raises:
asyncio.CancelledError:
|May| be raised if the task is cancelled during an await point.
"""
await asyncio.sleep(1)
yield "Cherry"
await asyncio.sleep(1)
yield "Strawberry"
4.5. Definitions¶
A Definitions section can be provided for modules, classes, and callables.
Each subsection label consists of a comma-separated list of identifiers.
The first identifier denotes the Term, while any following identifiers
denote Variations of that term.
The purpose of allowing multiple identifiers is to capture morpho-syntactic
and orthographic variations of the same term. In the following example,
we define the term sensitive, as well as the capitalized form
Sensitive (for sentence-initial usage) and the nominalized form
Sensitivity.
r"""
Preamble:
profile:
module
normative_sections:
Contract, Definitions
Contract:
general:
|Must| provide a |label|`Definitions` section\
for demonstration purposes.
Definitions:
sensitive, Sensitive, Sensitivity:
* A widget is |dfn|`sensitive` if and only if it responds to user input.
* |dfn|`Sensitivity` can be set or unset via the Python API.
"""
The result is
|
|
|
|
|
|
Module |
test_docitem_definitions |
|---|
4.6. Inherited Definitions¶
The following example demonstrates how definition items can be inherited from module level to class or function level in order to avoid unnecessary repetition.
r"""
Preamble:
profile:
module
normative_sections:
Contract, Definitions
Contract:
general:
|Must| provide a |label|`Definitions` section for demonstration purposes.
Definitions:
Identifier:
A string that matches the regular expression |value|`[a-zA-Z_][a-zA-Z0-9_]*`
Qualified_Identifier:
A |term|`Qualified_Identifier` is a string formed by concatenating |term|`Identifier` values\
separated by a dot.
Term_Z:
|term|`Term_Z` is another term inherited by objects in this module.
"""
class X:
r"""
Preamble:
profile:
class
normative_sections:
Contract, Definitions
Contract:
general:
|Must| demonstrate inherited definition items.
constructor:
default
Definitions:
Private_Identifier:
A |term|`Private_Identifier` is an |term|`Identifier` that starts with a double underscore.
_inherit:
Identifier, Term_Z
"""
def spam(self) -> None:
r"""
Preamble:
profile:
method
normative_sections:
Contract, Definitions, Parameters, Returns, Raises
Contract:
general:
|Must| demonstrate inherited definition items.
Definitions:
_inherit:
Identifier,Qualified_Identifier
Anchor:
An |term|`Anchor` is a special string computed from a |term|`Qualified_Identifier`
in the following way: 1. The |term|`Qualified_Identifier` is split into segments,
where each segment is an |term|`Identifier`. 2. To each segment the string
|value|`"<n>:"` is prepended where |var|`<n>` stands for the length of the segment.
3. The results are concatenated with |value|`"-"` as separator.
Example: The |term|`Anchor` of |value|`AAA.BB.C` is |value|`3:AAA-2:BB-1:C`.
Parameters:
Returns:
|None|
Raises:
"""
The module docstring is rendered as shown below. It contains commonly used
definitions for Identifier and Qualified_Identifier.
By means of the subsection Definitions._inherited, each class or
function object defined in the module can reference these definitions in its
free-text sections.
|
|
|
|
|
|
Module |
test_docitem_definitions_inherited |
|---|
The class docstring inherits the terms Identifier and
Term_Z, and defines a new term
Private_Identifier based on Identifier.
Referencing inherited terms using |term| is valid and
results in semantic highlighting in the generated HTML artifact.
|
|
|
|
|
|
Class |
X |
|---|
The method docstring inherits the terms
Identifier and Qualified_Identifier
and uses them to define a new term Anchor.
|
test_docitem_definitions_inherited.X.spam(
) -> None
|
|
|
|
|
|
|
|
|
|
|
|
<empty> |
Method |
X.spam |
|---|
4.7. A display cabinet of sections¶
The following example shows the docstring of one of Waterloo’s built-in functions, get_num_indent.
The function has the following signature:
and the docstring reads:
r"""
Preamble:
profile:
function
normative_sections:
Definitions, Contract, Parameters, Returns, Raises
status:
stable
Definitions:
TAB:
A scheme that demands indentation by means of an integer number\
of tab characters (ASCII |value|`0x09`).\
For this scheme, |var|`INDENT_UNIT` is a single tab character.
SPC4:
A scheme that demands indentation by means of an integer multiple\
of four space characters (ASCII |value|`0x20`).
For this scheme, |var|`INDENT_UNIT` consists of four space characters.
Contract:
general:
|Must| accept a single line string and an indentation scheme.
|Must| count the number of leading indentations of the input according\
to the scheme passed.
|Must| accept an empty string.
Parameters:
tr:
Tracer for better error messages
line:
A single line string.
indent_scheme:
A symbolic value representing one of the two possible indentation\
schemes |term|`TAB` or |term|`SPC4`.
Returns:
|Must| return the number of indentations found at the beginning of the string\
in units as decribed by the indentation scheme passed.
Raises:
RuntimeError:
|Must| raise if prefix contains a mix not representable\
as |var|`n` repetitions of |var|`INDENT_UNIT`.
|Must| raise if the leading white space characters (tabs or four spaces)\
at the beginning of the line\
cannot be described by the indentation scheme passed.
"""
Note that we have a section here called Definitions, which significantly
reduces the load on the normative sections Parameters and Raises.
Definitions is normative because we list it under Preamble.normative_sections,
although it does not contain a normativity keyword. Yet the definitions given there are binding
for the entire documentation box rendered from this docstring.
4.8. Subections requires, ensures, and invariants¶
This example illustrates the use of the subsections requires,
ensures, and invariants.
These labels follow the classic Design-by-Contract distinction between
preconditions, postconditions, and invariants:
requiresspecifies obligations of the caller. If a requirement is violated, the function’s behaviour is undefined within the normative model.ensuresspecifies guarantees provided by the function upon successful completion, assuming all requirements were met.invariantsspecifies properties that must hold independently of a particular call. They typically express structural, semantic, or algebraic constraints that characterize the function itself (e.g. idempotence, determinism, monotonicity).
Syntactically, these subsections are not treated differently from
general. Their purpose is semantic refinement: they allow
normative statements to be grouped according to their logical role within
the contract. This separation improves clarity for both human readers and automated tooling,
as normative statements can be classified according to their contractual role.
|
test_docitem_invariants_requires_ensures.normalize_identifier(
name: str
) -> str
|
|
|
|
|
|
|
|
|
|
<empty> |
Function |
normalize_identifier |
|---|
4.9. Class within class¶
The following example of a class Y nested in another class X,
both equipped with a method, can be rendered by a single command
.. wtrl_autodoc_class_full:: X
This directive recusrsively iterates over the class hierarchy and ramifies to the other member, like e.g. methods. The source code is:
class X:
"""
Preamble:
profile:
class
normative_sections:
Contract, Public_methods, Public_classes
Contract:
general:
|Must| provide a method for writing a greeting message.
constructor:
|Must| be default-constructible
Public_methods:
greeting
Method_overview:
greeting:
A simple test method.
Public_classes:
Y
Class_overview:
Y:
A nested class for testing purposes.
"""
def greeting(self):
"""
Preamble:
profile:
method
normative_sections:
Contract, Parameters, Returns,Raises
Contract:
general:
|Must| render a greeting message to ``stdout``.
Parameters:
Returns:
|None|
Raises:
"""
print("Hello world!")
class Y:
"""
Preamble:
profile:
class
normative_sections:
Contract, Public_methods
Contract:
general:
A class, nested in class :wtrl_type:`X`, |must| do nothing.
constructor:
|Must| be default-constructible
Public_methods:
greeting_but_in_Y
"""
def greeting_but_in_Y(self):
"""
Preamble:
profile:
method
normative_sections:
Contract, Parameters, Returns,Raises
Contract:
general:
|Must| render a greeting message to ``stdout``.
Parameters:
Returns:
|None|
Raises:
"""
print("Hello world from Y!")
And the result is:
|
|
|
|
|
|
|
|
|
|
|
|
Class |
X |
|---|
Nested classes in X
|
|
|
|
|
|
Class |
X.Y |
|---|
Public Methods in class X.Y
|
test_docitem_class_class.X.Y.greeting_but_in_Y(
) -> Any
|
|
|
|
|
|
|
|
|
|
<empty> |
Method |
X.Y.greeting_but_in_Y |
|---|
Public Methods in class X
|
test_docitem_class_class.X.greeting(
) -> Any
|
|
|
|
|
|
|
|
|
|
<empty> |
Method |
X.greeting |
|---|
4.10. Formatting tests¶
A simple test for demonstrating the pipe operator convention in the output layer.
|
test_docitem_function_pipe.f(
) -> None
|
|
|
|
|
|
A paragraph starting in a physical line continued in the next physical line. 1. Item 1 2. Item 2 |
|
|
|
A paragraph starting in a physical line continued in the next physical line. Another paragraph |
|
<empty> |
|
A note A paragraph starting in a physical line continued in the next physical line. Another paragraph A second note A paragraph starting in a physical line continued in the next physical line. Another paragraph |
Function |
f |
|---|