How to Document Your Code
Docstrings
In Python, a string at the top of a module, class or function is called adocstring.For example:
"""This docstring describes the purpose of this module."""classC:"""This docstring describes the purpose of this class."""defm(self):"""This docstring describes the purpose of this method."""
Pydoctor also supportsattribute docstrings:
CONST=123"""This docstring describes a module level constant."""classC:cvar=None"""This docstring describes a class variable."""def__init__(self):self.ivar=[]"""This docstring describes an instance variable."""
Attribute docstrings are not part of the Python language itself (PEP 224 was rejected), so these docstrings are not available at runtime.
For long docstrings, start with a short summary, followed by an empty line:
deff():"""This line is used as the summary. More detail about the workings of this function can be added here. They will be displayed in the documentation of the function itself but omitted from the summary table. """
Since docstrings are Python strings, escape sequences such as\n will be parsed as if the corresponding character—for example a newline—occurred at the position of the escape sequence in the source code.
To have the text\n in a docstring at runtime and in the generated documentation, you either have escape it twice in the source:\\n or use ther prefix for a raw string literal.The following example shows the raw string approach:
defiter_lines(stream):r"""Iterate through the lines in the given text stream, with newline characters (\n) removed. """forlineinstream:yieldline.rstrip('\n')
Further reading:
Docstring assignments
Simple assignments to the__doc__ attribute of a class or function are recognized by pydoctor:
classCustomException(Exception):__doc__=MESSAGE="Oops!"
Non-trivial assignments to__doc__ are not supported. A warning will be logged by pydoctor as a reminder that the assignment will not be part of the generated API documentation:
ifLOUD_DOCS:f.__doc__=f.__doc__.upper()
Assignments to__doc__ inside functions are ignored by pydoctor. This can be used to avoid warnings when you want to modify runtime docstrings without affecting the generated API documentation:
defmark_unavailable(func):func.__doc__=func.__doc__+'\n\nUnavailable on this system.'ifnotis_supported('thing'):mark_unavailable(do_the_thing)
Augmented assignments like+= are currently ignored as well, but that is an implementation limitation rather than a design decision, so this might change in the future.
Doc-comments
Documentation can also be put into a comment with special formatting, using a#: to start the comment instead of just#.Comments need to be either on their own before the definition, OR immediately after the assignment on the same line.The latter form is restricted to one line only.:
var=True#: Doc comment for module attribute.classFoo:#: Doc comment for class attribute Foo.bar.#: It can have multiple lines.#: @type: intbar=1flox=1.5#: Doc comment for Foo.flox. One line only.def__init__(self):#: Doc comment for instance attribute qux.self.qux=3
Constants
The value of a constant is rendered with syntax highlighting.Seemodule demonstrating the constant values rendering.
FollowingPEP8, any variable defined with all upper case name will be considered as a constant.Additionally, starting with Python 3.8, one can use thetyping.Final qualifier to declare a constant.
For instance, these variables will be recognized as constants:
fromtypingimportFinalX=3.14y:Final=['a','b']
In Python 3.6 and 3.7, you can use the qualifier present in thetyping_extensions instead oftyping.Final:
fromtyping_extensionsimportFinalz:Final='relative/path'
Fields
Pydoctor supports most of the common fields usable in Sphinx, and some others.
Epytext fields are written with arobase, like@field: or@fieldarg:.ReStructuredText fields are written with colons, like:field: or:fieldarg:.
Here are the supported fields (written with ReStructuredText format, but same fields are supported with Epytext):
:cvarfoo:, document a class variable namedfoo. Applicable in the context of the docstring of a class.
:ivarfoo:, document a instance variable namedfoo. Applicable in the context of the docstring of a class.
:varfoo:, document a variable namedfoo. Applicable in the context of the docstring of a module or class.If used in the context of a class, behaves just like@ivar:.
:note:, add a note section.
:parambar:(synonym:@argbar:), document a function’s (or method’s) parameter namedbar.Applicable in the context of the docstring of a function of method.
:keyword:, document a function’s (or method’s) keyword parameter (**kwargs).
:typebar:C{list}, document the type of an argument/keyword or variable (barin this example), depending on the context.
:return:(synonym:@returns:), document the return type of a function (or method).
:rtype:(synonym:@returntype:), document the type of the return value of a function (or method).
:yield:(synonym:@yields:), document the values yielded by a generator function (or method).
:ytype:(synonym:@yieldtype:), document the type of the values yielded by a generator function (or method).
:raiseValueError:(synonym:@raisesValueError:), document the potential exception a function (or method) can raise.
:warnRuntimeWarning:(synonym:@warnsValueError:), document the potential warning a function (or method) can trigger.
:see:(synonym:@seealso:), add a see also section.
:since:, document the date and/or version since a component is present in the API.
:author:, document the author of a component, generally a module.
Note
Currently, any other fields will be considered “unknown” and will be flagged as such.See“fields” issuesfor discussions and improvements.
Note
Unlike Sphinx,vartype andkwtype are not recognized as valid fields, we simply usetype everywhere.
Type fields
Type fields, namelytype,rtype andytype, can be interpreted, such that, instead of being just a regular text field,types can be linked automatically.For reStructuredText and Epytext documentation format, enable this behaviour with the option:
--process-types
The type auto-linking is always enabled for Numpy and Google style documentation formats.
Like in Sphinx, regular types and container types such as lists and dictionaries can be linked automatically:
:typepriority:int:typepriorities:list[int]:typemapping:dict(str,int):typepoint:tuple[float,float]
Natural language types can be linked automatically if separated by the words “or”, “and”, “to”, “of” or the comma:
:rtype:floatorstr:returntype:listofstrorlist[int]:ytype:tupleofstr,intandfloat:yieldtype:mappingofstrtoint
Additionally, it’s still possible to include regular text description inside a type specification:
:rtype:aresultthatneedsalongertextdescriptionorstr:rtype:tupleofaresultthatneedsalongertextdescriptionandstr
Some special keywords will be recognized: “optional” and “default”:
:typevalue:list[float],optional:typevalue:int,default:-1:typevalue:dict(str,int),default:sameasdefault_dict
Note
Literals caracters - numbers and strings within quotes - will be automatically rendered like docutils literals.
Note
It’s not currently possible to combine parameter type and description inside the sameparam field, see issue#267.
Type annotations
Type annotations in your source code will be included in the API documentation that pydoctor generates.For example:
colors:dict[str,int]={'red':0xFF0000,'green':0x00FF00,'blue':0x0000FF}definverse(name:str)->int:returncolors[name]^0xFFFFFF
If your project still supports Python versions prior to 3.6, you can also use type comments:
fromtypingimportOptionalfavorite_color=None# type: Optional[str]
However, the ability to extract type comments only exists in the parser of Python 3.8 and later, so make sure you run pydoctor using a recent Python version, or the type comments will be ignored.
There is basic type inference support for variables/constants that are assigned literal values.Unlike for example mypy, pydoctor cannot infer the type for computed values:
FIBONACCI=[1,1,2,3,5,8,13]# pydoctor will automatically determine the type: list[int]SQUARES=[n**2forninrange(10)]# pydoctor needs an annotation to document this type
Type variables and type aliases will be recognized as such and their value will be colorized in HTML:
fromtypingimportCallable,Tuple,TypeAlias,TypeVarT=TypeVar('T')# a type variableParser=Callable[[str],Tuple[int,bytes,bytes]]# a type alias
Note
About name resolving in annotations:pydoctor checks for top-level names first before checking for other names,this is true only for annotations.
This behaviour matches pyright’s when PEP-563 is enabled(module starts withfrom__future__importannotations).
When there is an ambiguous annotation, a warning can be printed if option-v is supplied.
Further reading:
Properties
A method with a decoration ending inproperty orProperty will be included in the generated API documentation as an attribute rather than a method:
classKnight:@propertydefname(self):returnself._name@abc.abstractpropertydefage(self):raiseNotImplementedError@customPropertydefquest(self):returnf'Find the{self._object}'
All you have to do for pydoctor to recognize your custom properties is stick to this naming convention.
Usingattrs
If you use theattrs library to define attributes on your classes, you can use inline docstrings combined with type annotations to provide pydoctor with all the information it needs to document those attributes:
importattr@attr.s(auto_attribs=True)classSomeClass:a_number:int=42"""One number."""list_of_numbers:list[int]"""Multiple numbers."""
If you are using explicitattr.ib definitions instead ofauto_attribs, pydoctor will try to infer the type of the attribute from the default value, but will need help in the form of type annotations or comments for collections and custom types:
fromtypingimportListimportattr@attr.sclassSomeClass:a_number=attr.ib(default=42)"""One number."""list_of_numbers=attr.ib(factory=list)# type: List[int]"""Multiple numbers."""
Private API
Modules, classes and functions of which the name starts with an underscore are consideredprivate. These will not be shown by default, but there is a button in the generated documentation to reveal them. An exception to this rule isdunders: names that start and end with double underscores, like__str__ and__eq__, which are always considered public:
class_Private:"""This class won't be shown unless explicitly revealed."""classPublic:"""This class is public, but some of its methods are private."""defpublic(self):"""This is a public method."""def_private(self):"""For internal use only."""def__eq__(self,other):"""Is this object equal to 'other'? This method is public. """
Note
Pydoctor actually supports 3 types of privacy: public, private and hidden.SeeOverride objects privacy for more informations.
Re-exporting
If your project is a library or framework of significant size, you might want to split the implementation over multiple private modules while keeping the public API importable from a single module. This is supported using pydoctor’s re-export feature.
A documented element which is defined in one (typically private) module can be imported into another module and re-exported by naming it in the__all__ special variable. Doing so will move its documentation to the module from where it was re-exported, which is where users of your project will be importing it from.
In the following example, the documentation ofMyClass is written in themy_project.core._impl module, which is imported into the top-level__init__.py and then re-exported by including"MyClass" in the value of__all__. As a result, the documentation forMyClass can be read in the documentation of the top-levelmy_project package:
├── README.rst├── my_project│ ├── __init__.py <-- Re-exports my_project.core._impl.MyClass│ ├── core as my_project.MyClass│ │ ├── __init__.py│ │ ├── _impl.py <-- Defines and documents MyClass
The content ofmy_project/__init__.py includes:
from.core._implimportMyClass__all__=("MyClass",)
Branch priorities
When pydoctor deals with try/except/else or if/else block, it makes sure that the names defined inthe main flow has precedence over the definitions inexcept handlers orelse blocks.
Meaning that in the context of the code below,ssl would resolve totwisted.internet.ssl:
try:# main flowfromtwisted.internetimportsslas_sslexceptImportError:# exceptional flowssl=None# ignored since 'ssl' is defined in the main flow below.var=True# not ignored since 'var' is not defined anywhere else.else:# main flowssl=_ssl
Similarly, in the context of the code below, theCapSys protocol under theTYPE_CHECKING block will bedocumented and the runtime version will be ignored.
fromtypingimportTYPE_CHECKINGifTYPE_CHECKING:# main flowfromtypingimportProtocolclassCapSys(Protocol):defreadouterr()->Any:...else:# secondary flowclassCapSys(object):# ignored since 'CapSys' is defined in the main flow above....
But sometimes pydoctor can be better off analysing theTYPE_CHECKING blocks and shouldstick to the runtime version of the code instead.For these case, you might want to inverse the condition of if statement:
ifnotTYPE_CHECKING:# main flowfrom._implementationimportThingelse:# secondary flowfrom._typingimportThing# ignored since 'Thing' is defined in the main flow above.