source: specdomain/trunk/src/specdomain/sphinxcontrib/specdomain.py @ 994

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

refs #8, add autospecdir, no options supported yet

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