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