Edgewall Software

ApiDocs: PythonDoc.py

File PythonDoc.py, 7.5 KB (added by cmlenz, 8 years ago)

Wiki macros used to generate the API docs

Line 
1# -*- coding: utf-8 -*-
2
3import compiler
4import posixpath
5import re
6import textwrap
7
8from trac.core import *
9from trac.util.html import html, Markup
10from trac.wiki.api import IWikiMacroProvider
11
12try:
13    from docutils.core import publish_parts
14except ImportError:
15    def publish_parts(text, **kwargs):
16        return {'body': '<pre>%s</pre>' % text}
17
18
19class PythonDocMacro(Component):
20    """Renders Python API documentation using the PythonDoc library.
21
22    Usage:
23    {{{
24        [[PythonDoc(path/in/repos)]]
25    }}}
26    """
27
28    implements(IWikiMacroProvider)
29
30    class _BaseMacro(object):
31
32        def __init__(self, modname, modpath, content, href):
33            self.modname = modname
34            self.modpath = modpath
35            self.content = content
36            self.href = href
37
38            self.ast = compiler.parse(self.content, 'exec')
39            self._depth = 0
40
41        def _dump_node(self, node, parents):
42            attrname = '_dump_%s' % node.__class__.__name__.lower()
43            if hasattr(self, attrname):
44                return getattr(self, attrname)(node, parents)
45            return []
46
47        def _dump_stmt(self, node, parents):
48            for child in node.getChildNodes():
49                for output in self._dump_node(child, parents + [node]):
50                    yield output
51
52        def _generate_id(self, node, parents):
53            buf = [parent.name for parent in parents if hasattr(parent, 'name')]
54            return ':'.join([self.modname] + buf + [node.name])
55
56
57    class _PythonDoc(_BaseMacro):
58
59        def render(self):
60            return html.DL(class_='pydoc')(
61                html.DT(class_='module')(html.H2(
62                    html.A(self.modname, href=self.href.browser(self.modpath))
63                )),
64                html.DD(class_='module')(self._format_docstring(self.ast.doc)),
65                html.DD(class_='module')(
66                    [self._dump_node(node, [self.ast])
67                     for node in self.ast.getChildNodes()]
68                )
69            )
70
71        def _dump_class(self, node, parents):
72            if node.lineno:
73                label = html.A(node.name, href=self.href.browser(self.modpath) +
74                                               '#L%d' % node.lineno)
75            else:
76                label = node.name
77
78            Hn = getattr(html, 'H%d' % min(self._depth + 3, 6))
79            yield html.DT(class_='class')(
80                Hn(id_=self._generate_id(node, parents))(label)
81            )
82            yield html.DD(self._format_docstring(node.doc))
83            if node.getChildNodes():
84                self._depth += 1
85                yield html.DD(html.DL(
86                    [self._dump_node(child, parents + [node])
87                     for child in node.getChildNodes()]
88                ))
89                self._depth -= 1
90
91        def _dump_function(self, node, parents):
92            if node.name.startswith('_'):
93                return
94
95            def _render_args():
96                varargs_offset = kwargs_offset = len(node.argnames)
97                if node.varargs:
98                    varargs_offset -= 1
99                    if node.kwargs:
100                        kwargs_offset -= 1
101                if node.kwargs:
102                    kwargs_offset -= 1
103                defaults_offset = min(varargs_offset, kwargs_offset) - len(node.defaults)
104
105                for idx, argname in enumerate(node.argnames):
106                    if idx > 0:
107                        yield ', '
108                    if idx == varargs_offset:
109                        yield '*'
110                    elif idx == kwargs_offset:
111                        yield '**'
112                    yield argname
113                    if idx >= defaults_offset and idx < min(varargs_offset, kwargs_offset):
114                        yield '='
115                        val = node.defaults[idx - defaults_offset]
116                        if hasattr(val, 'value'):
117                            val = repr(val.value)
118                        yield val
119
120            if node.lineno:
121                label = html.A(node.name, href=self.href.browser(self.modpath) +
122                                               '#L%d' % node.lineno)
123            else:
124                label = node.name
125
126            Hn = getattr(html, 'H%d' % min(self._depth + 2, 6))
127            yield html.DT(class_='function')(
128                Hn(id_=self._generate_id(node, parents))(
129                    html.CODE(html.B(label), '(', _render_args(), ')')
130                )
131            )
132            yield html.DD(self._format_docstring(node.doc))
133
134        def _format_docstring(self, docstring):
135            if not docstring:
136                return html.P(html.EM('(Not documented)'))
137            text = docstring.expandtabs()
138
139            if '\n' in text: # Fix indentation
140                indent = 0
141                for line in text.split('\n'):
142                    content = line.lstrip()
143                    indent = len(line) - len(content)
144                if indent:
145                    text = ' ' * indent + text
146                    text = textwrap.dedent(text)
147
148            return Markup(publish_parts(text, writer_name='html')['body'])
149
150
151    class _PythonOutline(_BaseMacro):
152
153        def render(self):
154            return html.DIV(class_='wiki-toc')(
155                html.OL([self._dump_node(node, [self.ast])
156                         for node in self.ast.getChildNodes()])
157            )
158
159        def _dump_class(self, node, parents):
160            def _render_children():
161                children = [self._dump_node(child, parents + [node])
162                            for child in node.getChildNodes()]
163                if children:
164                    yield html.OL(children)
165            yield html.LI(class_='class')(
166                html.A(href='#' + self._generate_id(node, parents),
167                       title=self._get_summary(node.doc))(node.name),
168                _render_children()
169            )
170
171        def _dump_function(self, node, parents):
172            if node.name.startswith('_'):
173                return
174
175            yield html.LI(class_='function')(
176                html.A(href='#' + self._generate_id(node, parents))(node.name)
177            )
178
179        def _get_summary(self, docstring):
180            if not docstring:
181                return None
182            text = docstring.expandtabs()
183
184            if '\n' in text: # Fix indentation
185                indent = 0
186                for line in text.split('\n'):
187                    content = line.lstrip()
188                    indent = len(line) - len(content)
189                if indent:
190                    text = ' ' * indent + text
191                    text = textwrap.dedent(text)
192
193            lines = text.strip().split('\n\n', 1)
194            if len(lines) > 1:
195                summary = lines[0]
196            else:
197                summary = text
198
199            return re.sub(r'\s{2,}', r'\s', summary.replace('\n', ' '))
200
201    # IWikiMacroProvider methods
202
203    def get_macros(self):
204        yield 'PythonDoc'
205        yield 'PythonOutline'
206
207    def get_macro_description(self, name):
208        """Return the subclass's docstring."""
209        return inspect.getdoc(getattr(self, '_' + name))
210
211    def render_macro(self, req, name, content):
212        basepath, modname = [x.strip() for x in content.split(',', 1)]
213        modpath = posixpath.join(basepath, modname.replace('.', '/')) + '.py'
214
215        repos = self.env.get_repository()
216        node = repos.get_node(modpath)
217        content = node.get_content().read()
218
219        instance = getattr(self, '_' + name)(modname, modpath, content, req.href)
220        return instance.render()