1 | # -*- coding: utf-8 -*- |
---|
2 | """ |
---|
3 | sphinxcontrib.specdomain |
---|
4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
---|
5 | |
---|
6 | :synopsis: SPEC domain for Sphinx |
---|
7 | |
---|
8 | Automatically insert ReST-formatted extended comments |
---|
9 | from SPEC files for macro definitions and variable declarations into |
---|
10 | the Sphinx doctree, thus avoiding duplication between docstrings and documentation |
---|
11 | for those who like elaborate docstrings. |
---|
12 | |
---|
13 | :copyright: Copyright 2012 by BCDA, Advanced Photon Source, Argonne National Laboratory |
---|
14 | :license: ANL Open Source License, see LICENSE for details. |
---|
15 | """ |
---|
16 | |
---|
17 | # http://sphinx.pocoo.org/ext/appapi.html |
---|
18 | |
---|
19 | import os |
---|
20 | import re |
---|
21 | import string #@UnusedImport |
---|
22 | |
---|
23 | from docutils import nodes #@UnusedImport |
---|
24 | from docutils.parsers.rst import directives #@UnusedImport |
---|
25 | |
---|
26 | from sphinx import addnodes |
---|
27 | from sphinx.roles import XRefRole |
---|
28 | from sphinx.locale import l_, _ #@UnusedImport |
---|
29 | from sphinx.directives import ObjectDescription |
---|
30 | from sphinx.domains import Domain, ObjType, Index #@UnusedImport |
---|
31 | from sphinx.util.compat import Directive #@UnusedImport |
---|
32 | from sphinx.util.nodes import make_refnode, nested_parse_with_titles |
---|
33 | from sphinx.util.docfields import Field, TypedField |
---|
34 | from sphinx.util.docstrings import prepare_docstring #@UnusedImport |
---|
35 | |
---|
36 | from docutils.statemachine import ViewList, string2lines |
---|
37 | import sphinx.util.nodes |
---|
38 | |
---|
39 | |
---|
40 | match_all = r'.*' |
---|
41 | non_greedy_filler = match_all + r'?' |
---|
42 | double_quote_string_match = r'("' + non_greedy_filler + r'")' |
---|
43 | word_match = r'((?:[a-z_]\w*))' |
---|
44 | cdef_match = r'(cdef)' |
---|
45 | extended_comment_flag = r'\"\"\"' |
---|
46 | |
---|
47 | |
---|
48 | spec_macro_sig_re = re.compile( |
---|
49 | r'''^ ([a-zA-Z_]\w*) # macro name |
---|
50 | ''', re.VERBOSE) |
---|
51 | |
---|
52 | spec_func_sig_re = re.compile(word_match + r'\(' |
---|
53 | + r'(' + match_all + r')' |
---|
54 | + r'\)', |
---|
55 | re.IGNORECASE|re.DOTALL) |
---|
56 | |
---|
57 | spec_cdef_name_sig_re = re.compile(double_quote_string_match, |
---|
58 | re.IGNORECASE|re.DOTALL) |
---|
59 | |
---|
60 | |
---|
61 | spec_extended_comment_flag_sig_re = re.compile(extended_comment_flag, |
---|
62 | re.IGNORECASE|re.DOTALL) |
---|
63 | spec_extended_comment_start_sig_re = re.compile(r'^' |
---|
64 | + non_greedy_filler |
---|
65 | + extended_comment_flag, |
---|
66 | re.IGNORECASE|re.DOTALL) |
---|
67 | spec_extended_comment_block_sig_re = re.compile(r'^' |
---|
68 | + non_greedy_filler |
---|
69 | + extended_comment_flag |
---|
70 | + r'(' + non_greedy_filler + r')' |
---|
71 | + extended_comment_flag |
---|
72 | + non_greedy_filler |
---|
73 | + r'$', |
---|
74 | re.IGNORECASE|re.DOTALL|re.MULTILINE) |
---|
75 | |
---|
76 | |
---|
77 | class SpecMacroObject(ObjectDescription): |
---|
78 | """ |
---|
79 | Description of a SPEC macro definition |
---|
80 | """ |
---|
81 | |
---|
82 | doc_field_types = [ |
---|
83 | TypedField('parameter', label=l_('Parameters'), |
---|
84 | names=('param', 'parameter', 'arg', 'argument', |
---|
85 | 'keyword', 'kwarg', 'kwparam'), |
---|
86 | typerolename='def', typenames=('paramtype', 'type'), |
---|
87 | can_collapse=True), |
---|
88 | Field('returnvalue', label=l_('Returns'), has_arg=False, |
---|
89 | names=('returns', 'return')), |
---|
90 | Field('returntype', label=l_('Return type'), has_arg=False, |
---|
91 | names=('rtype',)), |
---|
92 | ] |
---|
93 | |
---|
94 | def add_target_and_index(self, name, sig, signode): |
---|
95 | targetname = '%s-%s' % (self.objtype, name) |
---|
96 | signode['ids'].append(targetname) |
---|
97 | self.state.document.note_explicit_target(signode) |
---|
98 | indextext = self._get_index_text(name) |
---|
99 | if indextext: |
---|
100 | self.indexnode['entries'].append(('single', indextext, |
---|
101 | targetname, '')) |
---|
102 | |
---|
103 | def _get_index_text(self, name): |
---|
104 | macro_types = { |
---|
105 | 'def': 'SPEC macro definition; %s', |
---|
106 | 'rdef': 'SPEC run-time macro definition; %s', |
---|
107 | 'cdef': 'SPEC chained macro definition; %s', |
---|
108 | } |
---|
109 | if self.objtype in macro_types: |
---|
110 | return _(macro_types[self.objtype]) % name |
---|
111 | else: |
---|
112 | return '' |
---|
113 | |
---|
114 | def handle_signature(self, sig, signode): |
---|
115 | # Must be able to match these (without preceding def or rdef) |
---|
116 | # def macro_name |
---|
117 | # def macro_name() |
---|
118 | # def macro_name(arg1, arg2) |
---|
119 | # rdef macro_name |
---|
120 | # cdef("macro_name", "content", "groupname", flags) |
---|
121 | m = spec_func_sig_re.match(sig) or spec_macro_sig_re.match(sig) |
---|
122 | if m is None: |
---|
123 | raise ValueError |
---|
124 | arglist = m.groups() |
---|
125 | name = arglist[0] |
---|
126 | args = [] |
---|
127 | if len(arglist) > 1: |
---|
128 | args = arglist[1:] |
---|
129 | if name == 'cdef': |
---|
130 | # TODO: need to match complete arg list |
---|
131 | # several different signatures are possible (see cdef-examples.mac) |
---|
132 | # for now, just get the macro name and ignore the arg list |
---|
133 | m = spec_cdef_name_sig_re.match(args[0]) |
---|
134 | arglist = m.groups() |
---|
135 | name = arglist[0].strip('"') |
---|
136 | args = ['<<< cdef argument list not handled yet >>>'] # FIXME: |
---|
137 | signode += addnodes.desc_name(name, name) |
---|
138 | if len(args) > 0: |
---|
139 | signode += addnodes.desc_addname(args, args) |
---|
140 | return name |
---|
141 | |
---|
142 | |
---|
143 | class SpecVariableObject(ObjectDescription): |
---|
144 | """ |
---|
145 | Description of a SPEC variable |
---|
146 | """ |
---|
147 | |
---|
148 | |
---|
149 | class SpecMacroSourceObject(ObjectDescription): |
---|
150 | """ |
---|
151 | Document a SPEC macro source code file |
---|
152 | |
---|
153 | This code responds to the ReST file directive:: |
---|
154 | |
---|
155 | .. spec:macrofile:: partial/path/name/somefile.mac |
---|
156 | :displayorder: fileorder |
---|
157 | |
---|
158 | The ``:displayorder`` parameter indicates how the |
---|
159 | contents will be sorted for appearance in the ReST document. |
---|
160 | |
---|
161 | **fileorder**, **file** |
---|
162 | Items will be documented in the order in |
---|
163 | which they appear in the ``.mac`` file. |
---|
164 | |
---|
165 | **alphabetical**, **alpha** |
---|
166 | Items will be documented in alphabetical order. |
---|
167 | |
---|
168 | A (near) future enhancement would be to provide for |
---|
169 | documenting all macro files in a directory, with optional |
---|
170 | recursion into subdirectories. By default, the code would |
---|
171 | only document files that match the glob pattern ``*.mac``. |
---|
172 | Such as:: |
---|
173 | |
---|
174 | .. spec:directory:: partial/path/name |
---|
175 | :recursion: |
---|
176 | :displayorder: alphabetical |
---|
177 | """ |
---|
178 | |
---|
179 | # TODO: work-in-progress |
---|
180 | |
---|
181 | doc_field_types = [ |
---|
182 | Field('displayorder', label=l_('Display order'), has_arg=False, |
---|
183 | names=('displayorder', 'synonym')), |
---|
184 | ] |
---|
185 | |
---|
186 | def add_target_and_index(self, name, sig, signode): |
---|
187 | targetname = '%s-%s' % (self.objtype, name) |
---|
188 | signode['ids'].append(targetname) |
---|
189 | self.state.document.note_explicit_target(signode) |
---|
190 | indextext = sig |
---|
191 | if indextext: |
---|
192 | self.indexnode['entries'].append(('single', indextext, |
---|
193 | targetname, '')) |
---|
194 | |
---|
195 | def handle_signature(self, sig, signode): |
---|
196 | signode += addnodes.desc_name(sig, sig) |
---|
197 | # TODO: this is the place to parse the SPEC macro source code file named in "sig" |
---|
198 | ''' |
---|
199 | Since 2002, SPEC has allowed for triple-quoted strings as extended comments. |
---|
200 | Few, if any, have used them. |
---|
201 | Assume that they will contain ReST formatted comments. |
---|
202 | The first, simplest thing to do is to read the .mac file and only extract |
---|
203 | all the extended comments and add them as nodes to the current document. |
---|
204 | |
---|
205 | An additional step would be to parse for def, cdef, rdef, global, local, const, ... |
---|
206 | Another step would be to attach source code and provide links from each to |
---|
207 | highlighted source code blocks. |
---|
208 | ''' |
---|
209 | extended_comments_list = self.parse_macro_file(sig) |
---|
210 | view = ViewList([u'TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up']) |
---|
211 | #contentnode = nodes.TextElement() |
---|
212 | node = nodes.paragraph() |
---|
213 | node.document = self.state.document |
---|
214 | self.state.nested_parse(view, 0, signode) |
---|
215 | # TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up |
---|
216 | # nodes.TextElement(raw, text) |
---|
217 | # sphinx.directives.__init__.py ObjectDescription.run() method |
---|
218 | # Summary: This does not belong here, in the signature processing part. |
---|
219 | # Instead, it goes at the directive.run() method. Where's that here? |
---|
220 | # for extended_comment in extended_comments_list: |
---|
221 | # for line in string2lines(extended_comment): |
---|
222 | # view = ViewList([line]) |
---|
223 | # nested_parse_with_titles(self.state, view, signode) |
---|
224 | return sig |
---|
225 | |
---|
226 | def XX_run(self): |
---|
227 | # TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up |
---|
228 | # nodes.TextElement(raw, text) |
---|
229 | # sphinx.directives.__init__.py ObjectDescription.run() method |
---|
230 | # Summary: This does not belong here, in the signature processing part. |
---|
231 | # Instead, it goes at the directive.run() method. This is the new place! |
---|
232 | pass |
---|
233 | |
---|
234 | def parse_macro_file(self, filename): |
---|
235 | """ |
---|
236 | parse the SPEC macro file and return the ReST blocks |
---|
237 | |
---|
238 | :param str filename: name (with optional path) of SPEC macro file |
---|
239 | (The path is relative to the ``.rst`` document.) |
---|
240 | :returns [str]: list of ReST-formatted extended comment blocks (docstrings) from SPEC macro file. |
---|
241 | |
---|
242 | [future] parse more stuff as planned, this is very simplistic for now |
---|
243 | """ |
---|
244 | results = [] |
---|
245 | if not os.path.exists(filename): |
---|
246 | raise RuntimeError, "could not find: " + filename |
---|
247 | |
---|
248 | buf = open(filename, 'r').read() |
---|
249 | #n = len(buf) |
---|
250 | for node in spec_extended_comment_block_sig_re.finditer(buf): |
---|
251 | #g = node.group() |
---|
252 | #gs = node.groups() |
---|
253 | #s = node.start() |
---|
254 | #e = node.end() |
---|
255 | #t = buf[s:e] |
---|
256 | results.append(node.groups()[0]) # TODO: can we get line number also? |
---|
257 | return results |
---|
258 | |
---|
259 | |
---|
260 | class SpecXRefRole(XRefRole): |
---|
261 | """ """ |
---|
262 | |
---|
263 | def process_link(self, env, refnode, has_explicit_title, title, target): |
---|
264 | key = ":".join((refnode['refdomain'], refnode['reftype'])) |
---|
265 | refnode[key] = env.temp_data.get(key) # key was 'spec:def' |
---|
266 | if not has_explicit_title: |
---|
267 | title = title.lstrip(':') # only has a meaning for the target |
---|
268 | target = target.lstrip('~') # only has a meaning for the title |
---|
269 | # if the first character is a tilde, don't display the module/class |
---|
270 | # parts of the contents |
---|
271 | if title[0:1] == '~': |
---|
272 | title = title[1:] |
---|
273 | colon = title.rfind(':') |
---|
274 | if colon != -1: |
---|
275 | title = title[colon+1:] |
---|
276 | return title, target |
---|
277 | |
---|
278 | def result_nodes(self, document, env, node, is_ref): |
---|
279 | # this code adds index entries for each role instance |
---|
280 | if not is_ref: |
---|
281 | return [node], [] |
---|
282 | varname = node['reftarget'] |
---|
283 | tgtid = 'index-%s' % env.new_serialno('index') |
---|
284 | indexnode = addnodes.index() |
---|
285 | indexnode['entries'] = [ |
---|
286 | ('single', varname, tgtid, ''), |
---|
287 | #('single', _('environment variable; %s') % varname, tgtid, ''), |
---|
288 | ] |
---|
289 | targetnode = nodes.target('', '', ids=[tgtid]) |
---|
290 | document.note_explicit_target(targetnode) |
---|
291 | return [indexnode, targetnode, node], [] |
---|
292 | |
---|
293 | |
---|
294 | class SpecDomain(Domain): |
---|
295 | """SPEC language domain.""" |
---|
296 | |
---|
297 | name = 'spec' |
---|
298 | label = 'SPEC, http://www.certif.com' |
---|
299 | object_types = { # type of object that a domain can document |
---|
300 | 'def': ObjType(l_('def'), 'def'), |
---|
301 | 'rdef': ObjType(l_('rdef'), 'rdef'), |
---|
302 | 'cdef': ObjType(l_('cdef'), 'cdef'), |
---|
303 | 'global': ObjType(l_('global'), 'global'), |
---|
304 | 'local': ObjType(l_('local'), 'local'), |
---|
305 | } |
---|
306 | directives = { |
---|
307 | 'def': SpecMacroObject, |
---|
308 | 'rdef': SpecMacroObject, |
---|
309 | 'cdef': SpecMacroObject, |
---|
310 | 'global': SpecVariableObject, |
---|
311 | 'local': SpecVariableObject, |
---|
312 | 'macrofile': SpecMacroSourceObject, |
---|
313 | } |
---|
314 | roles = { |
---|
315 | 'def' : SpecXRefRole(), |
---|
316 | 'rdef': SpecXRefRole(), |
---|
317 | 'cdef': SpecXRefRole(), |
---|
318 | 'global': SpecXRefRole(), |
---|
319 | 'local': SpecXRefRole(), |
---|
320 | } |
---|
321 | initial_data = { |
---|
322 | 'objects': {}, # fullname -> docname, objtype |
---|
323 | } |
---|
324 | |
---|
325 | def clear_doc(self, docname): |
---|
326 | for (typ, name), doc in self.data['objects'].items(): |
---|
327 | if doc == docname: |
---|
328 | del self.data['objects'][typ, name] |
---|
329 | |
---|
330 | def resolve_xref(self, env, fromdocname, builder, typ, target, node, |
---|
331 | contnode): |
---|
332 | objects = self.data['objects'] |
---|
333 | objtypes = self.objtypes_for_role(typ) |
---|
334 | for objtype in objtypes: |
---|
335 | if (objtype, target) in objects: |
---|
336 | return make_refnode(builder, fromdocname, |
---|
337 | objects[objtype, target], |
---|
338 | objtype + '-' + target, |
---|
339 | contnode, target + ' ' + objtype) |
---|
340 | |
---|
341 | def get_objects(self): |
---|
342 | for (typ, name), docname in self.data['objects'].iteritems(): |
---|
343 | yield name, name, typ, docname, typ + '-' + name, 1 |
---|
344 | |
---|
345 | |
---|
346 | # http://sphinx.pocoo.org/ext/tutorial.html#the-setup-function |
---|
347 | |
---|
348 | def setup(app): |
---|
349 | app.add_domain(SpecDomain) |
---|
350 | # http://sphinx.pocoo.org/ext/appapi.html#sphinx.domains.Domain |
---|