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 | |
---|
22 | from docutils import nodes |
---|
23 | |
---|
24 | from sphinx import addnodes |
---|
25 | from sphinx.roles import XRefRole |
---|
26 | from sphinx.locale import l_, _ |
---|
27 | from sphinx.directives import ObjectDescription |
---|
28 | from sphinx.domains import Domain, ObjType |
---|
29 | from sphinx.util.nodes import make_refnode |
---|
30 | from sphinx.util.docfields import Field, TypedField |
---|
31 | from sphinx.util.docstrings import prepare_docstring |
---|
32 | |
---|
33 | from sphinx.ext.autodoc import Documenter, bool_option |
---|
34 | from specmacrofileparser import SpecMacrofileParser |
---|
35 | |
---|
36 | |
---|
37 | # TODO: merge these with specmacrofileparser.py |
---|
38 | match_all = r'.*' |
---|
39 | non_greedy_filler = match_all + r'?' |
---|
40 | double_quote_string_match = r'("' + non_greedy_filler + r'")' |
---|
41 | word_match = r'((?:[a-z_]\w*))' |
---|
42 | cdef_match = r'(cdef)' |
---|
43 | |
---|
44 | |
---|
45 | spec_macro_sig_re = re.compile( |
---|
46 | r'''^ ([a-zA-Z_]\w*) # macro name |
---|
47 | ''', re.VERBOSE) |
---|
48 | |
---|
49 | spec_func_sig_re = re.compile(word_match + r'\(' |
---|
50 | + r'(' + match_all + r')' |
---|
51 | + r'\)', |
---|
52 | re.IGNORECASE|re.DOTALL) |
---|
53 | |
---|
54 | spec_cdef_name_sig_re = re.compile(double_quote_string_match, |
---|
55 | re.IGNORECASE|re.DOTALL) |
---|
56 | |
---|
57 | |
---|
58 | class SpecMacroDocumenter(Documenter): |
---|
59 | """ |
---|
60 | Document a SPEC macro source code file (autodoc.Documenter subclass) |
---|
61 | |
---|
62 | This code responds to the ReST file directive:: |
---|
63 | |
---|
64 | .. autospecmacro:: partial/path/name/somefile.mac |
---|
65 | :displayorder: fileorder |
---|
66 | |
---|
67 | The ``:displayorder`` parameter indicates how the |
---|
68 | contents will be sorted for appearance in the ReST document. |
---|
69 | |
---|
70 | **fileorder** or **file** |
---|
71 | Items will be documented in the order in |
---|
72 | which they appear in the ``.mac`` file. |
---|
73 | (Default) |
---|
74 | |
---|
75 | **alphabetical** or **alpha** |
---|
76 | Items will be documented in alphabetical order. |
---|
77 | (Not implemented at present.) |
---|
78 | |
---|
79 | .. tip:: |
---|
80 | A (near) future enhancement will provide for |
---|
81 | documenting all macro files in a directory, with optional |
---|
82 | recursion into subdirectories. By default, the code will |
---|
83 | only document files that match the glob pattern ``*.mac``. |
---|
84 | (This could be defined as a list in the ``conf.py`` file.) |
---|
85 | Such as:: |
---|
86 | |
---|
87 | .. spec:directory:: partial/path/name |
---|
88 | :recursion: |
---|
89 | :displayorder: alphabetical |
---|
90 | """ |
---|
91 | |
---|
92 | objtype = 'specmacro' |
---|
93 | member_order = 50 |
---|
94 | priority = 0 |
---|
95 | #: true if the generated content may contain titles |
---|
96 | titles_allowed = True |
---|
97 | |
---|
98 | option_spec = { |
---|
99 | 'displayorder': bool_option, |
---|
100 | } |
---|
101 | |
---|
102 | @classmethod |
---|
103 | def can_document_member(cls, member, membername, isattr, parent): |
---|
104 | # don't document submodules automatically |
---|
105 | #return isinstance(member, (FunctionType, BuiltinFunctionType)) |
---|
106 | r = membername in ('SpecMacroDocumenter', ) |
---|
107 | return r |
---|
108 | |
---|
109 | def generate(self, *args, **kw): |
---|
110 | """ |
---|
111 | Generate reST for the object given by *self.name*, and possibly for |
---|
112 | its members. |
---|
113 | """ |
---|
114 | # now, parse the SPEC macro file |
---|
115 | macrofile = self.parse_name() |
---|
116 | spec = SpecMacrofileParser(macrofile) |
---|
117 | extended_comment = spec.ReST() |
---|
118 | rest = prepare_docstring(extended_comment) |
---|
119 | |
---|
120 | #self.add_line(u'', '<autodoc>') |
---|
121 | #sig = self.format_signature() |
---|
122 | #self.add_directive_header(sig) |
---|
123 | |
---|
124 | self.add_line(u'', '<autodoc>') |
---|
125 | self.add_line(u'.. index:: SPEC macro file; %s' % macrofile, '<autodoc>') |
---|
126 | self.add_line(u'.. index:: !%s' % os.path.split(macrofile)[1], '<autodoc>') |
---|
127 | self.add_line(u'', '<autodoc>') |
---|
128 | self.add_line(u'', '<autodoc>') |
---|
129 | title = 'SPEC Macro File: %s' % macrofile |
---|
130 | self.add_line('@'*len(title), '<autodoc>') |
---|
131 | self.add_line(title, '<autodoc>') |
---|
132 | self.add_line('@'*len(title), '<autodoc>') |
---|
133 | self.add_line(u'', '<autodoc>') |
---|
134 | # TODO: provide links from each to highlighted source code blocks (like Python documenters). |
---|
135 | # This works for now. |
---|
136 | line = 'source code: :download:`%s <%s>`' % (macrofile, macrofile) |
---|
137 | self.add_line(line, macrofile) |
---|
138 | |
---|
139 | self.add_line(u'', '<autodoc>') |
---|
140 | for linenumber, line in enumerate(rest): |
---|
141 | self.add_line(line, macrofile, linenumber) |
---|
142 | |
---|
143 | #self.add_content(rest) |
---|
144 | #self.document_members(all_members) |
---|
145 | |
---|
146 | def resolve_name(self, modname, parents, path, base): |
---|
147 | if modname is not None: |
---|
148 | self.directive.warn('"::" in autospecmacro name doesn\'t make sense') |
---|
149 | return (path or '') + base, [] |
---|
150 | |
---|
151 | def parse_name(self): |
---|
152 | """Determine what file to parse. |
---|
153 | |
---|
154 | :returns: True if if parsing was successful |
---|
155 | """ |
---|
156 | ret = self.name |
---|
157 | self.fullname = os.path.abspath(ret) |
---|
158 | self.objpath, self.modname = os.path.split(self.fullname) |
---|
159 | self.args = None |
---|
160 | self.retann = None |
---|
161 | if self.args or self.retann: |
---|
162 | self.directive.warn('signature arguments or return annotation ' |
---|
163 | 'given for autospecmacro %s' % self.fullname) |
---|
164 | return ret |
---|
165 | |
---|
166 | |
---|
167 | class SpecDirDocumenter(Documenter): |
---|
168 | """ |
---|
169 | Document a directory containing SPEC macro source code files. |
---|
170 | |
---|
171 | This code responds to the ReST file directive:: |
---|
172 | |
---|
173 | .. autospecdir:: partial/path/name |
---|
174 | """ |
---|
175 | objtype = 'specdir' |
---|
176 | member_order = 50 |
---|
177 | priority = 0 |
---|
178 | #: true if the generated content may contain titles |
---|
179 | titles_allowed = True |
---|
180 | option_spec = { |
---|
181 | 'include_subdirs': bool_option, |
---|
182 | } |
---|
183 | |
---|
184 | @classmethod |
---|
185 | def can_document_member(cls, member, membername, isattr, parent): |
---|
186 | return membername in ('SpecDirDocumenter', ) |
---|
187 | |
---|
188 | def generate(self, *args, **kw): |
---|
189 | """ |
---|
190 | Look at the named directory and generate reST for the |
---|
191 | object given by *self.name*, and possibly for its members. |
---|
192 | """ |
---|
193 | # now, parse the .mac files in the SPEC directory |
---|
194 | specdir = self.name |
---|
195 | # self.add_line(u'', '<autodoc>') |
---|
196 | # self.add_line(u'directory:\n ``%s``' % specdir, '<autodoc>') |
---|
197 | if os.path.exists(specdir): |
---|
198 | for f in sorted(os.listdir(specdir)): |
---|
199 | filename = os.path.join(specdir, f) |
---|
200 | if os.path.isfile(filename) and filename.endswith('.mac'): |
---|
201 | # TODO: support a user choice for pattern match to the file name (glob v. re) |
---|
202 | # TODO: support the option to include subdirectories (include_subdirs) |
---|
203 | # TODO: do not add the same SPEC macro file more than once |
---|
204 | self.add_line(u'', '<autodoc>') |
---|
205 | self.add_line(u'.. autospecmacro:: %s' % filename, '<autodoc>') |
---|
206 | # TODO: any options? |
---|
207 | self.add_line(u'', '<autodoc>') |
---|
208 | # TODO: suppress delimiter after last file |
---|
209 | self.add_line(u'-'*15, '<autodoc>') # delimiter between files |
---|
210 | else: |
---|
211 | self.add_line(u'', '<autodoc>') |
---|
212 | self.add_line(u'Could not find directory: ``%s``' % specdir, '<autodoc>') |
---|
213 | |
---|
214 | |
---|
215 | |
---|
216 | class SpecMacroObject(ObjectDescription): |
---|
217 | """ |
---|
218 | Description of a SPEC macro definition |
---|
219 | """ |
---|
220 | |
---|
221 | doc_field_types = [ |
---|
222 | TypedField('parameter', label=l_('Parameters'), |
---|
223 | names=('param', 'parameter', 'arg', 'argument', |
---|
224 | 'keyword', 'kwarg', 'kwparam'), |
---|
225 | typerolename='def', typenames=('paramtype', 'type'), |
---|
226 | can_collapse=True), |
---|
227 | Field('returnvalue', label=l_('Returns'), has_arg=False, |
---|
228 | names=('returns', 'return')), |
---|
229 | Field('returntype', label=l_('Return type'), has_arg=False, |
---|
230 | names=('rtype',)), |
---|
231 | ] |
---|
232 | |
---|
233 | def add_target_and_index(self, name, sig, signode): |
---|
234 | targetname = '%s-%s' % (self.objtype, name) |
---|
235 | signode['ids'].append(targetname) |
---|
236 | self.state.document.note_explicit_target(signode) |
---|
237 | indextext = self._get_index_text(name) |
---|
238 | if indextext: |
---|
239 | self.indexnode['entries'].append(('single', indextext, targetname, '')) |
---|
240 | self.indexnode['entries'].append(('single', sig, targetname, '')) |
---|
241 | # TODO: what if there is more than one file, same name, different path? |
---|
242 | filename = os.path.split(signode.document.current_source)[1] |
---|
243 | if filename.endswith('.mac'): |
---|
244 | # TODO: change the match pattern with an option |
---|
245 | indextext = '%s; %s' % (filename, sig) |
---|
246 | self.indexnode['entries'].append(('single', indextext, targetname, '')) |
---|
247 | |
---|
248 | macro_types = { |
---|
249 | 'def': 'SPEC macro definition; %s', |
---|
250 | 'rdef': 'SPEC run-time macro definition; %s', |
---|
251 | 'cdef': 'SPEC chained macro definition; %s', |
---|
252 | } |
---|
253 | |
---|
254 | def _get_index_text(self, name): |
---|
255 | if self.objtype in self.macro_types: |
---|
256 | return _(self.macro_types[self.objtype]) % name |
---|
257 | else: |
---|
258 | return '' |
---|
259 | |
---|
260 | def handle_signature(self, sig, signode): |
---|
261 | '''return the name of this object from its signature''' |
---|
262 | # Must be able to match these (without preceding def or rdef) |
---|
263 | # def macro_name |
---|
264 | # def macro_name() |
---|
265 | # def macro_name(arg1, arg2) |
---|
266 | # rdef macro_name |
---|
267 | # cdef("macro_name", "content", "groupname", flags) |
---|
268 | m = spec_func_sig_re.match(sig) or spec_macro_sig_re.match(sig) |
---|
269 | if m is None: |
---|
270 | raise ValueError |
---|
271 | arglist = m.groups() |
---|
272 | name = arglist[0] |
---|
273 | args = [] |
---|
274 | if len(arglist) > 1: |
---|
275 | args = arglist[1:] |
---|
276 | if name == 'cdef': |
---|
277 | # TODO: need to match complete arg list |
---|
278 | # several different signatures are possible (see cdef-examples.mac) |
---|
279 | # for now, just get the macro name and ignore the arg list |
---|
280 | m = spec_cdef_name_sig_re.match(args[0]) |
---|
281 | arglist = m.groups() |
---|
282 | name = arglist[0].strip('"') |
---|
283 | args = ['<<< cdef argument list not handled yet >>>'] # FIXME: |
---|
284 | signode += addnodes.desc_name(name, name) |
---|
285 | if len(args) > 0: |
---|
286 | signode += addnodes.desc_addname(args, args) |
---|
287 | return name |
---|
288 | |
---|
289 | |
---|
290 | class SpecVariableObject(ObjectDescription): |
---|
291 | """ |
---|
292 | Description of a SPEC variable |
---|
293 | """ |
---|
294 | |
---|
295 | # TODO: The directive that declares the variable should be the primary (bold) index. |
---|
296 | # TODO: array variables are not handled at all |
---|
297 | # TODO: variables cited by *role* should link back to their *directive* declarations |
---|
298 | |
---|
299 | def handle_signature(self, sig, signode): |
---|
300 | '''return the name of this object from its signature''' |
---|
301 | # TODO: Should it match a regular expression? |
---|
302 | # TODO: What if global or local? |
---|
303 | signode += addnodes.desc_name(sig, sig) |
---|
304 | return sig |
---|
305 | |
---|
306 | def add_target_and_index(self, name, sig, signode): |
---|
307 | #text = u'! ' + sig # TODO: How to use emphasized index entry in this context? |
---|
308 | text = name.split()[0] # when sig = "tth #: scattering angle" |
---|
309 | targetname = '%s-%s' % (self.objtype, text) |
---|
310 | signode['ids'].append(targetname) |
---|
311 | # TODO: role does not point back to it yet |
---|
312 | # http://sphinx.pocoo.org/markup/misc.html#directive-index |
---|
313 | self.indexnode['entries'].append(('single', text, targetname, '')) |
---|
314 | text = u'SPEC %s variable; %s' % (self.objtype, sig) |
---|
315 | self.indexnode['entries'].append(('single', text, targetname, '')) |
---|
316 | |
---|
317 | class SpecXRefRole(XRefRole): |
---|
318 | """ """ |
---|
319 | |
---|
320 | def process_link(self, env, refnode, has_explicit_title, title, target): |
---|
321 | key = ":".join((refnode['refdomain'], refnode['reftype'])) |
---|
322 | refnode[key] = env.temp_data.get(key) # key was 'spec:def' |
---|
323 | if not has_explicit_title: |
---|
324 | title = title.lstrip(':') # only has a meaning for the target |
---|
325 | target = target.lstrip('~') # only has a meaning for the title |
---|
326 | # if the first character is a tilde, don't display the module/class |
---|
327 | # parts of the contents |
---|
328 | if title[0:1] == '~': |
---|
329 | title = title[1:] |
---|
330 | colon = title.rfind(':') |
---|
331 | if colon != -1: |
---|
332 | title = title[colon+1:] |
---|
333 | return title, target |
---|
334 | |
---|
335 | def result_nodes(self, document, env, node, is_ref): |
---|
336 | # this code adds index entries for each role instance |
---|
337 | if not is_ref: |
---|
338 | return [node], [] |
---|
339 | varname = node['reftarget'] |
---|
340 | tgtid = 'index-%s' % env.new_serialno('index') |
---|
341 | indexnode = addnodes.index() |
---|
342 | indexnode['entries'] = [ |
---|
343 | ('single', varname, tgtid, ''), |
---|
344 | #('single', _('environment variable; %s') % varname, tgtid, ''), |
---|
345 | ] |
---|
346 | targetnode = nodes.target('', '', ids=[tgtid]) |
---|
347 | document.note_explicit_target(targetnode) |
---|
348 | return [indexnode, targetnode, node], [] |
---|
349 | |
---|
350 | |
---|
351 | class SpecDomain(Domain): |
---|
352 | """SPEC language domain.""" |
---|
353 | |
---|
354 | name = 'spec' |
---|
355 | label = 'SPEC, http://www.certif.com' |
---|
356 | object_types = { # type of object that a domain can document |
---|
357 | 'def': ObjType(l_('def'), 'def'), |
---|
358 | 'rdef': ObjType(l_('rdef'), 'rdef'), |
---|
359 | 'cdef': ObjType(l_('cdef'), 'cdef'), |
---|
360 | 'global': ObjType(l_('global'), 'global'), |
---|
361 | 'local': ObjType(l_('local'), 'local'), |
---|
362 | 'constant': ObjType(l_('constant'), 'constant'), |
---|
363 | #'specmacro': ObjType(l_('specmacro'), 'specmacro'), |
---|
364 | } |
---|
365 | directives = { |
---|
366 | 'def': SpecMacroObject, |
---|
367 | 'rdef': SpecMacroObject, |
---|
368 | 'cdef': SpecMacroObject, |
---|
369 | 'global': SpecVariableObject, |
---|
370 | 'local': SpecVariableObject, |
---|
371 | 'constant': SpecVariableObject, |
---|
372 | } |
---|
373 | roles = { |
---|
374 | 'def' : SpecXRefRole(), |
---|
375 | 'rdef': SpecXRefRole(), |
---|
376 | 'cdef': SpecXRefRole(), |
---|
377 | 'global': SpecXRefRole(), |
---|
378 | 'local': SpecXRefRole(), |
---|
379 | 'constant': SpecXRefRole(), |
---|
380 | } |
---|
381 | initial_data = { |
---|
382 | 'objects': {}, # fullname -> docname, objtype |
---|
383 | } |
---|
384 | |
---|
385 | def clear_doc(self, docname): |
---|
386 | for (typ, name), doc in self.data['objects'].items(): |
---|
387 | if doc == docname: |
---|
388 | del self.data['objects'][typ, name] |
---|
389 | |
---|
390 | def resolve_xref(self, env, fromdocname, builder, typ, target, node, |
---|
391 | contnode): |
---|
392 | objects = self.data['objects'] |
---|
393 | objtypes = self.objtypes_for_role(typ) |
---|
394 | for objtype in objtypes: |
---|
395 | if (objtype, target) in objects: |
---|
396 | return make_refnode(builder, fromdocname, |
---|
397 | objects[objtype, target], |
---|
398 | objtype + '-' + target, |
---|
399 | contnode, target + ' ' + objtype) |
---|
400 | |
---|
401 | def get_objects(self): |
---|
402 | for (typ, name), docname in self.data['objects'].iteritems(): |
---|
403 | yield name, name, typ, docname, typ + '-' + name, 1 |
---|
404 | |
---|
405 | |
---|
406 | # http://sphinx.pocoo.org/ext/tutorial.html#the-setup-function |
---|
407 | |
---|
408 | def setup(app): |
---|
409 | app.add_domain(SpecDomain) |
---|
410 | app.add_autodocumenter(SpecMacroDocumenter) |
---|
411 | app.add_autodocumenter(SpecDirDocumenter) |
---|
412 | app.add_config_value('autospecmacrodir_process_subdirs', True, True) |
---|