source: specdomain/src/specdomain/sphinxcontrib/specmacrofileparser.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:executable set to *
  • Property svn:keywords set to Author Date Id Revision URL Header
File size: 9.9 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-06-22 22:58:50 +0000 (Fri, 22 Jun 2012) $
5# $Author: jemian $
6# $Revision: 963 $
7# $HeadURL: specdomain/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 963 2012-06-22 22:58:50Z jemian $
9########### SVN repository information ###################
10
11
12"""
13Construct a SPEC macro source code file parser for
14use by the specdomain for Sphinx.
15
16:copyright: Copyright 2012 by BCDA, Advanced Photon Source, Argonne National Laboratory
17:license: ANL Open Source License, see LICENSE for details.
18"""
19
20import os
21import re
22
23
24#   http://www.txt2re.com/index-python.php3
25#  http://regexpal.com/
26
27string_start                = r'^'
28string_end                  = r'$'
29match_all                   = r'.*'
30non_greedy_filler           = match_all + r'?'
31non_greedy_whitespace       = r'\s*?'
32double_quote_string_match   = r'("' + non_greedy_filler + r'")'
33prog_name_match             = r'([a-z_]\w*)'
34word_match                  = r'((?:[a-z_]\w*))'
35cdef_match                  = r'(cdef)'
36extended_comment_marker     = r'\"{3}'
37extended_comment_match      = r'(' + extended_comment_marker + r')'
38
39macro_sig_re = re.compile(
40                               r'''^ ([a-zA-Z_]\w*)         # macro name
41                               ''', re.VERBOSE)
42
43func_sig_re = re.compile(word_match + r'\('
44                      + r'(' + match_all + r')' 
45                      + r'\)', 
46                      re.IGNORECASE|re.DOTALL)
47
48cdef_name_sig_re = re.compile(double_quote_string_match, 
49                                   re.IGNORECASE|re.DOTALL)
50
51
52extended_comment_flag_sig_re = re.compile(extended_comment_marker, 
53                                               re.IGNORECASE|re.DOTALL)
54extended_comment_start_sig_re = re.compile(string_start
55                                                + non_greedy_whitespace
56                                                + extended_comment_match, 
57                                                re.IGNORECASE|re.VERBOSE)
58extended_comment_end_sig_re = re.compile(non_greedy_whitespace
59                                                + extended_comment_match
60                                                + non_greedy_whitespace
61                                                + r'#' + non_greedy_filler
62                                                + string_end,
63                                                re.IGNORECASE|re.VERBOSE)
64extended_comment_block_sig_re = re.compile(string_start
65                                                + non_greedy_whitespace
66                                                + extended_comment_marker
67                                                + r'(' + non_greedy_filler + r')'
68                                                + extended_comment_marker
69                                                + non_greedy_filler
70                                                + string_end, 
71                                                re.IGNORECASE|re.DOTALL|re.MULTILINE)
72lgc_variable_sig_re = re.compile(string_start
73                                    + non_greedy_whitespace
74                                    + r'(local|global|constant)'
75                                    + r'((?:\s*@?[\w.eE+-]+\[?\]?)*)'
76                                    + non_greedy_whitespace
77                                    + r'#' + non_greedy_filler
78                                    + string_end, 
79                                    re.VERBOSE)
80
81
82class SpecMacrofileParser:
83    '''
84    Parse a SPEC macro file for macro definitions,
85    variable declarations, and extended comments.
86
87        Since 2002, SPEC has allowed for triple-quoted
88        strings as extended comments.  Few, if any, have used them.
89        Assume all extended comments contain ReST formatted comments,
90        *including initial section titles or transitions*.
91        The first and simplest thing to do is to read the .mac file and only extract
92        all the extended comments and add them as nodes to the current document.
93       
94        An additional step would be to parse for:
95        * def
96        * cdef
97        * rdef
98        * global    (done)
99        * local    (done)
100        * constant    (done)
101        * array
102        * ...
103       
104    '''
105
106    states = (                  # assume SPEC def macros cannot be nested
107        'global',               # the level that provides the SPEC command prompt
108        'extended comment',     # inside a multiline extended comment
109        'def macro',            # inside a multiline def macro definition
110        'rdef macro',           # inside a multiline rdef macro definition
111        'cdef macro',           # inside a multiline cdef macro definition
112    )
113
114    def __init__(self, macrofile):
115        '''
116        Constructor
117        '''
118        self.buf = None
119        self.findings = []
120        self.filename = None
121        self.read(macrofile)
122        self.parse_macro_file()
123   
124    def read(self, filename):
125        """
126        load the SPEC macro source code file into an internal buffer
127       
128        :param str filename: name (with optional path) of SPEC macro file
129            (The path is relative to the ``.rst`` document.)
130        """
131        if not os.path.exists(filename):
132            raise RuntimeError, "file not found: " + filename
133        self.filename = filename
134        self.buf = open(filename, 'r').read()
135   
136    def parse_macro_file(self):
137        """
138        parse the internal buffer
139        """
140        line_number = 0
141        state = 'global'
142        state_stack = []
143        for line in self.buf.split('\n'):
144            if state not in self.states:
145                raise RuntimeError, "unexpected parser state: " + state
146            line_number += 1
147            if state == 'global':
148
149                m = self._match(lgc_variable_sig_re, line)
150                if m is not None:           # local, global, or constant variable declaration
151                    objtype, args = lgc_variable_sig_re.match(line).groups()
152                    pos = args.find('#')
153                    if pos > -1:
154                        args = args[:pos]
155                    m['objtype'] = objtype
156                    m['start_line'] = m['end_line'] = line_number
157                    del m['start'], m['end'], m['line']
158                    if objtype == 'constant':
159                        var, _ = args.split()
160                        m['text'] = var
161                        self.findings.append(dict(m))
162                    else:
163                        # TODO: consider not indexing "global" inside a def
164                        # TODO: consider not indexing "local" at global level
165                        for var in args.split():
166                            m['text'] = var
167                            self.findings.append(dict(m))
168                            # TODO: to what is this local?  (remember the def it belongs to)
169                    continue
170
171                # test if one-line extended comment
172                m = self._match(extended_comment_block_sig_re, line)
173                if m is not None:
174                    del m['start'], m['end'], m['line']
175                    m['objtype'] = 'extended comment'
176                    m['start_line'] = m['end_line'] = line_number
177                    m['text'] = m['text'].strip()
178                    self.findings.append(dict(m))
179                    continue
180               
181                # test if start of multiline extended comment
182                m = self._match(extended_comment_start_sig_re, line)
183                if m is not None:
184                    text = m['line'][m['end']:]
185                    del m['start'], m['end'], m['line']
186                    m['objtype'] = 'extended comment'
187                    m['start_line'] = line_number
188                    ec = dict(m)    # container for extended comment data
189                    ec['text'] = [text]
190                    state_stack.append(state)
191                    state = 'extended comment'
192                    continue
193
194            elif state == 'extended comment':
195                # test if end of multiline extended comment
196                m = self._match(extended_comment_end_sig_re, line)
197                if m is not None:
198                    text = m['line'][:m['start']]
199                    ec['text'].append(text)
200                    ec['text'] = '\n'.join(ec['text'])
201                    ec['end_line'] = line_number
202                    self.findings.append(dict(ec))
203                    state = state_stack.pop()
204                    del ec
205                else:
206                    # multiline extended comment continues
207                    ec['text'].append(line)
208                continue
209   
210    def _match(self, regexp, line):
211        m = regexp.search(line)
212        if m is None:
213            return None
214        d = {
215            'start': m.start(1),
216            'end':   m.end(1),
217            'text':  m.group(1),
218            'line':  line,
219            'filename':  self.filename,
220        }
221        return d
222
223    def __str__(self):
224        s = []
225        for r in self.findings:
226            s.append( '' )
227            t = '%s %s %d %d %s' % ('.. ' + '*'*20, 
228                                    r['objtype'], 
229                                    r['start_line'], 
230                                    r['end_line'], 
231                                    '*'*20)
232            s.append( t )
233            s.append( '' )
234            s.append( r['text'] )
235        return '\n'.join(s)
236
237    def ReST(self):
238        """create the ReStructured Text from what has been found"""
239        s = []
240        for r in self.findings:
241            if r['objtype'] == 'extended comment':
242                s.append( '' )
243                s.append( '.. %s %s %d %d' % (self.filename, 
244                                              r['objtype'], 
245                                              r['start_line'], 
246                                              r['end_line']) )
247                s.append( '' )
248                s.append(r['text'])
249            # TODO: other objtypes
250        return '\n'.join(s)
251
252
253if __name__ == '__main__':
254    p = SpecMacrofileParser('../test/test-battery.mac')
255    #print p.ReST()
256    print p
257    p = SpecMacrofileParser('../test/cdef-examples.mac')
258    #print p.ReST()
Note: See TracBrowser for help on using the repository browser.