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

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

refs #8, clean up the imports, provide .mac source link from autodocumenter

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