1 | # -*- coding: utf-8 -*- |
---|
2 | """ |
---|
3 | sphinxcontrib.specdomain |
---|
4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
---|
5 | |
---|
6 | SPEC domain. |
---|
7 | |
---|
8 | :copyright: Copyright 2012 by Pete Jemian |
---|
9 | :license: BSD, see LICENSE for details. |
---|
10 | """ |
---|
11 | |
---|
12 | # http://sphinx.pocoo.org/ext/appapi.html |
---|
13 | |
---|
14 | import re |
---|
15 | import string |
---|
16 | |
---|
17 | from docutils import nodes |
---|
18 | from docutils.parsers.rst import directives |
---|
19 | |
---|
20 | from sphinx import addnodes |
---|
21 | from sphinx.roles import XRefRole |
---|
22 | from sphinx.locale import l_, _ |
---|
23 | from sphinx.directives import ObjectDescription |
---|
24 | from sphinx.domains import Domain, ObjType, Index |
---|
25 | from sphinx.util.compat import Directive |
---|
26 | from sphinx.util.nodes import make_refnode |
---|
27 | from sphinx.util.docfields import Field, TypedField |
---|
28 | |
---|
29 | |
---|
30 | # TODO: tailor these for SPEC |
---|
31 | # RE to split at word boundaries |
---|
32 | wsplit_re = re.compile(r'(\W+)') |
---|
33 | |
---|
34 | # http://www.greenend.org.uk/rjk/tech/regexp.html |
---|
35 | |
---|
36 | # REs for SPEC signatures |
---|
37 | spec_func_sig_re = re.compile( |
---|
38 | r'''^ ([\w.]*:)? # module name |
---|
39 | (\w+) \s* # thing name |
---|
40 | (?: \((.*)\) # optional: arguments |
---|
41 | (?:\s* -> \s* (.*))? # return annotation |
---|
42 | )? $ # and nothing more |
---|
43 | ''', re.VERBOSE) |
---|
44 | |
---|
45 | |
---|
46 | spec_macro_sig_re = re.compile( |
---|
47 | r'''^ ([\w.]*:)? # module name |
---|
48 | ([A-Z]\w+) $ # thing name |
---|
49 | ''', re.VERBOSE) |
---|
50 | |
---|
51 | spec_global_sig_re = re.compile( |
---|
52 | r'''^ ([\w.]*:)? # module name |
---|
53 | ([A-Z]\w+) $ # thing name |
---|
54 | ''', re.VERBOSE) |
---|
55 | |
---|
56 | |
---|
57 | spec_var_sig_re = re.compile( |
---|
58 | r'''^ ([\w.]*:)? # module name |
---|
59 | ([A-Z]\w+) $ # thing name |
---|
60 | ''', re.VERBOSE) |
---|
61 | |
---|
62 | |
---|
63 | spec_record_sig_re = re.compile( |
---|
64 | r'''^ ([\w.]*:)? # module name |
---|
65 | (\#\w+) $ # thing name |
---|
66 | ''', re.VERBOSE) |
---|
67 | |
---|
68 | |
---|
69 | spec_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ',' |
---|
70 | |
---|
71 | |
---|
72 | class SpecObject(ObjectDescription): |
---|
73 | """ |
---|
74 | Description of a SPEC language object. |
---|
75 | """ |
---|
76 | doc_field_types = [ |
---|
77 | TypedField('parameter', label=l_('Parameters'), |
---|
78 | names=('param', 'parameter', 'arg', 'argument'), |
---|
79 | typerolename='type', typenames=('type',)), |
---|
80 | Field('returnvalue', label=l_('Returns'), has_arg=False, |
---|
81 | names=('returns', 'return')), |
---|
82 | Field('returntype', label=l_('Return type'), has_arg=False, |
---|
83 | names=('rtype',)), |
---|
84 | ] |
---|
85 | |
---|
86 | def _add_signature_prefix(self, signode): |
---|
87 | if self.objtype != 'function': |
---|
88 | sig_prefix = self.objtype + ' ' |
---|
89 | signode += addnodes.desc_annotation(sig_prefix, sig_prefix) |
---|
90 | |
---|
91 | def needs_arglist(self): |
---|
92 | return self.objtype == 'function' |
---|
93 | |
---|
94 | def handle_signature(self, sig, signode): |
---|
95 | if sig.startswith('#'): |
---|
96 | return self._handle_record_signature(sig, signode) |
---|
97 | elif sig[0].isupper(): |
---|
98 | return self._handle_macro_signature(sig, signode) |
---|
99 | return self._handle_function_signature(sig, signode) |
---|
100 | |
---|
101 | def _resolve_module_name(self, signode, modname, name): |
---|
102 | # determine module name, as well as full name |
---|
103 | env_modname = self.options.get( |
---|
104 | 'module', self.env.temp_data.get('spec:module', 'spec')) |
---|
105 | if modname: |
---|
106 | fullname = modname + name |
---|
107 | signode['module'] = modname[:-1] |
---|
108 | else: |
---|
109 | fullname = env_modname + ':' + name |
---|
110 | signode['module'] = env_modname |
---|
111 | signode['fullname'] = fullname |
---|
112 | self._add_signature_prefix(signode) |
---|
113 | name_prefix = signode['module'] + ':' |
---|
114 | signode += addnodes.desc_addname(name_prefix, name_prefix) |
---|
115 | signode += addnodes.desc_name(name, name) |
---|
116 | return fullname |
---|
117 | |
---|
118 | def _handle_record_signature(self, sig, signode): |
---|
119 | m = spec_record_sig_re.match(sig) |
---|
120 | if m is None: |
---|
121 | raise ValueError |
---|
122 | modname, name = m.groups() |
---|
123 | return self._resolve_module_name(signode, modname, name) |
---|
124 | |
---|
125 | def _handle_macro_signature(self, sig, signode): |
---|
126 | m = spec_macro_sig_re.match(sig) |
---|
127 | if m is None: |
---|
128 | raise ValueError |
---|
129 | modname, name = m.groups() |
---|
130 | return self._resolve_module_name(signode, modname, name) |
---|
131 | |
---|
132 | def _handle_function_signature(self, sig, signode): |
---|
133 | m = spec_func_sig_re.match(sig) |
---|
134 | if m is None: |
---|
135 | raise ValueError |
---|
136 | modname, name, arglist, retann = m.groups() |
---|
137 | |
---|
138 | fullname = self._resolve_module_name(signode, modname, name) |
---|
139 | |
---|
140 | if not arglist: |
---|
141 | if self.needs_arglist(): |
---|
142 | # for callables, add an empty parameter list |
---|
143 | signode += addnodes.desc_parameterlist() |
---|
144 | if retann: |
---|
145 | signode += addnodes.desc_returns(retann, retann) |
---|
146 | if self.objtype == 'function': |
---|
147 | return fullname + '/0' |
---|
148 | return fullname |
---|
149 | signode += addnodes.desc_parameterlist() |
---|
150 | |
---|
151 | stack = [signode[-1]] |
---|
152 | counters = [0, 0] |
---|
153 | for token in spec_paramlist_re.split(arglist): |
---|
154 | if token == '[': |
---|
155 | opt = addnodes.desc_optional() |
---|
156 | stack[-1] += opt |
---|
157 | stack.append(opt) |
---|
158 | elif token == ']': |
---|
159 | try: |
---|
160 | stack.pop() |
---|
161 | except IndexError: |
---|
162 | raise ValueError |
---|
163 | elif not token or token == ',' or token.isspace(): |
---|
164 | pass |
---|
165 | else: |
---|
166 | token = token.strip() |
---|
167 | stack[-1] += addnodes.desc_parameter(token, token) |
---|
168 | if len(stack) == 1: |
---|
169 | counters[0] += 1 |
---|
170 | else: |
---|
171 | counters[1] += 1 |
---|
172 | if len(stack) != 1: |
---|
173 | raise ValueError |
---|
174 | if not counters[1]: |
---|
175 | fullname = '%s/%d' % (fullname, counters[0]) |
---|
176 | else: |
---|
177 | fullname = '%s/%d..%d' % (fullname, counters[0], sum(counters)) |
---|
178 | if retann: |
---|
179 | signode += addnodes.desc_returns(retann, retann) |
---|
180 | return fullname |
---|
181 | |
---|
182 | def _get_index_text(self, name): |
---|
183 | if self.objtype == 'function': |
---|
184 | return _('%s (SPEC function)') % name |
---|
185 | elif self.objtype == 'macro': |
---|
186 | return _('%s (SPEC macro)') % name |
---|
187 | elif self.objtype == 'global': |
---|
188 | return _('%s (SPEC global)') % name |
---|
189 | elif self.objtype == 'record': |
---|
190 | return _('%s (SPEC record)') % name |
---|
191 | else: |
---|
192 | return '' |
---|
193 | |
---|
194 | def add_target_and_index(self, name, sig, signode): |
---|
195 | if name not in self.state.document.ids: |
---|
196 | signode['names'].append(name) |
---|
197 | signode['ids'].append(name) |
---|
198 | signode['first'] = (not self.names) |
---|
199 | self.state.document.note_explicit_target(signode) |
---|
200 | if self.objtype =='function': |
---|
201 | finv = self.env.domaindata['spec']['functions'] |
---|
202 | fname, arity = name.split('/') |
---|
203 | if '..' in arity: |
---|
204 | first, last = map(int, arity.split('..')) |
---|
205 | else: |
---|
206 | first = last = int(arity) |
---|
207 | for arity_index in range(first, last+1): |
---|
208 | if fname in finv and arity_index in finv[fname]: |
---|
209 | self.env.warn( |
---|
210 | self.env.docname, |
---|
211 | ('duplicate SPEC function description' |
---|
212 | 'of %s, ') % name + |
---|
213 | 'other instance in ' + |
---|
214 | self.env.doc2path(finv[fname][arity_index][0]), |
---|
215 | self.lineno) |
---|
216 | arities = finv.setdefault(fname, {}) |
---|
217 | arities[arity_index] = (self.env.docname, name) |
---|
218 | else: |
---|
219 | oinv = self.env.domaindata['spec']['objects'] |
---|
220 | if name in oinv: |
---|
221 | self.env.warn( |
---|
222 | self.env.docname, |
---|
223 | 'duplicate SPEC object description of %s, ' % name + |
---|
224 | 'other instance in ' + self.env.doc2path(oinv[name][0]), |
---|
225 | self.lineno) |
---|
226 | oinv[name] = (self.env.docname, self.objtype) |
---|
227 | |
---|
228 | indextext = self._get_index_text(name) |
---|
229 | if indextext: |
---|
230 | self.indexnode['entries'].append(('single', indextext, name, name)) |
---|
231 | |
---|
232 | |
---|
233 | class SpecCurrentModule(Directive): |
---|
234 | """ |
---|
235 | This directive is just to tell Sphinx that we're documenting |
---|
236 | stuff in module foo, but links to module foo won't lead here. |
---|
237 | """ |
---|
238 | |
---|
239 | has_content = False |
---|
240 | required_arguments = 1 |
---|
241 | optional_arguments = 0 |
---|
242 | final_argument_whitespace = False |
---|
243 | option_spec = {} |
---|
244 | |
---|
245 | def run(self): |
---|
246 | env = self.state.document.settings.env |
---|
247 | modname = self.arguments[0].strip() |
---|
248 | if modname == 'None': |
---|
249 | env.temp_data['spec:module'] = None |
---|
250 | else: |
---|
251 | env.temp_data['spec:module'] = modname |
---|
252 | return [] |
---|
253 | |
---|
254 | |
---|
255 | class SpecModule(Directive): |
---|
256 | """ |
---|
257 | Directive to mark description of a new module. |
---|
258 | |
---|
259 | :returns: list of nodes |
---|
260 | """ |
---|
261 | has_content = False |
---|
262 | required_arguments = 1 |
---|
263 | optional_arguments = 0 |
---|
264 | final_argument_whitespace = False |
---|
265 | option_spec = { |
---|
266 | 'platform': lambda x: x, |
---|
267 | 'synopsis': lambda x: x, |
---|
268 | 'noindex': directives.flag, |
---|
269 | 'deprecated': directives.flag, |
---|
270 | } |
---|
271 | |
---|
272 | def run(self): |
---|
273 | env = self.state.document.settings.env |
---|
274 | modname = self.arguments[0].strip() |
---|
275 | noindex = 'noindex' in self.options |
---|
276 | env.temp_data['spec:module'] = modname |
---|
277 | |
---|
278 | env.domaindata['spec']['modules'][modname] = \ |
---|
279 | (env.docname, self.options.get('synopsis', ''), |
---|
280 | self.options.get('platform', ''), 'deprecated' in self.options) |
---|
281 | targetnode = nodes.target('', '', ids=['module-' + modname], ismod=True) |
---|
282 | self.state.document.note_explicit_target(targetnode) |
---|
283 | ret = [targetnode] |
---|
284 | # XXX this behavior of the module directive is a mess... |
---|
285 | if 'platform' in self.options: |
---|
286 | platform = self.options['platform'] |
---|
287 | node = nodes.paragraph() |
---|
288 | node += nodes.emphasis('', _('Platforms: ')) |
---|
289 | node += nodes.Text(platform, platform) |
---|
290 | ret.append(node) |
---|
291 | # the synopsis isn't printed; in fact, it is only used in the |
---|
292 | # modindex currently |
---|
293 | if not noindex: |
---|
294 | indextext = _('%s (module)') % modname |
---|
295 | inode = addnodes.index(entries=[('single', indextext, |
---|
296 | 'module-' + modname, modname)]) |
---|
297 | ret.append(inode) |
---|
298 | return ret |
---|
299 | |
---|
300 | |
---|
301 | class SpecVariable(Directive): |
---|
302 | pass |
---|
303 | |
---|
304 | class SpecModuleIndex(Index): |
---|
305 | """ |
---|
306 | Index subclass to provide the SPEC module index. |
---|
307 | """ |
---|
308 | |
---|
309 | name = 'modindex' |
---|
310 | localname = l_('SPEC Module Index') |
---|
311 | shortname = l_('modules') |
---|
312 | |
---|
313 | def generate(self, docnames=None): |
---|
314 | content = {} |
---|
315 | # list of prefixes to ignore |
---|
316 | ignores = self.domain.env.config['modindex_common_prefix'] |
---|
317 | ignores = sorted(ignores, key=len, reverse=True) |
---|
318 | # list of all modules, sorted by module name |
---|
319 | modules = sorted(self.domain.data['modules'].iteritems(), |
---|
320 | key=lambda x: x[0].lower()) |
---|
321 | # sort out collapsable modules |
---|
322 | prev_modname = '' |
---|
323 | num_toplevels = 0 |
---|
324 | for modname, (docname, synopsis, platforms, deprecated) in modules: |
---|
325 | if docnames and docname not in docnames: |
---|
326 | continue |
---|
327 | |
---|
328 | for ignore in ignores: |
---|
329 | if modname.startswith(ignore): |
---|
330 | modname = modname[len(ignore):] |
---|
331 | stripped = ignore |
---|
332 | break |
---|
333 | else: |
---|
334 | stripped = '' |
---|
335 | |
---|
336 | # we stripped the whole module name? |
---|
337 | if not modname: |
---|
338 | modname, stripped = stripped, '' |
---|
339 | |
---|
340 | entries = content.setdefault(modname[0].lower(), []) |
---|
341 | |
---|
342 | package = modname.split(':')[0] |
---|
343 | if package != modname: |
---|
344 | # it's a submodule |
---|
345 | if prev_modname == package: |
---|
346 | # first submodule - make parent a group head |
---|
347 | entries[-1][1] = 1 |
---|
348 | elif not prev_modname.startswith(package): |
---|
349 | # submodule without parent in list, add dummy entry |
---|
350 | entries.append([stripped + package, 1, '', '', '', '', '']) |
---|
351 | subtype = 2 |
---|
352 | else: |
---|
353 | num_toplevels += 1 |
---|
354 | subtype = 0 |
---|
355 | |
---|
356 | qualifier = deprecated and _('Deprecated') or '' |
---|
357 | entries.append([stripped + modname, subtype, docname, |
---|
358 | 'module-' + stripped + modname, platforms, |
---|
359 | qualifier, synopsis]) |
---|
360 | prev_modname = modname |
---|
361 | |
---|
362 | # apply heuristics when to collapse modindex at page load: |
---|
363 | # only collapse if number of toplevel modules is larger than |
---|
364 | # number of submodules |
---|
365 | collapse = len(modules) - num_toplevels < num_toplevels |
---|
366 | |
---|
367 | # sort by first letter |
---|
368 | content = sorted(content.iteritems()) |
---|
369 | |
---|
370 | return content, collapse |
---|
371 | |
---|
372 | |
---|
373 | class SpecXRefRole(XRefRole): |
---|
374 | def process_link(self, env, refnode, has_explicit_title, title, target): |
---|
375 | refnode['spec:module'] = env.temp_data.get('spec:module') |
---|
376 | if not has_explicit_title: |
---|
377 | title = title.lstrip(':') # only has a meaning for the target |
---|
378 | target = target.lstrip('~') # only has a meaning for the title |
---|
379 | # if the first character is a tilde, don't display the module/class |
---|
380 | # parts of the contents |
---|
381 | if title[0:1] == '~': |
---|
382 | title = title[1:] |
---|
383 | colon = title.rfind(':') |
---|
384 | if colon != -1: |
---|
385 | title = title[colon+1:] |
---|
386 | return title, target |
---|
387 | |
---|
388 | |
---|
389 | class SpecDomain(Domain): |
---|
390 | """SPEC language domain.""" |
---|
391 | name = 'spec' |
---|
392 | label = 'SPEC' |
---|
393 | object_types = { |
---|
394 | 'function': ObjType(l_('function'), 'func'), |
---|
395 | 'macro': ObjType(l_('macro'), 'macro'), |
---|
396 | 'global': ObjType(l_('global'), 'global'), |
---|
397 | 'record': ObjType(l_('record'), 'record'), |
---|
398 | 'module': ObjType(l_('module'), 'mod'), |
---|
399 | 'variable': ObjType(l_('variable'), 'var'), |
---|
400 | } |
---|
401 | directives = { |
---|
402 | 'function': SpecObject, |
---|
403 | 'macro': SpecObject, |
---|
404 | 'record': SpecObject, |
---|
405 | 'module': SpecModule, |
---|
406 | 'variable': SpecVariable, |
---|
407 | 'global': SpecVariable, |
---|
408 | 'currentmodule': SpecCurrentModule, |
---|
409 | } |
---|
410 | roles = { |
---|
411 | 'func' : SpecXRefRole(), |
---|
412 | 'macro': SpecXRefRole(), |
---|
413 | 'global': SpecXRefRole(), |
---|
414 | 'record': SpecXRefRole(), |
---|
415 | 'mod': SpecXRefRole(), |
---|
416 | } |
---|
417 | initial_data = { |
---|
418 | 'objects': {}, # fullname -> docname, objtype |
---|
419 | 'functions' : {}, # fullname -> arity -> (targetname, docname) |
---|
420 | 'modules': {}, # modname -> docname, synopsis, platform, deprecated |
---|
421 | 'globals': {}, # ?????????? |
---|
422 | } |
---|
423 | indices = [ |
---|
424 | SpecModuleIndex, |
---|
425 | ] |
---|
426 | |
---|
427 | def clear_doc(self, docname): |
---|
428 | for fullname, (fn, _) in self.data['objects'].items(): |
---|
429 | if fn == docname: |
---|
430 | del self.data['objects'][fullname] |
---|
431 | for modname, (fn, _, _, _) in self.data['modules'].items(): |
---|
432 | if fn == docname: |
---|
433 | del self.data['modules'][modname] |
---|
434 | for fullname, funcs in self.data['functions'].items(): |
---|
435 | for arity, (fn, _) in funcs.items(): |
---|
436 | if fn == docname: |
---|
437 | del self.data['functions'][fullname][arity] |
---|
438 | if not self.data['functions'][fullname]: |
---|
439 | del self.data['functions'][fullname] |
---|
440 | |
---|
441 | def _find_obj(self, env, modname, name, objtype, searchorder=0): |
---|
442 | """ |
---|
443 | Find a Python object for "name", perhaps using the given module and/or |
---|
444 | classname. |
---|
445 | """ |
---|
446 | if not name: |
---|
447 | return None, None |
---|
448 | if ":" not in name: |
---|
449 | name = "%s:%s" % (modname, name) |
---|
450 | |
---|
451 | if name in self.data['objects']: |
---|
452 | return name, self.data['objects'][name][0] |
---|
453 | |
---|
454 | if '/' in name: |
---|
455 | fname, arity = name.split('/') |
---|
456 | arity = int(arity) |
---|
457 | else: |
---|
458 | fname = name |
---|
459 | arity = -1 |
---|
460 | if fname not in self.data['functions']: |
---|
461 | return None, None |
---|
462 | |
---|
463 | arities = self.data['functions'][fname] |
---|
464 | if arity == -1: |
---|
465 | arity = min(arities) |
---|
466 | if arity in arities: |
---|
467 | docname, targetname = arities[arity] |
---|
468 | return targetname, docname |
---|
469 | return None, None |
---|
470 | |
---|
471 | def resolve_xref(self, env, fromdocname, builder, |
---|
472 | typ, target, node, contnode): |
---|
473 | if typ == 'mod' and target in self.data['modules']: |
---|
474 | docname, synopsis, platform, deprecated = \ |
---|
475 | self.data['modules'].get(target, ('','','', '')) |
---|
476 | if not docname: |
---|
477 | return None |
---|
478 | else: |
---|
479 | title = '%s%s%s' % ((platform and '(%s) ' % platform), |
---|
480 | synopsis, |
---|
481 | (deprecated and ' (deprecated)' or '')) |
---|
482 | return make_refnode(builder, fromdocname, docname, |
---|
483 | 'module-' + target, contnode, title) |
---|
484 | else: |
---|
485 | modname = node.get('spec:module') |
---|
486 | searchorder = node.hasattr('refspecific') and 1 or 0 |
---|
487 | name, obj = self._find_obj(env, modname, target, typ, searchorder) |
---|
488 | if not obj: |
---|
489 | return None |
---|
490 | else: |
---|
491 | return make_refnode(builder, fromdocname, obj, name, |
---|
492 | contnode, name) |
---|
493 | |
---|
494 | def get_objects(self): |
---|
495 | for refname, (docname, doctype) in self.data['objects'].iteritems(): |
---|
496 | yield (refname, refname, doctype, docname, refname, 1) |
---|
497 | |
---|
498 | |
---|
499 | def setup(app): |
---|
500 | app.add_domain(SpecDomain) |
---|
501 | # http://sphinx.pocoo.org/ext/appapi.html#sphinx.domains.Domain |
---|