Skip to content

Instantly share code, notes, and snippets.

@dleske
Last active February 16, 2022 19:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dleske/9b192fa3abbd7264a157c74d58a469fd to your computer and use it in GitHub Desktop.
Save dleske/9b192fa3abbd7264a157c74d58a469fd to your computer and use it in GitHub Desktop.
pdoc/mako template for generating simple Markdown documentation from Python docstring

pdoc/mako template for making API docs from Python docstrings

Use this to generate basic API documentation that you can drop into your repository alongside your code. To build for a module named myapp.mymodule:

  1. Place the text.mako file into some directory $DIR.
  2. Install pdoc: pip install pdoc
  3. Generate the documentation: pdoc3 --template-dir=$DIR myapp.module > myapp.module.md

Note this template was actually written for a project hosted on a GitLab instance, so for GitHub, remove [[_TOC_]] since that is not supported by GitHub.

Limitations:

  • Unlike the stock pdoc templates, this does not support Numpy style Docstrings. Just Google style.
  • It would be possible to create a TOC as part of this template instead of leaving it up to GitLab/not having one at all in GitHub, but limitations of Markdown in creating anchors get in the way.
docs/%.md: myapp/%.py misc/pdoc-templates/text.mako
@pdoc3 --template-dir=misc/pdoc-templates myapp.$* > $@
docs: docs/mymodule.md docs/othermodule.md
<%
import os
import pdoc
import re
import textwrap
import inspect
def firstline(ds):
return ds.split('\n\n', 1)[0]
def link(dobj: pdoc.Doc, name=None):
name = name or dobj.qualname + ('()' if isinstance(dobj, pdoc.Function) else '')
# dobj.module is None so pull module name from qualname
parts = dobj.qualname.split('.')
app = parts[0]
module = parts[1]
if len(parts) > 2:
obj = parts[2]
return '[{}](docs/{}.md#{})'.format(obj, module, obj)
return '[{}](docs/{}.md)'.format(module, module)
def get_annotation(bound_method, sep=':'):
annot = show_type_annotations and bound_method(link=link) or ''
if annot:
annot = ' ' + sep + '\N{NBSP}' + annot
return annot
def header(text, level):
hashes = '#' * level
return '\n{} {}'.format(hashes, text)
def breakdown_google(text):
"""
Break down Google-style docstring format.
"""
def get_terms(body):
breakdown = re.compile(r'\n+\s+(\S+):\s+', re.MULTILINE).split('\n' + body)
# first match is blank (or could be section name if it was still there)
return list(map(lambda x: textwrap.dedent(x), breakdown[1:]))
# what we want to do is return the body, before any of the below is
# matched, and then a list of sections and their terms
matches = re.compile(r'([A-Z]\w+):$\n', re.MULTILINE).split(inspect.cleandoc(text))
if not matches:
return
body = textwrap.dedent(matches[0].strip())
sections = {}
for i in range(1, len(matches), 2):
title = matches[i].title()
section = matches[i+1]
if title in ('Args', 'Attributes', 'Raises'):
sections[title] = get_terms(section)
else:
sections[title] = textwrap.dedent(section)
return (body, sections)
def format_for_list(docstring, depth=1):
spaces = depth * 2 * ' '
return re.compile(r'\n\n', re.MULTILINE).sub('\n\n' + spaces, docstring)
%>
## --------------------------------------------------------------------------
## show_breakdown
<%def name="show_breakdown(breakdown)">
<%
body = breakdown[0]
sections = breakdown[1]
def docsection(text):
return "**{}**\n\n".format(text)
%>
${body}
<%def name="show_args(args)">
% for i in range(0, len(args), 2):
* **`${args[i]}`**: ${args[i+1]}
% endfor
</%def>
% if sections.get('Args', None):
${docsection('Arguments')}
${show_args(sections['Args'])}
% endif
% if sections.get('Attributes', None):
${docsection('Attributes')}
${show_args(sections['Attributes'])}
% endif
% if sections.get('Returns', None):
${docsection('Returns')}
${sections['Returns']}
% endif
% if sections.get('Raises', None):
${docsection('Raises')}
${show_args(sections['Raises'])}
% endif
% if sections.get('Note', None):
${docsection('Note')}
${sections['Note']}
% endif
% if sections.get('Notes', None):
${docsection('Notes')}
${sections['Notes']}
% endif
</%def>
## --------------------------------------------------------------------------
## show_desc
<%def name="show_desc(d, short=False)">
<%
inherits = ' inherited' if d.inherits else ''
#docstring = firstline(d.docstring) if short or inherits else breakdown_google(d.docstring)
%>
% if d.inherits:
_Inherited from:_
% if hasattr(d.inherits, 'cls'):
`${link(d.inherits.cls)}`.`${link(d.inherits, d.name)}`
% else:
`${link(d.inherits)}`
% endif
% endif
% if short or inherits:
${firstline(d.docstring)}
% else:
${show_breakdown(breakdown_google(d.docstring))}
% endif
</%def>
## --------------------------------------------------------------------------
## show_list
<%def name="show_list(items, indent=1)">
<%
spaces = ' ' * indent
%>
% for item in items:
${spaces}* ${link(item, item.name)}
% endfor
</%def>
## --------------------------------------------------------------------------
## show_funcs
<%def name="show_func(f, qual='')">
<%
params = ', '.join(f.params(annotate=show_type_annotations, link=link))
return_type = get_annotation(f.return_annotation, '\N{non-breaking hyphen}>')
qual = qual + ' ' if qual else ''
%>
---
${header('', 4)} ${qual}<code>${f.name}(${params})${return_type}</code>
${show_desc(f)}
</%def>
<%def name="show_funcs(fs, qual='')">
% for f in fs:
${show_func(f, qual)}
% endfor
</%def>
## --------------------------------------------------------------------------
## show_vars
<%def name="show_vars(vs, qual='')">
<%
qual = qual + ' ' if qual else ''
%>
% for v in vs:
<%
return_type = get_annotation(v.type_annotation)
return_type_d = ' ' + return_type if return_type else ''
desc = ' - ' + format_for_list(v.docstring, 1) if v.docstring else ''
%>
* ${qual}`${v.name}${return_type_d}`${desc}
% endfor
</%def>
## --------------------------------------------------------------------------
## show_module
<%def name="show_module(module)">
<%
variables = module.variables(sort=sort_identifiers)
classes = module.classes(sort=sort_identifiers)
functions = module.functions(sort=sort_identifiers)
submodules = module.submodules()
%>
## # ${'Namespace' if module.is_namespace else \
## 'Package' if module.is_package and not module.supermodule else \
## 'Module'} <code>${module.name}</code></h1>
${header(module.docstring, 1)}
[[_TOC_]]
%if submodules or variables or functions:
${header('Module', 2)}
% if submodules:
${header('Submodules', 3)}
% for m in submodules:
<%
desc = ' - ' + firstline(m.docstring) if m.docstring else ''
%>
* `${link(m)}`${desc}
% endfor
% endif
% if functions:
${header('Functions', 3)}
${show_funcs(functions)}
% endif
% endif
% if variables:
${header('Global variables', 3)}
${show_vars(variables)}
% endif
% if classes:
${header('Classes', 2)}
% for c in classes:
<%
class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers)
smethods = c.functions(show_inherited_members, sort=sort_identifiers)
inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers)
methods = c.methods(show_inherited_members, sort=sort_identifiers)
mro = c.mro()
subclasses = c.subclasses()
params = ', '.join(c.params(annotate=show_type_annotations, link=link))
%>
${header('', 3)} ${c.name}
<code>class <b>${c.name}</b>(${params})</code>
${show_desc(c)}
% if mro:
${header('Ancestors', 4)}
% for cls in mro:
* ${link(cls)}
% endfor
%endif
% if subclasses:
${header('Subclasses', 4)}
% for sub in subclasses:
* ${link(sub)}
% endfor
% endif
% if smethods:
## ${header('Static methods', 4)}
${show_funcs(smethods, 'static')}
% endif
% if methods:
## ${header('Methods', 4)}
${show_funcs(methods)}
% endif
% if class_vars or inst_vars:
---
${header('Variables', 4)}
% if class_vars:
${show_vars(class_vars, 'static')}
% endif
% if inst_vars:
${show_vars(inst_vars)}
% endif
% endif
% if not show_inherited_members:
<%
members = c.inherited_members()
%>
% if members:
${header('Inherited members', 4)}
% for cls, mems in members:
* `${link(cls)}`:
% for m in mems:
* `${link(m, name=m.name)}`
% endfor
% endfor
% endif
% endif
---
% endfor
% endif
</%def>
${show_module(module)}
---
Generated by [pdoc ${pdoc.__version__}](https://pdoc3.github.io/pdoc).
@NicolasGoeddel
Copy link

Thank you for that template!

I found a bug: It seems the submodules have always to be placed inside a docs folder ignoring the --output-dir parameter. Also it seems to miss the modules folder.

Example call: pdoc3 -o doc --template-dir=pdoc --force ./icinga-api-client
Results in the files:

  • doc/icinga-api-client/index.md
  • doc/icinga-api-client/client.md

Inside the index.md the link looks like this:

### Submodules
    
* `[client](docs/client.md)` - IcingaAPIClient

Instead it should be like this:

### Submodules
    
* `[client](doc/icinga-api-client/client.md)` - IcingaAPIClient

Is this an easy fix?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment