1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
|
'''
Extension for enhancing sphinx documentation generation for cython module
'''
import re
import types
import sys
from os.path import dirname, join
import sphinx
from sphinx.ext.autodoc import MethodDocumenter
class CythonMethodDocumenter(MethodDocumenter):
# XXX I don't understand the impact of having a priority more than the
# attribute or instance method but the thing is, if it's a cython module,
# the attribute will be preferred over method.
priority = 12
def is_cython_extension(what, obj):
# try to check if the first line of the doc is a signature
doc = obj.__doc__
if not doc:
return False
doc = doc.split('\n')
if not len(doc):
return False
doc = doc[0]
# an identifier starts with a letter or underscore
# followed by optional numbers or letters or underscores
identifier_pattern = r"([a-zA-Z_][a-zA-Z0-9_]*)"
params_pattern = r"\((.*)\)"
# test for cython cpdef
if what in ('attribute', 'method') and hasattr(obj, '__objclass__'):
# match identifier.identifier(anything).
return re.match(
r"^" + identifier_pattern
+ r"\." + identifier_pattern + params_pattern,
doc)
# test for cython class
if what == 'class' and hasattr(obj, '__pyx_vtable__'):
# match identifier(anything)
return re.match(r"^" + identifier_pattern + params_pattern, doc)
# test for python method in cython class
if what in ('method', 'function') and obj.__class__ == types.BuiltinFunctionType:
# match identifier(anything) where
return re.match(r"^" + identifier_pattern + params_pattern, doc)
def callback_docstring(app, what, name, obj, options, lines):
if what == 'module':
# remove empty lines
while len(lines):
line = lines[0].strip()
if not line.startswith('.. _') and line != '':
break
lines.pop(0)
# if we still have lines, remove the title
if len(lines):
lines.pop(0)
# if the title is followed by a separator, remove it.
if len(lines) and lines[0].startswith('=='):
lines.pop(0)
elif is_cython_extension(what, obj) and lines:
if what == 'class':
lines.pop(0)
line = lines.pop(0)
# trick to realign the first line to the second one.
# FIXME: fail if we finishing with::
line_with_text = [x for x in lines if len(x.strip())]
if len(line_with_text) and line is not None and len(lines):
l = len(line_with_text[0]) - len(line_with_text[0].lstrip())
else:
l = 0
lines.insert(0, ' ' * l + line)
# calculate the minimum space available
min_space = 999
for line in lines:
if not line.strip():
continue
min_space = min(min_space, len(line) - len(line.lstrip()))
# remove that kind of space now.
if min_space > 0:
spaces = ' ' * min_space
for idx, line in enumerate(lines):
if not line.strip():
continue
if not line.startswith(spaces):
continue
lines[idx] = line[min_space:]
def callback_signature(app, what, name, obj, options, signature,
return_annotation):
# remove the first 'self' argument, because python autodoc don't
# add it for python method class. So do the same for cython class.
if is_cython_extension(what, obj):
try:
doc = obj.__doc__.split('\n').pop(0)
doc = '(%s' % doc.split('(')[1]
doc = doc.replace('(self, ', '(')
doc = doc.replace('(self)', '( )')
return doc, None
except AttributeError:
pass
except IndexError:
pass
def setup(app):
import kivy
sys.path += [join(dirname(kivy.__file__), 'extras')]
from highlight import KivyLexer
if sphinx.version_info[0] >= 3:
app.add_lexer('kv', KivyLexer)
else:
app.add_lexer('kv', KivyLexer())
app.add_autodocumenter(CythonMethodDocumenter)
app.connect('autodoc-process-docstring', callback_docstring)
app.connect('autodoc-process-signature', callback_signature)
|