PythonDoc Trac macro

27 June 2008
13:42

By cmlenz as Python

Trac macro that pulls a Python file from the repository, and uses compiler.ast to pull out API information

1 # -*- coding: utf-8 -*-
2
3 import compiler
4 import inspect
5 import posixpath
6 import re
7 import textwrap
8
9 from genshi import Markup
10 from genshi.builder import tag
11 from trac.core import *
12 from trac.wiki.api import IWikiMacroProvider
13
14 try:
15 from docutils.core import publish_parts
16 except ImportError:
17 def publish_parts(text, **kwargs):
18 return {'body': '<pre>%s</pre>' % text}
19
20
21 class PythonDocMacro(Component):
22 """Renders Python API documentation using the PythonDoc library.
23
24 Usage:
25 {{{
26 [[PythonDoc(path/in/repos)]]
27 }}}
28 """
29
30 implements(IWikiMacroProvider)
31
32 class _BaseMacro(object):
33
34 def __init__(self, modname, modpath, content, href):
35 self.modname = modname
36 self.modpath = modpath
37 self.content = content
38 self.href = href
39
40 self.ast = compiler.parse(self.content, 'exec')
41 self._depth = 0
42
43 def _dump_node(self, node, parents):
44 attrname = '_dump_%s' % node.__class__.__name__.lower()
45 if hasattr(self, attrname):
46 return getattr(self, attrname)(node, parents)
47 return []
48
49 def _dump_stmt(self, node, parents):
50 for child in node.getChildNodes():
51 for output in self._dump_node(child, parents + [node]):
52 yield output
53
54 def _generate_id(self, node, parents):
55 buf = [parent.name for parent in parents if hasattr(parent, 'name')]
56 return ':'.join([self.modname] + buf + [node.name])
57
58
59 class _PythonDoc(_BaseMacro):
60
61 def render(self):
62 return tag.dl(class_='pydoc')(
63 tag.dt(class_='module')(tag.h2(
64 tag.a(tag.code(self.modname), href=self.href.browser(self.modpath))
65 )),
66 tag.dd(class_='module')(self._format_docstring(self.ast.doc)),
67 tag.dd(class_='module')(
68 [self._dump_node(node, [self.ast])
69 for node in self.ast.getChildNodes()]
70 )
71 )
72
73 def _dump_class(self, node, parents):
74 if node.lineno:
75 label = tag.a(node.name, href=self.href.browser(self.modpath) + '#L%d' % node.lineno)
76 else:
77 label = node.name
78
79 Hn = getattr(tag, 'h%d' % min(self._depth + 3, 6))
80 yield tag.dt(class_='class')(
81 Hn(id_=self._generate_id(node, parents))(tag.code(label))
82 )
83 yield tag.dd(self._format_docstring(node.doc))
84 if node.getChildNodes():
85 self._depth += 1
86 yield tag.dd(tag.dl(
87 [self._dump_node(child, parents + [node])
88 for child in node.getChildNodes()]
89 ))
90 self._depth -= 1
91
92 def _dump_function(self, node, parents):
93 if node.name.startswith('_'):
94 return
95
96 def _render_args():
97 varargs_offset = kwargs_offset = len(node.argnames)
98 if node.varargs:
99 varargs_offset -= 1
100 if node.kwargs:
101 kwargs_offset -= 1
102 if node.kwargs:
103 kwargs_offset -= 1
104 defaults_offset = min(varargs_offset, kwargs_offset) - len(node.defaults)
105
106 for idx, argname in enumerate(node.argnames):
107 if idx > 0:
108 yield ', '
109 if idx == varargs_offset:
110 yield '*'
111 elif idx == kwargs_offset:
112 yield '**'
113 yield argname
114 if idx >= defaults_offset and idx < min(varargs_offset, kwargs_offset):
115 yield '='
116 val = node.defaults[idx - defaults_offset]
117 if hasattr(val, 'value'):
118 val = repr(val.value)
119 yield val
120
121 if node.lineno:
122 label = tag.a(node.name, href=self.href.browser(self.modpath) + '#L%d' % node.lineno)
123 else:
124 label = node.name
125
126 Hn = getattr(tag, 'h%d' % min(self._depth + 2, 6))
127 yield tag.dt(class_='function')(
128 Hn(id_=self._generate_id(node, parents))(
129 tag.code(tag.b(label), '(', _render_args(), ')')
130 )
131 )
132 yield tag.dd(self._format_docstring(node.doc))
133
134 def _format_docstring(self, docstring):
135 if not docstring:
136 return tag.p(tag.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 tag.div(class_='wiki-toc')(
155 tag.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 tag.ol(children)
165 yield tag.li(class_='class')(
166 tag.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 tag.li(class_='function')(
176 tag.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()
221

Download Raw Source

Comments

No comments so far.