source: specdomain/src/specdomain/sphinxcontrib/specdomain.py @ 965

Last change on this file since 965 was 965, checked in by jemian, 10 years ago

refs #8, section titles in extended comments now handled properly

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