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

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

to progress with the *autodoc* feature for specdomain, need a SPEC macro file parser class that records line numbers and other desired info

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 13.8 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
22
23from docutils import nodes                              #@UnusedImport
24from docutils.parsers.rst import directives             #@UnusedImport
25
26from sphinx import addnodes
27from sphinx.roles import XRefRole
28from sphinx.locale import l_, _                         #@UnusedImport
29from sphinx.directives import ObjectDescription
30from sphinx.domains import Domain, ObjType, Index       #@UnusedImport
31from sphinx.util.compat import Directive                #@UnusedImport
32from sphinx.util.nodes import make_refnode, nested_parse_with_titles
33from sphinx.util.docfields import Field, TypedField
34from sphinx.util.docstrings import prepare_docstring    #@UnusedImport
35
36from docutils.statemachine import ViewList, string2lines
37import sphinx.util.nodes
38
39
40match_all                   = r'.*'
41non_greedy_filler           = match_all + r'?'
42double_quote_string_match   = r'("' + non_greedy_filler + r'")'
43word_match                  = r'((?:[a-z_]\w*))'
44cdef_match                  = r'(cdef)'
45extended_comment_flag       = r'\"\"\"'
46
47
48spec_macro_sig_re = re.compile(
49                               r'''^ ([a-zA-Z_]\w*)         # macro name
50                               ''', re.VERBOSE)
51
52spec_func_sig_re = re.compile(word_match + r'\('
53                      + r'(' + match_all + r')' 
54                      + r'\)', 
55                      re.IGNORECASE|re.DOTALL)
56
57spec_cdef_name_sig_re = re.compile(double_quote_string_match, 
58                                   re.IGNORECASE|re.DOTALL)
59
60
61spec_extended_comment_flag_sig_re = re.compile(extended_comment_flag, 
62                                               re.IGNORECASE|re.DOTALL)
63spec_extended_comment_start_sig_re = re.compile(r'^'
64                                                + non_greedy_filler
65                                                + extended_comment_flag, 
66                                                re.IGNORECASE|re.DOTALL)
67spec_extended_comment_block_sig_re = re.compile(r'^'
68                                                + non_greedy_filler
69                                                + extended_comment_flag
70                                                + r'(' + non_greedy_filler + r')'
71                                                + extended_comment_flag
72                                                + non_greedy_filler
73                                                + r'$', 
74                                                re.IGNORECASE|re.DOTALL|re.MULTILINE)
75
76
77class SpecMacroObject(ObjectDescription):
78    """
79    Description of a SPEC macro definition
80    """
81
82    doc_field_types = [
83        TypedField('parameter', label=l_('Parameters'),
84                   names=('param', 'parameter', 'arg', 'argument',
85                          'keyword', 'kwarg', 'kwparam'),
86                   typerolename='def', typenames=('paramtype', 'type'),
87                   can_collapse=True),
88        Field('returnvalue', label=l_('Returns'), has_arg=False,
89              names=('returns', 'return')),
90        Field('returntype', label=l_('Return type'), has_arg=False,
91              names=('rtype',)),
92    ]
93
94    def add_target_and_index(self, name, sig, signode):
95        targetname = '%s-%s' % (self.objtype, name)
96        signode['ids'].append(targetname)
97        self.state.document.note_explicit_target(signode)
98        indextext = self._get_index_text(name)
99        if indextext:
100            self.indexnode['entries'].append(('single', indextext,
101                                              targetname, ''))
102
103    def _get_index_text(self, name):
104        macro_types = {
105            'def':  'SPEC macro definition; %s',
106            'rdef': 'SPEC run-time macro definition; %s',
107            'cdef': 'SPEC chained macro definition; %s',
108        }
109        if self.objtype in macro_types:
110            return _(macro_types[self.objtype]) % name
111        else:
112            return ''
113
114    def handle_signature(self, sig, signode):
115        # Must be able to match these (without preceding def or rdef)
116        #     def macro_name
117        #     def macro_name()
118        #     def macro_name(arg1, arg2)
119        #     rdef macro_name
120        #     cdef("macro_name", "content", "groupname", flags)
121        m = spec_func_sig_re.match(sig) or spec_macro_sig_re.match(sig)
122        if m is None:
123            raise ValueError
124        arglist = m.groups()
125        name = arglist[0]
126        args = []
127        if len(arglist) > 1:
128            args = arglist[1:]
129            if name == 'cdef':
130                # TODO: need to match complete arg list
131                # several different signatures are possible (see cdef-examples.mac)
132                # for now, just get the macro name and ignore the arg list
133                m = spec_cdef_name_sig_re.match(args[0])
134                arglist = m.groups()
135                name = arglist[0].strip('"')
136                args = ['<<< cdef argument list not handled yet >>>']       # FIXME:
137        signode += addnodes.desc_name(name, name)
138        if len(args) > 0:
139            signode += addnodes.desc_addname(args, args)
140        return name
141
142
143class SpecVariableObject(ObjectDescription):
144    """
145    Description of a SPEC variable
146    """
147
148
149class SpecMacroSourceObject(ObjectDescription):
150    """
151    Document a SPEC macro source code file
152   
153    This code responds to the ReST file directive::
154   
155        .. spec:macrofile:: partial/path/name/somefile.mac
156            :displayorder: fileorder
157   
158    The ``:displayorder`` parameter indicates how the
159    contents will be sorted for appearance in the ReST document.
160   
161        **fileorder**, **file**
162            Items will be documented in the order in
163            which they appear in the ``.mac`` file.
164       
165        **alphabetical**, **alpha**
166            Items will be documented in alphabetical order.
167   
168    A (near) future enhancement would be to provide for
169    documenting all macro files in a directory, with optional
170    recursion into subdirectories.  By default, the code would
171    only document files that match the glob pattern ``*.mac``.
172    Such as::
173   
174       .. spec:directory:: partial/path/name
175          :recursion:
176          :displayorder: alphabetical
177    """
178   
179    # TODO: work-in-progress
180   
181    doc_field_types = [
182        Field('displayorder', label=l_('Display order'), has_arg=False,
183              names=('displayorder', 'synonym')),
184    ]
185
186    def add_target_and_index(self, name, sig, signode):
187        targetname = '%s-%s' % (self.objtype, name)
188        signode['ids'].append(targetname)
189        self.state.document.note_explicit_target(signode)
190        indextext = sig
191        if indextext:
192            self.indexnode['entries'].append(('single', indextext,
193                                              targetname, ''))
194
195    def handle_signature(self, sig, signode):
196        signode += addnodes.desc_name(sig, sig)
197        # TODO: this is the place to parse the SPEC macro source code file named in "sig"
198        '''
199        Since 2002, SPEC has allowed for triple-quoted strings as extended comments.
200        Few, if any, have used them.
201        Assume that they will contain ReST formatted comments.
202        The first, simplest thing to do is to read the .mac file and only extract
203        all the extended comments and add them as nodes to the current document.
204       
205        An additional step would be to parse for def, cdef, rdef, global, local, const, ...
206        Another step would be to attach source code and provide links from each to
207        highlighted source code blocks.
208        '''
209        extended_comments_list = self.parse_macro_file(sig)
210        view = ViewList([u'TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up'])
211        #contentnode = nodes.TextElement()
212        node = nodes.paragraph()
213        node.document = self.state.document
214        self.state.nested_parse(view, 0, signode)
215        # TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up
216        # nodes.TextElement(raw, text)
217        # sphinx.directives.__init__.py  ObjectDescription.run() method
218        #  Summary:  This does not belong here, in the signature processing part.
219        #            Instead, it goes at the directive.run() method.  Where's that here?
220#        for extended_comment in extended_comments_list:
221#            for line in string2lines(extended_comment):
222#                view = ViewList([line])
223#                nested_parse_with_titles(self.state, view, signode)
224        return sig
225   
226    def XX_run(self):
227        # TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up
228        # nodes.TextElement(raw, text)
229        # sphinx.directives.__init__.py  ObjectDescription.run() method
230        #  Summary:  This does not belong here, in the signature processing part.
231        #            Instead, it goes at the directive.run() method.  This is the new place!
232        pass
233   
234    def parse_macro_file(self, filename):
235        """
236        parse the SPEC macro file and return the ReST blocks
237       
238        :param str filename: name (with optional path) of SPEC macro file
239            (The path is relative to the ``.rst`` document.)
240        :returns [str]: list of ReST-formatted extended comment blocks (docstrings) from SPEC macro file.
241       
242        [future] parse more stuff as planned, this is very simplistic for now
243        """
244        results = []
245        if not os.path.exists(filename):
246            raise RuntimeError, "could not find: " + filename
247       
248        buf = open(filename, 'r').read()
249        #n = len(buf)
250        for node in spec_extended_comment_block_sig_re.finditer(buf):
251            #g = node.group()
252            #gs = node.groups()
253            #s = node.start()
254            #e = node.end()
255            #t = buf[s:e]
256            results.append(node.groups()[0])            # TODO: can we get line number also?
257        return results
258
259
260class SpecXRefRole(XRefRole):
261    """ """
262   
263    def process_link(self, env, refnode, has_explicit_title, title, target):
264        key = ":".join((refnode['refdomain'], refnode['reftype']))
265        refnode[key] = env.temp_data.get(key)        # key was 'spec:def'
266        if not has_explicit_title:
267            title = title.lstrip(':')   # only has a meaning for the target
268            target = target.lstrip('~') # only has a meaning for the title
269            # if the first character is a tilde, don't display the module/class
270            # parts of the contents
271            if title[0:1] == '~':
272                title = title[1:]
273                colon = title.rfind(':')
274                if colon != -1:
275                    title = title[colon+1:]
276        return title, target
277
278    def result_nodes(self, document, env, node, is_ref):
279        # this code adds index entries for each role instance
280        if not is_ref:
281            return [node], []
282        varname = node['reftarget']
283        tgtid = 'index-%s' % env.new_serialno('index')
284        indexnode = addnodes.index()
285        indexnode['entries'] = [
286            ('single', varname, tgtid, ''),
287            #('single', _('environment variable; %s') % varname, tgtid, ''),
288        ]
289        targetnode = nodes.target('', '', ids=[tgtid])
290        document.note_explicit_target(targetnode)
291        return [indexnode, targetnode, node], []
292
293
294class SpecDomain(Domain):
295    """SPEC language domain."""
296   
297    name = 'spec'
298    label = 'SPEC, http://www.certif.com'
299    object_types = {    # type of object that a domain can document
300        'def':    ObjType(l_('def'),    'def'),
301        'rdef':   ObjType(l_('rdef'),   'rdef'),
302        'cdef':   ObjType(l_('cdef'),   'cdef'),
303        'global': ObjType(l_('global'), 'global'),
304        'local':  ObjType(l_('local'),  'local'),
305    }
306    directives = {
307        'def':          SpecMacroObject,
308        'rdef':         SpecMacroObject,
309        'cdef':         SpecMacroObject,
310        'global':       SpecVariableObject,
311        'local':        SpecVariableObject,
312        'macrofile':    SpecMacroSourceObject,
313    }
314    roles = {
315        'def' :     SpecXRefRole(),
316        'rdef':     SpecXRefRole(),
317        'cdef':     SpecXRefRole(),
318        'global':   SpecXRefRole(),
319        'local':    SpecXRefRole(),
320    }
321    initial_data = {
322        'objects': {}, # fullname -> docname, objtype
323    }
324
325    def clear_doc(self, docname):
326        for (typ, name), doc in self.data['objects'].items():
327            if doc == docname:
328                del self.data['objects'][typ, name]
329
330    def resolve_xref(self, env, fromdocname, builder, typ, target, node,
331                     contnode):
332        objects = self.data['objects']
333        objtypes = self.objtypes_for_role(typ)
334        for objtype in objtypes:
335            if (objtype, target) in objects:
336                return make_refnode(builder, fromdocname,
337                                    objects[objtype, target],
338                                    objtype + '-' + target,
339                                    contnode, target + ' ' + objtype)
340
341    def get_objects(self):
342        for (typ, name), docname in self.data['objects'].iteritems():
343            yield name, name, typ, docname, typ + '-' + name, 1
344
345
346# http://sphinx.pocoo.org/ext/tutorial.html#the-setup-function
347
348def setup(app):
349    app.add_domain(SpecDomain)
350    # http://sphinx.pocoo.org/ext/appapi.html#sphinx.domains.Domain
Note: See TracBrowser for help on using the repository browser.