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

Last change on this file since 1069 was 1069, checked in by jemian, 11 years ago

refs #15

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