source: specdomain/src/round1/sphinxcontrib/specdomain.py @ 909

Last change on this file since 909 was 909, checked in by jemian, 11 years ago

refs #8

File size: 17.4 KB
Line 
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
14import re
15import string
16
17from docutils import nodes
18from docutils.parsers.rst import directives
19
20from sphinx import addnodes
21from sphinx.roles import XRefRole
22from sphinx.locale import l_, _
23from sphinx.directives import ObjectDescription
24from sphinx.domains import Domain, ObjType, Index
25from sphinx.util.compat import Directive
26from sphinx.util.nodes import make_refnode
27from sphinx.util.docfields import Field, TypedField
28
29
30# TODO: tailor these for SPEC
31# RE to split at word boundaries
32wsplit_re = re.compile(r'(\W+)')
33
34# http://www.greenend.org.uk/rjk/tech/regexp.html
35
36# REs for SPEC signatures
37spec_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
46spec_macro_sig_re = re.compile(
47    r'''^ ([\w.]*:)?             # module name
48          ([A-Z]\w+) $           # thing name
49          ''', re.VERBOSE)
50
51spec_global_sig_re = re.compile(
52    r'''^ ([\w.]*:)?             # module name
53          ([A-Z]\w+) $           # thing name
54          ''', re.VERBOSE)
55
56
57spec_var_sig_re = re.compile(
58    r'''^ ([\w.]*:)?             # module name
59          ([A-Z]\w+) $           # thing name
60          ''', re.VERBOSE)
61
62
63spec_record_sig_re = re.compile(
64    r'''^ ([\w.]*:)?             # module name
65          (\#\w+) $              # thing name
66          ''', re.VERBOSE)
67
68
69spec_paramlist_re = re.compile(r'([\[\],])')  # split at '[', ']' and ','
70
71
72class 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
233class 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
255class 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
301class SpecVariable(Directive):
302    pass
303
304class 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
373class 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
389class 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
499def setup(app):
500    app.add_domain(SpecDomain)
501    # http://sphinx.pocoo.org/ext/appapi.html#sphinx.domains.Domain
Note: See TracBrowser for help on using the repository browser.