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

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

refs #8, big progress today, now able to get ReST-formatted extended comments from SPEC .mac files into Sphinx output using autodoc and subclassing the autodoc.Documenter class for SPEC.

Also, breaking up the test document into parts to make it easier to follow.

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