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

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

refs #8

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 14.3 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    # TODO: The directive that declares the variable should be the primary (bold) index.
149    # TODO: array variables are not handled at all
150
151
152class SpecMacroSourceObject(ObjectDescription):
153    """
154    Document a SPEC macro source code file
155   
156    This code responds to the ReST file directive::
157   
158        .. spec:macrofile:: partial/path/name/somefile.mac
159            :displayorder: fileorder
160   
161    The ``:displayorder`` parameter indicates how the
162    contents will be sorted for appearance in the ReST document.
163   
164        **fileorder**, **file**
165            Items will be documented in the order in
166            which they appear in the ``.mac`` file.
167       
168        **alphabetical**, **alpha**
169            Items will be documented in alphabetical order.
170   
171    A (near) future enhancement would be to provide for
172    documenting all macro files in a directory, with optional
173    recursion into subdirectories.  By default, the code would
174    only document files that match the glob pattern ``*.mac``.
175    Such as::
176   
177       .. spec:directory:: partial/path/name
178          :recursion:
179          :displayorder: alphabetical
180    """
181   
182    # TODO: work-in-progress
183   
184    doc_field_types = [
185        Field('displayorder', label=l_('Display order'), has_arg=False,
186              names=('displayorder', 'synonym')),
187    ]
188
189    def add_target_and_index(self, name, sig, signode):
190        targetname = '%s-%s' % (self.objtype, name)
191        signode['ids'].append(targetname)
192        self.state.document.note_explicit_target(signode)
193        indextext = sig
194        if indextext:
195            self.indexnode['entries'].append(('single', indextext,
196                                              targetname, ''))
197
198    def handle_signature(self, sig, signode):
199        signode += addnodes.desc_name(sig, sig)
200        # TODO: this is the place to parse the SPEC macro source code file named in "sig"
201        '''
202        Since 2002, SPEC has allowed for triple-quoted strings as extended comments.
203        Few, if any, have used them.
204        Assume that they will contain ReST formatted comments.
205        The first, simplest thing to do is to read the .mac file and only extract
206        all the extended comments and add them as nodes to the current document.
207       
208        An additional step would be to parse for def, cdef, rdef, global, local, const, ...
209        Another step would be to attach source code and provide links from each to
210        highlighted source code blocks.
211        '''
212        #extended_comments_list = self.parse_macro_file(sig)
213        view = ViewList([u'TODO: Just handle the macro signature, additional documentation elsewhere'])
214        #contentnode = nodes.TextElement()
215        node = nodes.paragraph()
216        node.document = self.state.document
217        self.state.nested_parse(view, 0, signode)
218        # TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up
219        # nodes.TextElement(raw, text)
220        # sphinx.directives.__init__.py  ObjectDescription.run() method
221        #  Summary:  This does not belong here, in the signature processing part.
222        #            Instead, it goes at the directive.run() method.  Where's that here?
223#        for extended_comment in extended_comments_list:
224#            for line in string2lines(extended_comment):
225#                view = ViewList([line])
226#                nested_parse_with_titles(self.state, view, signode)
227        return sig
228   
229    def run(self):
230        # TODO: recognize the ReST formatting in the following extended comment and it needs to be cleaned up
231        # nodes.TextElement(raw, text)
232        # sphinx.directives.__init__.py  ObjectDescription.run() method
233        #  Summary:  This belongs with the directive.run() method.  This is the new place!
234        #self.content.append(u'')
235        #self.content.append(u'.. caution:: Use caution.')
236        from specmacrofileparser import SpecMacrofileParser
237        macrofile = self.arguments[0]
238        p = SpecMacrofileParser(macrofile)
239        return ObjectDescription.run(self)
240   
241    def parse_macro_file(self, filename):
242        """
243        parse the SPEC macro file and return the ReST blocks
244       
245        :param str filename: name (with optional path) of SPEC macro file
246            (The path is relative to the ``.rst`` document.)
247        :returns [str]: list of ReST-formatted extended comment blocks (docstrings) from SPEC macro file.
248       
249        [future] parse more stuff as planned, this is very simplistic for now
250        """
251        results = []
252        if not os.path.exists(filename):
253            raise RuntimeError, "could not find: " + filename
254       
255        buf = open(filename, 'r').read()
256        #n = len(buf)
257        for node in spec_extended_comment_block_sig_re.finditer(buf):
258            #g = node.group()
259            #gs = node.groups()
260            #s = node.start()
261            #e = node.end()
262            #t = buf[s:e]
263            results.append(node.groups()[0])            # TODO: can we get line number also?
264        return results
265
266
267class SpecXRefRole(XRefRole):
268    """ """
269   
270    def process_link(self, env, refnode, has_explicit_title, title, target):
271        key = ":".join((refnode['refdomain'], refnode['reftype']))
272        refnode[key] = env.temp_data.get(key)        # key was 'spec:def'
273        if not has_explicit_title:
274            title = title.lstrip(':')   # only has a meaning for the target
275            target = target.lstrip('~') # only has a meaning for the title
276            # if the first character is a tilde, don't display the module/class
277            # parts of the contents
278            if title[0:1] == '~':
279                title = title[1:]
280                colon = title.rfind(':')
281                if colon != -1:
282                    title = title[colon+1:]
283        return title, target
284
285    def result_nodes(self, document, env, node, is_ref):
286        # this code adds index entries for each role instance
287        if not is_ref:
288            return [node], []
289        varname = node['reftarget']
290        tgtid = 'index-%s' % env.new_serialno('index')
291        indexnode = addnodes.index()
292        indexnode['entries'] = [
293            ('single', varname, tgtid, ''),
294            #('single', _('environment variable; %s') % varname, tgtid, ''),
295        ]
296        targetnode = nodes.target('', '', ids=[tgtid])
297        document.note_explicit_target(targetnode)
298        return [indexnode, targetnode, node], []
299
300
301class SpecDomain(Domain):
302    """SPEC language domain."""
303   
304    name = 'spec'
305    label = 'SPEC, http://www.certif.com'
306    object_types = {    # type of object that a domain can document
307        'def':        ObjType(l_('def'),        'def'),
308        'rdef':       ObjType(l_('rdef'),       'rdef'),
309        'cdef':       ObjType(l_('cdef'),       'cdef'),
310        'global':     ObjType(l_('global'),     'global'),
311        'local':      ObjType(l_('local'),      'local'),
312        'constant':   ObjType(l_('constant'),   'constant'),
313        'macrofile':  ObjType(l_('macrofile'),  'macrofile'),
314    }
315    directives = {
316        'def':          SpecMacroObject,
317        'rdef':         SpecMacroObject,
318        'cdef':         SpecMacroObject,
319        'global':       SpecVariableObject,
320        'local':        SpecVariableObject,
321        'constant':     SpecVariableObject,
322        'macrofile':    SpecMacroSourceObject,
323    }
324    roles = {
325        'def' :     SpecXRefRole(),
326        'rdef':     SpecXRefRole(),
327        'cdef':     SpecXRefRole(),
328        'global':   SpecXRefRole(),
329        'local':    SpecXRefRole(),
330        'constant': SpecXRefRole(),
331    }
332    initial_data = {
333        'objects': {}, # fullname -> docname, objtype
334    }
335
336    def clear_doc(self, docname):
337        for (typ, name), doc in self.data['objects'].items():
338            if doc == docname:
339                del self.data['objects'][typ, name]
340
341    def resolve_xref(self, env, fromdocname, builder, typ, target, node,
342                     contnode):
343        objects = self.data['objects']
344        objtypes = self.objtypes_for_role(typ)
345        for objtype in objtypes:
346            if (objtype, target) in objects:
347                return make_refnode(builder, fromdocname,
348                                    objects[objtype, target],
349                                    objtype + '-' + target,
350                                    contnode, target + ' ' + objtype)
351
352    def get_objects(self):
353        for (typ, name), docname in self.data['objects'].iteritems():
354            yield name, name, typ, docname, typ + '-' + name, 1
355
356
357# http://sphinx.pocoo.org/ext/tutorial.html#the-setup-function
358
359def setup(app):
360    app.add_domain(SpecDomain)
361    # http://sphinx.pocoo.org/ext/appapi.html#sphinx.domains.Domain
Note: See TracBrowser for help on using the repository browser.