#! /usr/bin/env python3
"""
markdowndoc.py
Written by Patrick Laban and Geremy Condra
Licensed under GPLv3
Released 28 April 2009
This module contains a simple class to output Markdown-style
pydocs.
"""
import pydoc, inspect, re, builtins
class MarkdownDoc(pydoc.TextDoc):
underline = "*" * 40
def process_docstring(self, obj):
"""Get the docstring and turn it into a list."""
docstring = pydoc.getdoc(obj)
if docstring:
return docstring + "\n\n"
return ""
def process_class_name(self, name, bases, module):
"""Format the class's name and bases."""
title = "## class " + self.bold(name)
if bases:
# get the names of each of the bases
base_titles = [pydoc.classname(base, module) for base in bases]
# if its not just object
if len(base_titles) > 1:
# append the list to the title
title += "(%s)" % ", ".join(base_titles)
return title
def process_subsection(self, name):
"""format the subsection as a header"""
return "### " + name
def docclass(self, cls, name=None, mod=None):
"""Produce text documentation for the class object cls."""
# the overall document, as a line-delimited list
document = []
# get the object's actual name, defaulting to the passed in name
name = name or cls.__name__
# get the object's bases
bases = cls.__bases__
# get the object's module
mod = cls.__module__
# get the object's MRO
mro = [pydoc.classname(base, mod) for base in inspect.getmro(cls)]
# get the object's classname, which should be printed
classtitle = self.process_class_name(name, bases, mod)
document.append(classtitle)
document.append(self.underline)
# get the object's docstring, which should be printed
docstring = self.process_docstring(cls)
document.append(docstring)
# get all the attributes of the class
attrs = []
for name, kind, classname, value in pydoc.classify_class_attrs(cls):
if pydoc.visiblename(name):
attrs.append((name, kind, classname, value))
# sort them into categories
data, descriptors, methods = [], [], []
for attr in attrs:
if attr[1] == "data" and not attr[0].startswith("_"):
data.append(attr)
elif attr[1] == "data descriptor" and not attr[0].startswith("_"):
descriptors.append(attr)
elif "method" in attr[1] and not attr[2] is builtins.object:
methods.append(attr)
if data:
# start the data section
document.append(self.process_subsection(self.bold("data")))
document.append(self.underline)
# process your attributes
for name, kind, classname, value in data:
if hasattr(value, '__call__') or inspect.isdatadescriptor(value):
doc = getdoc(value)
else:
doc = None
document.append(self.docother(getattr(cls, name), name, mod, maxlen=70, doc=doc) + '\n')
if descriptors:
# start the descriptors section
document.append(self.process_subsection(self.bold("descriptors")))
document.append(self.underline)
# process your descriptors
for name, kind, classname, value in descriptors:
document.append(self._docdescriptor(name, value, mod))
if methods:
# start the methods section
document.append(self.process_subsection(self.bold("methods")))
document.append(self.underline)
# process your methods
for name, kind, classname, value in methods:
document.append(self.document(getattr(cls, name), name, mod, cls))
return "\n".join(document)
def bold(self, text):
""" Formats text as bold in markdown. """
if text.startswith('_') and text.endswith('_'):
return "__\%s\__" %text
elif text.startswith('_'):
return "__\%s__" %text
elif text.endswith('_'):
return "__%s\__" %text
else:
return "__%s__" %text
def indent(self, text, prefix=''):
"""Indent text by prepending a given prefix to each line."""
return text
def section(self, title, contents):
"""Format a section with a given heading."""
clean_contents = self.indent(contents).rstrip()
return "# " + self.bold(title) + '\n\n' + clean_contents + '\n\n'
def docroutine(self, object, name=None, mod=None, cl=None):
"""Produce text documentation for a function or method object."""
realname = object.__name__
name = name or realname
note = ''
skipdocs = 0
if inspect.ismethod(object):
object = object.__func__
if name == realname:
title = self.bold(realname)
else:
if (cl and realname in cl.__dict__ and cl.__dict__[realname] is object):
skipdocs = 1
title = self.bold(name) + ' = ' + realname
if inspect.isfunction(object):
args, varargs, varkw, defaults, kwonlyargs, kwdefaults, ann = inspect.getfullargspec(object)
argspec = inspect.formatargspec(
args, varargs, varkw, defaults, kwonlyargs, kwdefaults, ann,
formatvalue=self.formatvalue,
formatannotation=inspect.formatannotationrelativeto(object))
if realname == '<lambda>':
title = self.bold(name) + ' lambda '
# XXX lambda's won't usually have func_annotations['return']
# since the syntax doesn't support but it is possible.
# So removing parentheses isn't truly safe.
argspec = argspec[1:-1] # remove parentheses
else:
argspec = '(...)'
decl = "#### " + "def " + title + argspec + ':' + '\n' + note
if skipdocs:
return decl + '\n'
else:
doc = pydoc.getdoc(object) or ''
return decl + '\n' + (doc and self.indent(doc).rstrip() + '\n')
def docother(self, object, name=None, mod=None, parent=None, maxlen=None, doc=None):
"""Produce text documentation for a data object."""
line = "#### " + object.__name__ + "\n"
line += super().docother(object, name, mod, parent, maxlen, doc)
return line + "\n"
def _docdescriptor(self, name, value, mod):
results = ""
if name: results += "#### " + self.bold(name) + "\n"
doc = pydoc.getdoc(value) or ""
if doc: results += doc + "\n"
return results