source: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py @ 982

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

also match cdef() embedded within an if(){} block, as in standard.mac

  • 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: 11.9 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-06-27 19:33:01 +0000 (Wed, 27 Jun 2012) $
5# $Author: jemian $
6# $Revision: 982 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 982 2012-06-27 19:33:01Z 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
39
40# TODO: handle "#: " indicating a description of a variable on the preceding line
41
42class SpecMacrofileParser:
43    '''
44    Parse a SPEC macro file for macro definitions,
45    variable declarations, and extended comments.
46
47        Since 2002, SPEC has allowed for triple-quoted
48        strings as extended comments.  Few, if any, have used them.
49        Assume all extended comments contain ReST formatted comments,
50        *including initial section titles or transitions*.
51        The first and simplest thing to do is to read the .mac file and only extract
52        all the extended comments and add them as nodes to the current document.
53   
54    Assume macro definitions are not nested (but test for this).
55       
56    An additional step would be to parse for:
57    * def
58    * cdef
59    * rdef
60    * global    (done)
61    * local    (done)
62    * constant    (done)
63    * array
64    * ...
65    '''
66
67    # consider using:  docutils.statemachine here
68    states = (                  # assume SPEC def macros cannot be nested
69        'global',               # the level that provides the SPEC command prompt
70        'extended comment',     # inside a multiline extended comment
71        'def macro',            # inside a multiline def macro definition
72        'rdef macro',           # inside a multiline rdef macro definition
73        'cdef macro',           # inside a multiline cdef macro definition
74        'parsed',               # parsing of file is complete
75    )
76
77    def __init__(self, macrofile):
78        '''
79        Constructor
80        '''
81        self.buf = None
82        self.findings = []
83        self.filename = None
84        self.read(macrofile)
85        self.parse_macro_file()
86   
87    def read(self, filename):
88        """
89        load the SPEC macro source code file into an internal buffer
90       
91        :param str filename: name (with optional path) of SPEC macro file
92            (The path is relative to the ``.rst`` document.)
93        """
94        if not os.path.exists(filename):
95            raise RuntimeError, "file not found: " + filename
96        self.filename = filename
97        self.buf = open(filename, 'r').read()
98
99    def parse_macro_file(self):
100        """
101        parse the internal buffer
102        """
103        line_number = 0
104        self.state = 'global'
105        self.state_stack = []
106        for line in self.buf.split('\n'):
107
108            line_number += 1
109            if self.state not in self.states:
110                # this quickly points out a programmer error
111                msg = "unexpected parser state: %s, line %s" % (self.state, line_number)
112                raise RuntimeError, msg
113
114            if self.state == 'global':
115                if self._is_lgc_variable(line, line_number):
116                    continue
117                if self._is_one_line_extended_comment(line, line_number):
118                    continue
119                if self._is_multiline_start_extended_comment(line, line_number):
120                    continue
121            elif self.state == 'extended comment':
122                if not self._is_multiline_end_extended_comment(line, line_number):
123                    # multiline extended comment continues
124                    self.ec['text'].append(line)
125                continue
126            elif self.state == 'def macro':
127                pass
128            elif self.state == 'cdef macro':
129                pass
130            elif self.state == 'rdef macro':
131                pass
132       
133        if len(self.state_stack) > 0:
134            fmt = "encountered EOF while parsing %s, line %d, in state %s, stack=%s"
135            msg = fmt % (self.filename, line_number, self.state, self.state_stack)
136            raise RuntimeWarning, msg
137
138        self.state = 'parsed'
139       
140    lgc_variable_sig_re = re.compile(string_start
141                                        + non_greedy_whitespace
142                                        + r'(local|global|constant)'
143                                        + r'((?:,?\s*@?[\w.eE+-]+\[?\]?)*)'
144                                        + non_greedy_whitespace
145                                        + r'#' + non_greedy_filler
146                                        + string_end, 
147                                        re.VERBOSE)
148
149    def _is_lgc_variable(self, line, line_number):
150        ''' local, global, or constant variable declaration '''
151        m = self._search(self.lgc_variable_sig_re, line)
152        if m is None:
153            return False
154       
155        objtype, args = self.lgc_variable_sig_re.match(line).groups()
156        pos = args.find('#')
157        if pos > -1:
158            args = args[:pos]
159        m['objtype'] = objtype
160        m['start_line'] = m['end_line'] = line_number
161        del m['start'], m['end'], m['line']
162        if objtype == 'constant':
163            var, _ = args.split()
164            m['text'] = var.rstrip(',')
165            self.findings.append(dict(m))
166        else:
167            # TODO: consider not indexing "global" inside a def
168            # TODO: consider not indexing "local" at global level
169            #      or leave these decisions for later, including some kind of analyzer
170            for var in args.split():
171                m['text'] = var.rstrip(',')
172                self.findings.append(dict(m))
173                # TODO: to what is this local?  (remember the def it belongs to)
174        return True
175   
176    extended_comment_block_sig_re = re.compile(string_start
177                                                + non_greedy_whitespace
178                                                + extended_comment_marker
179                                                + r'(' + non_greedy_filler + r')'
180                                                + extended_comment_marker
181                                                + non_greedy_filler
182                                                + string_end, 
183                                                re.IGNORECASE|re.DOTALL|re.MULTILINE)
184
185    def _is_one_line_extended_comment(self, line, line_number):
186        m = self._search(self.extended_comment_block_sig_re, line)
187        if m is None:
188            return False
189        del m['start'], m['end'], m['line']
190        m['objtype'] = 'extended comment'
191        m['start_line'] = m['end_line'] = line_number
192        m['text'] = m['text'].strip()
193        self.findings.append(dict(m))
194        return True
195
196    extended_comment_start_sig_re = re.compile(string_start
197                                                + non_greedy_whitespace
198                                                + extended_comment_match, 
199                                                re.IGNORECASE|re.VERBOSE)
200   
201    def _is_multiline_start_extended_comment(self, line, line_number):
202        m = self._search(self.extended_comment_start_sig_re, line)
203        if m is None:
204            return False
205        text = m['line'][m['end']:]
206        del m['start'], m['end'], m['line']
207        m['objtype'] = 'extended comment'
208        m['start_line'] = line_number
209        self.ec = dict(m)    # container for extended comment data
210        self.ec['text'] = [text]
211        self.state_stack.append(self.state)
212        self.state = 'extended comment'
213        return True
214
215    extended_comment_end_sig_re = re.compile(non_greedy_whitespace
216                                                + extended_comment_match
217                                                + non_greedy_whitespace
218                                                + r'#' + non_greedy_filler
219                                                + string_end,
220                                                re.IGNORECASE|re.VERBOSE)
221
222    def _is_multiline_end_extended_comment(self, line, line_number):
223        m = self._search(self.extended_comment_end_sig_re, line)
224        if m is None:
225            return False
226        text = m['line'][:m['start']]
227        self.ec['text'].append(text)
228        self.ec['text'] = '\n'.join(self.ec['text'])
229        self.ec['end_line'] = line_number
230        self.findings.append(dict(self.ec))
231        self.state = self.state_stack.pop()
232        del self.ec
233        return True
234   
235    def _search(self, regexp, line):
236        '''regular expression search of line, returns a match as a dictionary or None'''
237        m = regexp.search(line)
238        if m is None:
239            return None
240        # TODO: define a parent key somehow
241        d = {
242            'start': m.start(1),
243            'end':   m.end(1),
244            'text':  m.group(1),
245            'line':  line,
246            'filename':  self.filename,
247        }
248        return d
249
250    def __str__(self):
251        s = []
252        for r in self.findings:
253            s.append( '' )
254            t = '%s %s %d %d %s' % ('.. ' + '*'*20, 
255                                    r['objtype'], 
256                                    r['start_line'], 
257                                    r['end_line'], 
258                                    '*'*20)
259            s.append( t )
260            s.append( '' )
261            s.append( r['text'] )
262        return '\n'.join(s)
263
264    def ReST(self):
265        """create the ReStructured Text from what has been found"""
266        if not self.state == 'parsed':
267            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
268           
269        s = []
270        declarations = []
271        for r in self.findings:
272            if r['objtype'] == 'extended comment':
273                s.append( '' )
274                s.append( '.. %s %s %d %d' % (self.filename, 
275                                              r['objtype'], 
276                                              r['start_line'], 
277                                              r['end_line']) )
278                s.append( '' )
279                s.append(r['text'])
280            elif r['objtype'] in ('local', 'global', 'constant'):
281                declarations.append(r)      # remember, show this later
282            # TODO: other objtypes
283        if len(declarations) > 0:
284            col_keys = ('text', 'objtype', 'start_line', 'end_line', )
285            widths = dict([( key, len(str(key)) ) for key in col_keys])
286            for d in declarations:
287                widths = dict([( key, max(w, len(str(d[key])))) for key, w in widths.items()])
288            separator = " ".join( ["="*widths[key] for key in col_keys] )
289            fmt = " ".join( ["%%-%ds"%widths[key] for key in col_keys] )
290            s.append( '' )
291#            s.append( '.. rubric:: Variable Declarations:' )
292            s.append( 'Variable Declarations' )
293            s.append( '=====================' )
294            s.append( '' )
295            s.append( separator )
296            s.append( fmt % tuple([str(key) for key in col_keys]) )
297            s.append( separator )
298            for d in declarations:
299                s.append( fmt % tuple([str(d[key]) for key in col_keys]) )
300            s.append( separator )
301        return '\n'.join(s)
302
303
304if __name__ == '__main__':
305    filelist = [
306        '../macros/test-battery.mac',
307        '../macros/cdef-examples.mac',
308        '../macros/shutter.mac',
309    ]
310    for item in filelist:
311        p = SpecMacrofileParser(item)
312        #print p
313        print p.ReST()
Note: See TracBrowser for help on using the repository browser.