Python docs
A modern, mobile-friendly Sphinx-alike Python documentation generator with a first-class search functionality and an ability to provide Python stubs for IDE autocompletion and type checking. Generated by inspecting Python modules and using either embedded docstrings or external reST files to populate the documentation.
One of the design goals is providing a similar user experience to the Doxygen documentation theme.
Basic usage
The base is contained in a single Python script and related style/template
files, for advanced features such as math rendering it’ll make use of internals
of some m.css plugins. Clone
the m.css GitHub repository and look
into the documentation/
directory:
git clone https://github.com/mosra/m.css cd m.css/documentation
The script requires Python 3.6 and depends on Jinja2
for templating and docutils for reST markup
rendering. You can install the dependencies via pip
or your distribution
package manager, in most cases you’ll probably have them already installed:
# You may need sudo here pip3 install docutils jinja2
Next, you need a configuration file which tells the script what modules to
inspect, how to name the project and where to put the output. In this example,
we’ll generate documentation for the Python builtin math
module:
PROJECT_TITLE = "Python math" INPUT_MODULES = ['math']
Now, run the script and pass path to the configuration file to it:
./python.py path/to/conf.py
This will generate an output/
directory next to the conf.py
file and
fill it with the generated output. Open index.html
to see the result.
Features
- Theme tailored from scratch for Python-specific language features
- Uses code inspection to query modules, classes, data, functions and their signatures, does not rely on error-prone source code parsing
- Does not force the documentation writer to explicitly list all symbols in order to have them documented
- Can use both in-code docstrings and external reST files to describe the APIs, giving the user a control over the code size vs documentation verbosity tradeoff
- Opt-in specialized behavior for understanding native bindings written with pybind11 and Python code using attrs
- Ability to generate lightweight Python stubs for IDE autocompletion and type checking that exactly matches the documentation, both for pure Python and native bindings.
Configuration
Together with the above PROJECT_TITLE
and INPUT_MODULES
variables
mentioned above, the configuration file supports the following variables. The
options are similar to the Doxygen config,
but free of the Doxygen-specific naming and constraints.
Variable | Description |
---|---|
PROJECT_TITLE: str |
Project title. Rendered in top navbar, page
title and fine print. If not set,
"My Python Project" is used. |
PROJECT_SUBTITLE: str |
Project subtitle. If set, appended in a
thinner font to PROJECT_TITLE . |
PROJECT_LOGO: str |
URL of an image to use as a log in the top navbar. Default is none. |
MAIN_PROJECT_URL: str |
If set and PROJECT_SUBTITLE is also
set, then PROJECT_TITLE in the top
navbar will link to this URL and
PROJECT_SUBTITLE to the documentation
main page, similarly as
shown here. |
INPUT: str |
Base input directory. If not set, config file base dir is used. Relative paths are relative to config file base dir. |
OUTPUT: Optional[str] |
Where to save the HTML output. Relative
paths are relative to INPUT . If not
set, output/ is used, if set to
None , no HTML output is generated. |
OUTPUT_STUBS: Optional[str] |
Where to save generated Python stubs. See
Stub generation for more information.
Relative paths are relative to INPUT .
If set to None , no stubs are
generated, default is None . |
INPUT_MODULES: List[Any] |
List of modules to generate the docs from. Values can be either strings or module objects. See Module inspection for more information. |
INPUT_PAGES: List[str] |
List of reST files for standalone pages. See Pages for more information. |
THEME_COLOR: str |
Color for <meta name="theme-color" /> ,
corresponding to the CSS style. If empty,
no <meta> tag is rendered. See
Theme selection for more information. |
FAVICON: str |
Favicon URL, used to populate
<link rel="icon" /> . If empty, no
<link> tag is rendered. Relative
paths are searched relative to INPUT
and to the python.py script dir as a
fallback. See Theme selection for more
information. |
STYLESHEETS: List[str] |
List of CSS files to include. Relative
paths are searched relative to INPUT
and to the python.py script dir as a
fallback. See Theme selection for more
information. |
HTML_HEADER: str |
HTML code to put at the end of the
<head> element. Useful for linking
arbitrary JavaScript code or, for example,
adding <link> CSS stylesheets with
additional properties and IDs that are
otherwise not possible with just
STYLESHEETS . |
EXTRA_FILES: List[str] |
List of extra files to copy (for example
additional CSS files that are @import ed
from the primary one). Relative paths are
searched relative to INPUT and to the
python.py script dir as a fallback. |
LINKS_NAVBAR1: List[Any] |
Left navbar column links. See Navbar links for more information. |
LINKS_NAVBAR2: List[Any] |
Right navbar column links. See Navbar links for more information. |
PAGE_HEADER: str |
reST markup to
put at the top of every page. If not set,
nothing is added anywhere. The
{url} placeholder is replaced with
current file URL. |
FINE_PRINT: str |
reST markup to put into the footer. If not set, a default generic text is used. If empty, no footer is rendered at all. |
FORMATTED_METADATA: List[str] |
Which metadata fields should be formatted
in documentation pages. By default only
the summary field is. |
PLUGINS: List[str] |
List of plugins to use. See Plugins for more information. |
PLUGIN_PATHS: List[str] |
Additional plugin search paths. Relative
paths are relative to INPUT . |
CLASS_INDEX_EXPAND_LEVELS |
How many levels of the class index tree to
expand. 0 means only the top-level
symbols are shown. If not set, 1 is
used. |
CLASS_INDEX_EXPAND_INNER |
Whether to expand inner classes in the
class index. If not set, False is
used. |
NAME_MAPPING: Dict[str, str] |
Additional name mapping in addition to
what’s figured out from the __all__
members |
PYBIND11_COMPATIBILITY: bool |
Enable some additional tricks for better
compatibility with pybind11. If not set,
False is used. See
pybind11 compatibility for more
information. |
ATTRS_COMPATIBILITY: bool |
Enable some additional tricks for better
compatibility with attrs. If not set,
False is used. See
attrs compatibility for more
information. |
SEARCH_DISABLED: bool |
Disable search functionality. If this
option is set, no search data is compiled
and the rendered HTML does not contain
search-related UI or support. If not set,
False is used. |
SEARCH_DOWNLOAD_BINARY |
Download search data as a binary to save
bandwidth and initial processing time. If
not set, False is used. See Search options
for more information. |
SEARCH_FILENAME_PREFIX: str |
Search data filename prefix. Useful to
prevent file conflicts if both C++ and
Python documentation shares the same
directory. If not set, searchdata is
used. |
SEARCH_RESULT_ID_BYTES: int |
Search data packing option. A value of
2 , 3 or 4 is allowed. If
not set, 2 is used. See
Search options for more information. |
SEARCH_FILE_OFFSET_BYTES: int |
Search data packing option. A value of
3 or 4 is allowed. If not set,
3 is used. See Search options for
more information. |
SEARCH_NAME_SIZE_BYTES: int |
Search data packing option. A value of
1 or 2 is allowed. If not set,
1 is used. See Search options for
more information. |
SEARCH_HELP: str |
reST markup to
display as help text on empty search popup.
If not set, a default message is used. Has
effect only if SEARCH_DISABLED is not
True . |
SEARCH_BASE_URL: str |
Base URL for OpenSearch-based search engine
suggestions for web browsers. See
Search options for more information. Has
effect only if SEARCH_DISABLED is not
True . |
SEARCH_EXTERNAL_URL: str |
URL for external search. The {query}
placeholder is replaced with urlencoded
search string. If not set, no external
search is offered. See Search options
for more information. Has effect only if
SEARCH_DISABLED is not True . |
DOCUTILS_SETTINGS: Dict[Any] |
Additional docutils settings. Key/value pairs as described in the docs. |
URL_FORMATTER: Callable |
Function for creating filenames and URLs for modules, classes, pages and index pages. See Custom URL formatters for more information. |
ID_FORMATTER: Callable |
Function for creating link anchors for module and class members. See Custom URL formatters for more information. |
STUB_EXTENSION: str |
Extension to use for generated Python
stub files. If not set, .pyi is used.
See Stub generation for more
information. |
STUB_HEADER: str |
Comment block or arbitrary other code to put at the top generated Python stub files. If not set, a default generic text is used. If empty, no header is present in the files at all. See Stub generation for more information. |
Theme selection
By default, the dark m.css theme together with documentation-theme-specific additions is used, which corresponds to the following configuration:
STYLESHEETS = [ 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600', '../css/m-dark+documentation.compiled.css'] THEME_COLOR = '#22272e' FAVICON = 'favicon-dark.png'
If you have a site already using the m-dark.compiled.css
file, there’s
another file called m-dark.documentation.compiled.css
, which contains just
the documentation-theme-specific additions so you can reuse the already cached
m-dark.compiled.css
file from your main site:
STYLESHEETS = [ 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600', '../css/m-dark.compiled.css', '../css/m-dark.documentation.compiled.css'] THEME_COLOR = '#22272e' FAVICON = 'favicon-dark.png'
If you prefer the light m.css theme
instead, use the following configuration (and, similarly, you can use
m-light.compiled.css
together with m-light.documentation.compiled-css
in place of m-light+documentation.compiled.css
:
STYLESHEETS = [ 'https://fonts.googleapis.com/css?family=Libre+Baskerville:400,400i,700,700i%7CSource+Code+Pro:400,400i,600', '../css/m-light+documentation.compiled.css'] THEME_COLOR = '#cb4b16' FAVICON = 'favicon-light.png'
See the CSS files section below for more information about customizing the CSS files.
Search options
Symbol search is implemented using JavaScript Typed Arrays and does not need any server-side functionality to perform well — the client automatically downloads a tightly packed binary containing search data and performs search directly on it.
However, due to restrictions of Chromium-based browsers,
it’s not possible to download data using XMLHttpRequest
when served from
a local file-system. Because of that, the search defaults to producing a
Base85-encoded representation of the search binary and loading that
asynchronously as a plain JavaScript file. This results in the search data
being 25% larger, but since this is for serving from a local filesystem, it’s
not considered a problem. If your docs are accessed through a server (or you
don’t need Chrome support), set the SEARCH_DOWNLOAD_BINARY
option to
True
. The search data are by default fetched from the current directory
on the webserver, if you want to supply a different location, set it to a
string and provide a custom URL formatter.
The site can provide search engine metadata using the OpenSearch
specification. On supported browsers this means you can add the search field to
search engines and search directly from the address bar. To enable search
engine metadata, point SEARCH_BASE_URL
to base URL of your documentation,
for example:
SEARCH_BASE_URL = 'https://doc.magnum.graphics/magnum/'
In general, even without the above setting, appending ?q={query}#search
to
the URL will directly open the search popup with results for {query}
.
If SEARCH_EXTERNAL_URL
is specified, full-text search using an external
search engine is offered if nothing is found for given string or if the user
has JavaScript disabled. It’s recommended to restrict the search to a
particular domain or add additional keywords to the search query to filter out
irrelevant results. Example, using Google search engine and restricting the
search to a subdomain:
SEARCH_EXTERNAL_URL = 'https://google.com/search?q=site:doc.magnum.graphics+{query}'
The search binary is implicitly made with the tightest packing possible for smallest download sizes. On large projects with tens of thousands of symbols it may however happen that the data won’t fit and doc generation fails with an exception such as the following, suggesting you to increase the packed type sizes:
OverflowError: Trie result ID too large to store in 16 bits, set SEARCH_RESULT_ID_BYTES = 3 in your conf.py.
The relevant configuration is SEARCH_RESULT_ID_BYTES
,
SEARCH_FILE_OFFSET_BYTES
and SEARCH_NAME_SIZE_BYTES
. Simply update
your conf.py
with suggested values and restart the generator. Due to the
way the search data get processed during serialization it’s unfortunately not
feasible to estimate the packing sizes beforehand.
Custom URL formatters
The URL_FORMATTER
option allows you to control how all filenames and
generated URLs look like. It takes an entry type and a “path” as a list of
strings (so for example my.module.Class
is represented as
['my', 'module', 'Class']
), returning a tuple of a filename and an URL.
Those can be the same, but also different (for example a file getting saved
into my/module/Class/index.html
but the actual URL being
https://docs.my.module/Class/
). The default implementation looks like this,
producing both filenames and URLs in the form of my.module.Class.html
:
def default_url_formatter(type: EntryType, path: List[str]) -> Tuple[str, str]: if type == EntryType.STATIC: url = os.path.basename(path[0]) # Encode version information into the search driver if url == 'search.js': url = 'search-v{}.js'.format(searchdata_format_version) return url, url url = '.'.join(path) + '.html' assert '/' not in url return url, url
The type
is an enum, if you don’t want to fiddle with imports, compare
type.name
against a string, which is one of 'PAGE'
, 'MODULE'
,
'CLASS'
, 'SPECIAL'
or 'STATIC'
. The 'SPECIAL'
is for
index pages and in that case the path
has always just one item, one of
'pages'
, 'modules'
or 'classes'
. The 'STATIC'
is for
static data such as images or CSS files and the path
is absolute input
filename including the extension and except for search data (which are
generated on-the-fly) it always exists. If the static path is an URL, the URL
formatter is not called.
The ID_FORMATTER
handles formatting of anchors on a page. Again it takes
an entry type (which in this case is always one of 'ENUM'
,
'ENUM_VALUE'
, 'FUNCTION'
, 'PROPERTY'
, 'DATA'
or, in
case of pybind11 code, 'OVERLOADED_FUNCTION'
. The second parameter is
again a path, being always just one item except for 'ENUM_VALUE'
(in
which case it’s enum name and value name together) and for
'OVERLOADED_FUNCTION'
, in which case it contains also a llist of argument
types. The default implementation simply returns the the path concatenated with
dashes:
def default_id_formatter(type: EntryType, path: List[str]) -> str: return '-'.join(path)
Module inspection
By default, if a module contains the __all__
attribute, all names
listed there are exposed in the documentation. Otherwise, all module (and
class) members are extracted using inspect.getmembers()
, skipping names
import
ed from elsewhere and undocumented underscored names.
Additionally, class data members with type annotations (but with no values) are
pulled out from __annotations__
, allowing you to expose (and document)
also fields that might otherwise only be populated from __init__()
:
class MyClass: a_float: float string_value: str
Detecting if a module is a submodule of the current package or if it’s
import
ed from elsewhere is tricky, the script thus includes only
submodules that have their __package__
property the same or one level
below the parent package. If a module’s __package__
is empty, it’s
assumed to be a plain module (instead of a package) and since those can’t have
submodules, all found submodules in it are ignored.
Documentation-only overrides
While the inspection and autodetection follows the structure of the original code, it might not be always desirable to show it exactly that way in the docs. Fortunately, thanks to Python’s dynamic nature, a lot can be done by monkey-patching during doc generation.
In case the autodetection includes more than you want or, conversely, you need
to include names that would otherwise be excluded (such as underscored names),
you can temporarily override the __all__
attribute when generating the
docs. For example, the following will list just the pow()
and log()
functions from the math
module, ignoring the rest:
import math math.__all__ = ['pow', 'log'] INPUT_MODULES = [math]
In other cases, especially when native modules are involved, the inspected name locations might not be what you want. By putting the names into __all__
you tell the script it should map the inspected location to the one provided.
Note you should also hide the original location from the script to avoid
duplicate definitions (unless it’s underscored, in which case it’ll get ignored
automatically).
# module math from _native_math import fast_sin as sin from _native_math import fast_cos as cos __all__ = ['sin', 'cos']
Additionally, for mapping types of external libraries where the autodetection
from __all__
can’t be performed, you can use the NAME_MAPPING
option:
NAME_MAPPING = { 'fastmath._native.Vector3': 'fastmath.Vector3', 'fastmath._native.Quaternion': 'fastmath.Quaternion', # or, equivalently, if the mapping is the same for all members: 'fastmath._native': 'fastmath' }
Docstrings
By default, the first paragraph of a module-level, class-level and function-level docstring is used as a doc summary, copied as-is to the output without formatting it in any way. What follows is put (again without formatting) paragraph-by-paragraph into detailed docs.
"""Module summary First paragraph of module detailed docs.""" class Foo: """Class summary""" def bar(self): """Function summary"""
Using just docstrings, however, comes with a few limitations:
- Class and module-level variables can’t have a docstring attached due to how Python works
- Because not every Python API can be documented using docstrings, the output contains everything, including undocumented names
- Instance variables added inside
__init__()
are not extracted, as this would require parsing Python code directly (which is what Sphinx has to do to support these). You can work around this by adding annotated “declarations” to the class as shown above, however no docstrings can be specified for those either.
To overcome the limitations, externally-supplied documentation provides means to document names that can’t have a docstring attached, and together with the m.sphinx plugin expanding formatting capabilities beyond plain text.
Function and variable annotations
The script uses inspect.signature()
to query function parameter / return
type annotations together with default values and displays them in the output.
Similar is for module and class variables, extracted from the
__annotations__
property. If a variable type implements __repr__()
,
a repr()
of it is printed as the value, otherwise the value is omitted.
from typing import Tuple, List def foo(a: str, be_nice: bool = True) -> Tuple[int, str]: pass SETTINGS: List[Tuple[str, bool]] = []
For better readability, if the function signature contains type annotations or a default value, the arguments are printed each on one line. Otherwise, to avoid wasting vertical space, the arguments are listed on a single line.
Similarly to how the builtin help()
in Python 3.7 started annotating
boundaries between position-only, position-or-keyword and keyword-only
arguments with /
and *
, the same is done here — it’s especially
helpful for native functions, where you can for example call math.sin(0.3)
but not math.sin(x=0.3)
, because the x
argument is positional-only.
Currently, positional-only arguments are possible only with native functions,
PEP570 adds them for pure Python
functions as well.
In some cases, especially when documenting native functions, the signature
can’t be extracted and the function signature shows just an ellipsis (…
)
instead of the actual argument list.
Class methods, static methods, dunder methods, properties
Methods decorated with @classmethod
are put into a “Class methods”
section, @staticmethod
s into a “Static methods” section.
Double-underscored methods explicitly implemented in the class are put into a
“Special methods” section, otherwise they’re ignored — by default, Python
adds a large collection of dunder methods to each class and the only way to
know if the method is user-provided or implicit is by checking the docstring.
class MyClass: @classmethod def a_classmethod(cls): """A class method""" @staticmethod def a_staticmethod(): """A static method""" def __init__(self, foo, bar): """A constructor"""
Properties added to classes either using the @property
decorator or
created with the property()
builtin are added to the “Properties”
section. Each property is annotated with get set del if
it has a getter, a setter and a del
eter or with get
and other variants if it has just some. The docstring and type annotation is
extracted from the property getter.
from typing import Tuple class MyClass: @property def a_read_write_property(self) -> Tuple[int, int]: """A read-write tuple property""" @a_read_write_property.setter def a_read_write_property(self, a): # Docstring and type annotation taken from the getter, no need to # have it repeated here too pass
Documenting enum values
Python supplies an implicit docstrings for enums derived from enum.Enum
and enum values implicitly inherit the docstring of the enum class. If either
is detected to be the case, docstring of the enum or the value is ignored.
While it’s possible to document enum classes the usual way, there’s a
non-obvious way to document enum values as well.
import enum class MyEnum(enum.Enum): """My enum""" ZERO = 0 TWO = 3 CONSISTENCY = -73 MyEnum.ZERO.__doc__ = "Zero value" MyEnum.TWO.__doc__ = "Three, but named TWO for compatibility"
The documentation output for enums includes enum value values and the class it was derived from, so it’s possible to know whether it’s an enum or a flag.
Including underscored names in the output
By default, names starting with an underscore (except for __dunder__
methods) are treated as private and not listed in the output. One way to expose
them is to list them in __all__
, however that works for module content
only. For exposing general underscored names, you either need to provide a
docstring or external documentation content (and in case of plain data,
external documentation content is the only option).
Note that at the point where modules and classes are crawled for members,
docstrings are not parsed yet — so e.g. a data documentation via a
:data:
option of the .. py:class::
m.sphinx directive won’t be
visible to the initial crawl and thus the data will stay hidden.
Sometimes, however, you’ll want the inverse — keeping an underscored name
hidden, even though it has a docstring. Solution is to remove the docstring
while generating the docs, directly in the conf.py
file during module
import:
import mymodule mymodule._private_thing.__doc__ = None INPUT_MODULES = [mymodule]
Pages
In addition to documentation generated by inspecting particular module, it’s
possible to add dedicated documentation pages. Content is written in
reST (see
Writing reST content for a short
introduction) and taken from files specified in INPUT_PAGES
. Filenames
are interpreted relative to configuration file path, output filename is input
basename with extension replaced to .html
. In particular, content of
a index.rst
file is used for the documentation main page. Example:
INPUT_PAGES = ['pages/index.rst']
My Python library ================= :summary: Welcome on the main page! This is a documentation of the mypythonlib module. You can use it like this: .. code:: py import mypythonlib mypythonlib.foo()
Apart from :summary:
, the page can have any number of metadata, with all
of them exposed as properties of page
in the output templates. Fields
listed in FORMATTED_METADATA
(the :summary:
is among them) are
expected to be formatted as reST and exposed as
HTML, otherwise as a plain text.
All referenced images are expected to have either an absolute URL or be
relative to INPUT
, the ones with relative paths are then copied directly
to OUTPUT
with the leading dirs stripped from the path.
Plugins
The reST content is not limited to just the builtin functionality and it’s possible to extend it via plugins either from m.css itself or 3rd party ones. See documentation of each plugin to see its usage; the m.htmlsanity plugin is used unconditionally while all others are optional. For example, enabling the common m.css plugins might look like this:
PLUGINS = ['m.code', 'm.components', 'm.dox']
External documentation content
Because it’s often not feasible to have the whole documentation stored in
Python docstrings, the generator allows you to supply documentation from
external files. Similarly to pages, the INPUT_DOCS
setting is a list
of reST files that contain documentation for
particular names using custom directives. A set of custom directives is
provided by the m.sphinx plugin — see its documentation for detailed description of all features. Below is a simple
example of using it to document a class:
PLUGINS = ['m.sphinx'] INPUT_DOCS = ['docs.rst']
.. py:class:: mymodule.sub.Class :summary: A pretty class This class is *pretty*.
pybind11 compatibility
C++ bindings generated using pybind11 do
not have all information accessible through introspection and thus the script
has to do a few pybind11-specific workarounds to generate expected output. This
behavior is not enabled by default as it might have unwanted consequences in
pure Python code, enable it using the PYBIND11_COMPATIBILITY
option.
Function signatures, property annotations
For reasons explained in pybind/pybind11#990, pybind11 is not able to
provide function signatures through introspection and thus the script falls
back to parsing argument names, type annotations and default values from the
docstring instead. By default, unless py::arg()
is used, function
arguments are positional-only (shown as arg0
, arg1
, …) and marked
as such in the output.
Similarly, property types are extracted from getter docstrings.
Unlike Python, pybind11 has a builtin support for overloaded functions — depending on types passed to a function, it dispatches to a particular C++ overload. The overloads are expanded in the output as well, meaning you can see one function mentioned more than once with different signatures.
Because static methods in pybind11 are not decorated with @staticmethod
,
they are detected based on presence of self
as the first parameter — if
it’s there, it’s an instance method, otherwise it’s a static method.
Enums
Enums in pybind11 are not derived from enum.Enum
, but rather are plain
classes. The only reliable way to detect a pybind11 enum is by looking for a
__members__
member, which is a dict providing string names and their
corresponding values. With pybind 2.2, it’s only possible to document the
enum class itself, not the values.
attrs compatibility
If a codebase is using the attrs package and the
ATTRS_COMPATIBILITY
option is enabled, the script is able to extract the
(otherwise inaccessible by normal means) information about attributes defined
using attr.ib()
or via the @attr.s(auto_attribs=True)
decorator.
Note that attributes of classes using @attr.s(slots=True)
are visible
even without the compatibility enabled.
In all cases, there’s no possibility of adding in-source docstrings for any of
these and you need to supply the documentation with the .. py:property::
directive as described in External documentation content.
Additionally, various dunder methods that say just “Automatically created by attrs.” in their docstring are implicitly hidden from the output if this option is enabled. In order to show them again, override the docstring to something meaningful.
Stub generation
By default, the tool produces just a HTML documentation. In many cases, and especially when native bindings are involved, it’s useful to provide so-called stubs for the IDE in order for it to provide useful autocompletion and perform type checking. Another use case is when the amount of code in the actual implementation is too large for the IDE to handle.
Stubs can be opted in by specifying a path where to generate them in
OUTPUT_STUBS
. They can be generated either together with the HTML output,
or alone, if OUTPUT
is set to None
instead. A common setup in that
case is to create a second file, named conf-stubs.py
, that inherits
everything from conf.py
but enables only the stub output:
# Inherit everything from conf.py import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__))) from conf import * OUTPUT = None OUTPUT_STUBS = 'stubs/'
Now, when you run the script, the output directory will contain *.pyi
files
that you can supply to your IDE.
./python.py path/to/conf-stubs.py
Command-line options
./python.py [-h] [--templates TEMPLATES] [--debug] conf
Arguments:
conf
— configuration file
Options:
-h
,--help
— show this help message and exit--templates TEMPLATES
— template directory. Defaults to thetemplates/python/
subdirectory if not set.--debug
— verbose logging output. Useful for debugging.
Implementing custom plugins
Third-party plugins can be loaded from paths specified in PLUGIN_PATHS
.
Custom plugins need to implement a registration function named
register_mcss()
. It gets passed the following named arguments and the
plugin might or might not use them.
Keyword argument | Content |
---|---|
mcss_settings |
Dict containing all m.css settings |
jinja_environment |
Jinja2 environment. Useful for adding new filters etc. |
module_doc_contents |
Module documentation contents |
class_doc_contents |
Class documentation contents |
enum_doc_contents |
Enum documentation contents |
enum_value_doc_contents |
Enum documentation contents |
function_doc_contents |
Function documentation contents |
property_doc_contents |
Property documentation contents |
data_doc_contents |
Data documentation contents |
hooks_post_crawl |
Hooks to call after the initial name crawl |
hooks_pre_scope |
Hooks to call on scope enter |
hooks_post_scope |
Hooks to call on scope exit |
hooks_docstring |
Hooks to call when parsing a docstring |
hooks_pre_page |
Hooks to call before each page gets rendered |
hooks_post_run |
Hooks to call at the very end of the script run |
The module_doc_contents
, class_doc_contents
, enum_doc_contents
,
enum_value_doc_contents
, function_doc_contents
,
property_doc_contents
and data_doc_contents
variables are
Dict[str, Dict[str, str]]
, where the first level is a name and second
level are key/value pairs of the actual HTML documentation content. Plugins
that parse extra documentation inputs (such as m.sphinx) are supposed to add
to the dict, which is then used to fill the actual documentation contents. The
following corresponds to the documentation source shown in the
External documentation content section below. Note that the dict can already
have existing entries added from elsewhere, so it’s important to avoid fully
overwriting it:
docs = class_doc_contents.setdefault('mymodule.sub.Class', {}) docs['summary'] = "A pretty class" docs['details'] = "This class is *pretty*."
The hooks_post_crawl
, hooks_docstring
, hooks_pre_page
and
hooks_post_run
variables are lists of functions. Plugins that need to do
something at specific points of the execution are supposed to add functions to
the list.
The hooks_post_crawl
is called once gathering of all names is done. It
gets passed the following arguments:
Keyword argument | Content | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
name_map |
Map with all gathered module, class, enum, function, property, data and page metadata. Plugins are allowed to read from it (for example to serialize them to a file for searching or linking from other projects) as well as write to it (for example to allow linking to names from external projects). Key is a name, value has at the following properties:
|
- 1.
- ^ As this distinguishes between internal and external entries, new entries
added by the plugin need to have
object
set toNone
so the script as well as other plugins can correctly distinguish them.
The hooks_pre_scope
and hooks_post_scope
get called before entering
and after leaving a name scope (page, module, class, enum, enum value,
function, property or data), and are meant mainly to aid with context-sensitive
linking. Those scopes can be nested and can be called successively for the same
scope — for example, when rendering module docs, hooks_pre_scope
gets
called first for the module scope, but then another hooks_pre_scope
gets
called when rendering a summary for reference to an inner class. Then,
hooks_post_scope
gets called in reverse order. The plugins are expected
to implement a stack-like data structure for maintaining information about
current scope. Both of those functions get passed the following arguments:
Keyword argument | Content |
---|---|
type |
Type of the scope that’s being entered or exited. Same as the enum passed to custom URL formatters. |
path |
Path of the module / class / function / enum / enum value /
data scope that’s being entered or exited. A list of names,
'.'.join(path) is equivalent to the fully qualified
name. |
param_names |
In case of functions, list of parameter names. This argument is not present otherwise. |
Hooks listed in hooks_docstring
are called when docstrings are parsed,
and always preceded by a corresponding hooks_pre_scope
call. The first
listed hook gets the raw docstring only processed by inspect.cleandoc()
and each following gets the output of the previous. When a hook returns an
empty string, hooks later in the list are not called. String returned by the
last hook is processed, if any, the same way as if no hooks would be present
— it gets partitioned into summary and content and those put to the output
as-is, each paragraph wrapped in <p>
tags. The hooks are free to do
anything with the docstring — extracting metadata from it and returning it
as-is, transpiling it from one markup language to another, or fully consuming
it, populating the *_doc_contents
variables mentioned above and returning
nothing back. Each hook gets passed the following arguments:
Keyword argument | Content |
---|---|
type |
Name type. Same as the enum passed to custom URL formatters. |
path |
Path of the module / class / function / enum / enum value /
data containing the docstring. A list of names,
'.'.join(path) is equivalent to the fully qualified
name. |
signature |
Signature of a function, for distinguishing between
particular overloads. In a form of
(param1: type1, param2: type2) . |
doc |
Docstring content. Always non-empty — once a hook returns nothing back, no further hooks are called. |
The hooks_pre_page
is called before each page of output gets rendered.
Can be used for example for resetting some internal counter for page-wide
unique element IDs. The hooks_post_run
is called after the whole run is
done, useful for example to serialize cached internal state. Currently, those two functions get no arguments passed.
Registration function for a plugin that needs to query the OUTPUT
setting
might look like this — the remaining keyword arguments will collapse into
the **kwargs
parameter. See code of various m.css plugins for actual
examples. The below example shows registration of a hypothetic HTML validator
plugin — it saves the output path from settings and registers a post-run hook
that validates everything in given output directory.
output_dir = None … def _validate_output(): validate_all_html_files(output_dir) def register_mcss(mcss_settings, hooks_post_run, **kwargs): global output_dir output_dir = mcss_settings['OUTPUT'] hooks_post_run += [_validate_output]
Customizing the template
The rest of the documentation explains how to customize the builtin template to better suit your needs. Each documentation file is generated from one of the template files that are bundled with the script. However, it’s possible to provide your own Jinja2 template files for customized experience as well as modify the CSS styling.
CSS files
By default, compiled CSS files are used to reduce amount of HTTP requests and
bandwidth needed for viewing the documentation. However, for easier
customization and debugging it’s better to use the unprocessed stylesheets. The
STYLESHEETS
option lists all files that go to the
<link rel="stylesheet" />
in the resulting HTML markup, while
EXTRA_FILES
list the indirectly referenced files that need to be copied
to the output as well. Below is an example configuration corresponding to the
dark theme:
STYLESHEETS = [ 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600', '../css/m-dark.css', '../css/m-documentation.css'] EXTRA_FILES = [ '../css/m-grid.css', '../css/m-components.css', '../css/pygments-dark.css', '../css/pygments-console.css'] THEME_COLOR = '#22272e'
After making desired changes to the source files, it’s possible to postprocess
them back to the compiled version using the postprocess.py
utility as
explained in the CSS themes
documentation. In case of the dark theme, the m-dark+documentation.compiled.css
and m-dark.documentation.compiled.css
files are produced like this:
cd css ./postprocess.py m-dark.css m-documentation.css -o m-dark+documentation.compiled.css ./postprocess.py m-dark.css m-documentation.css --no-import -o m-dark.documentation.compiled.css
Output templates
Each output file is rendered with one of these templates:
Filename | Use |
---|---|
module.html |
Module documentation |
class.html |
Class documentation |
page.html |
Explicit documentation pages, including the main page |
Each template gets passed all configuration values from the Configuration
table as-is, together with a URL
variable with URL of given output file.
In addition to builtin Jinja2 filters, the format_url
filter returns either
a path formatted according to custom URL formatters, if the path is relative;
or a full URL, if the argument is an absolute URL. It’s useful in cases like
this:
{% for css in HTML_EXTRA_STYLESHEET %} <link rel="stylesheet" href="{{ css|format_url|e }}" /> {% endfor %}
The actual page contents are provided in a page
object, which has the
following properties. All exposed data are meant to be passed directly to the
HTML markup without any additional escaping.
Property | Description |
---|---|
page.summary |
Doc summary |
page.filename |
File name 4 |
page.url |
File URL 4 |
page.breadcrumb |
List of (title, URL) tuples for
breadcrumb navigation. |
page.content |
Detailed documentation, if any |
Each module page, rendered with module.html
, has the following additional
properties:
Property | Description |
---|---|
page.prefix_wbr |
Fully-qualified symbol prefix for given
compound with trailing . with
<wbr/> tag after every . . |
page.dependencies |
List of (prefix, module)
tuples with module dependencies. To
match relative type names, it’s either
import module if prefix is
empty, from prefix import * if
module is empty or
from prefix import module if both
are non-empty. |
page.modules |
List of inner modules. See Module properties for details. |
page.classes |
List of classes. See Class properties for details. |
page.enums |
List of enums. See Enum properties for details. |
page.functions |
List of module-level functions. See Function properties for details. |
page.data |
List of module-level data. See Data properties for details. |
page.has_enum_details |
If there is at least one enum with full description block 3 |
page.has_function_details |
If there is at least one function (or method, in case of classes) with full description block 3 |
page.has_data_details |
If there is at least one data with full description block 3 |
Each class page, rendered with class.html
, has the following additional
properties:
Property | Description |
---|---|
page.classmethods |
List of class methods (annotated with
@classmethod ). See
Function properties for details. |
page.staticmethods |
List of static methods (annotated with
@staticmethod ). See
Function properties for details. |
page.methods |
List of methods. See Function properties for details. |
page.dunder_methods |
List of double-underscored special functions. See Function properties for details. |
page.properties |
List of properties. See Property properties for details. |
page.has_property_details |
If there is at least one property with full description block 3 |
page.has_members |
If the class contains at least one member or is completely empty |
Explicit documentation pages rendered with class.html
have additional
properties taken from input metadata. If given metadata is listed in
FORMATTED_METADATA
, it’s rendered into HTML, otherwise it’s exposed as
plain text.
Module properties
Property | Description |
---|---|
module.url |
URL of detailed module documentation |
module.name |
Module name |
module.summary |
Doc summary |
Class properties
Property | Description |
---|---|
class_.url |
URL of detailed class documentation |
class_.name |
Class name |
class_.summary |
Doc summary |
Enum properties
Property | Description |
---|---|
enum.name |
Enum name |
enum.id |
Enum ID 5 |
enum.summary |
Doc summary |
enum.content |
Detailed documentation, if any |
enum.base |
Fully qualified name of a base class
from which the enum is derived. Set to
None if no base class information
is available. |
enum.base_relative |
Like enum.base , but relative,
i.e. with a common prefix of the base
class and containing module / class
omitted |
enum.base_link |
Like enum.base_relative , but with
cross-linked types |
enum.values |
List of enum values |
enum.has_details |
If there is enough content for the full description block. 3 |
enum.has_value_details |
If the enum values have description.
Impies enum.has_details . |
Every item of enum.values
has the following properties:
Property | Description |
---|---|
value.name |
Value name |
value.id |
Value ID 5 |
value.value |
Value value. Set to None if no value is
available. |
value.content |
Value documentation, if any |
Function properties
Property | Description |
---|---|
function.name |
Function name |
function.id |
Function ID 5 |
function.summary |
Doc summary |
function.content |
Detailed documentation, if any |
function.type |
Fully qualified function return type annotation 2 |
function.type_quoted |
Like function.type , but relative,
i.e. with a common prefix of the type and
containing module / class omitted, and with
parts quoted if type inspection failed to
match the annotation to an actual name |
function.type_link |
Like function.type , but relative,
i.e. with a common prefix of the type and
containing module / class omitted, and with
cross-linked types |
function.params |
List of function parameters. See below for details. |
function.exceptions |
List of exceptions raised by this function. See below for details. |
function.has_complex_params |
Set to True if the parameter list
should be wrapped on several lines for
better readability (for example when it
contains type annotations or default
arguments). Set to False when
wrapping on multiple lines would only
occupy too much vertical space. |
function.has_param_details |
If the function parameters are documented |
function.return_value |
Return value documentation. Can be empty. |
function.has_details |
If there is enough content for the full description block 3 |
function.is_method |
Set to True if the function is a
class method, False if it’s
standalone. |
function.is_classmethod |
Set to True if the function is
annotated with @classmethod ,
False otherwise. |
function.is_staticmethod |
Set to True if the function is
annotated with @staticmethod ,
False otherwise. |
function.is_overloaded |
Set to True if the function has
multiple overloads with the same name,
False otherwise. |
The function.params
is a list of function parameters and their
description. Each item has the following properties:
Property | Description |
---|---|
param.name |
Parameter name |
param.type |
Fully qualified parameter type annotation 2 |
param.type_quoted |
Like param.type , but relative, i.e. with
a common prefix of the type and containing
module / class omitted, and with parts quoted
if type inspection failed to match the
annotation to an actual name |
param.type_link |
Like param.type_relative , but relative,
i.e. with a common prefix of the type and
containing module / class omitted, and with
cross-linked types |
param.default |
Default parameter value, if any. If
param.type is an enum, is a
fully-qualified enum value. |
param.default_relative |
Like param.default , but relative, i.e.
with a common prefix of the type and containing
module / class omitted if param.type is
an enum |
param.default_link |
Like param.default_relative , but with
cross-linked types |
param.kind |
Parameter kind, a string equivalent to one of the inspect.Parameter.kind values |
param.content |
Detailed documentation, if any |
In some cases (for example in case of native APIs), the parameters can’t be
introspected. In that case, the parameter list is a single entry with name
set to None
and the rest being None
as well. It’s then up to the
template what it does. For example the builtin Python help()
shows
...
(which isn’t valid syntax), if you need a valid syntax you can also use
*args
instead.
The function.exceptions
is a list of exceptions types and descriptions.
Each item has the following properties:
Property | Description |
---|---|
exception.type |
Fully qualified exception type |
exception.type_relative |
Like exception.type , but relative, i.e.
with a common prefix of the type and containing
module / class omitted |
exception.type_link |
Like exception.type_relative , but with a
cross-linked type |
exception.content |
Detailed documentation |
Property properties
Property | Description |
---|---|
property.name |
Property name |
property.id |
Property ID 5 |
property.type |
Fully qualified property getter return type annotation 2 |
property.type_quoted |
Like property.type , but relative, i.e.
with a common prefix of the type and containing
module / class omitted, and with parts quoted
if type inspection failed to match the
annotation to an actual name |
property.type_link |
Like property.type , but relative, i.e.
with a common prefix of the type and containing
module / class omitted, and with cross-linked
types |
property.summary |
Doc summary |
property.content |
Detailed documentation, if any |
property.exceptions |
List of exceptions raised when accessing this
property. Same as function.exceptions
described in function properties. |
property.is_gettable |
If the property is gettable |
property.is_settable |
If the property is settable |
property.is_deletable |
If the property is deletable with del |
property.has_details |
If there is enough content for the full description block 3 |
Data properties
Property | Description |
---|---|
data.name |
Data name |
data.id |
Data ID 5 |
data.type |
Fully qualified data type annotation 2 |
data.type_quoted |
Like data.type , but relative, i.e. with a
common prefix of the type and containing module
/ class omitted, and with parts quoted
if type inspection failed to match the
annotation to an actual name |
data.type_link |
Like data.type , but relative, i.e.
with a common prefix of the type and containing
module / class omitted, and with cross-linked
types |
data.summary |
Doc summary |
data.content |
Detailed documentation, if any |
data.value |
Data value representation |
data.has_details |
If there is enough content for the full description block 3 |
Index page templates
The following index pages are provided, showing a expandable tree of the contents:
Filename | Use |
---|---|
classes.html |
Class listing |
modules.html |
Module listing |
pages.html |
Page listing |
Each template is passed all configuration values from the Configuration
table as-is, together with an URL
, as above. The navigation tree is
provided in an index
object, which has the following properties:
Property | Description |
---|---|
index.classes |
List of all modules + classes |
index.pages |
List of all pages |
The form of each list entry is the same:
Property | Description |
---|---|
i.kind |
Entry kind (one of 'module' ,
'class' or 'page' ) |
i.name |
Name |
i.url |
URL of the file with detailed documentation |
i.summary |
Doc summary |
i.has_nestable_children |
If the list has nestable children (i.e., dirs or namespaces) |
i.children |
Recursive list of child entries |
Module/class list is ordered in a way that all modules are before all classes.
- 2.
- ^ a b c d
i.type
is extracted out of function parameter type, function return type, property type and data type annotation. If the types aren’t annotated, the annotation is empty. - 3.
- ^ a b c d e f g h
page.has_*_details
andi.has_details
areTrue
if there is detailed description, function parameter documentation or documented enum value listing that makes it worth to render the full description block. IfFalse
, the member should be included only in the summary listing on top of the page to avoid unnecessary repetition. - 4.
- ^ a b
page.filename
andpage.url
is generated by an URL formatter, see Custom URL formatters for more information - 5.
- ^ a b c d e
i.id
is an ID used for linking to given entry on a page. Generated by an anchor formatter, see Custom URL formatters for more information.