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

Last change on this file since 980 was 980, checked in by jemian, 10 years ago
  • 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-26 05:17:32 +0000 (Tue, 26 Jun 2012) $
5# $Author: jemian $
6# $Revision: 980 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 980 2012-06-26 05:17:32Z 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    extended_comment_start_sig_re = re.compile(string_start
196                                                + non_greedy_whitespace
197                                                + extended_comment_match, 
198                                                re.IGNORECASE|re.VERBOSE)
199   
200    def _is_multiline_start_extended_comment(self, line, line_number):
201        m = self._search(self.extended_comment_start_sig_re, line)
202        if m is None:
203            return False
204        text = m['line'][m['end']:]
205        del m['start'], m['end'], m['line']
206        m['objtype'] = 'extended comment'
207        m['start_line'] = line_number
208        self.ec = dict(m)    # container for extended comment data
209        self.ec['text'] = [text]
210        self.state_stack.append(self.state)
211        self.state = 'extended comment'
212        return True
213
214    extended_comment_end_sig_re = re.compile(non_greedy_whitespace
215                                                + extended_comment_match
216                                                + non_greedy_whitespace
217                                                + r'#' + non_greedy_filler
218                                                + string_end,
219                                                re.IGNORECASE|re.VERBOSE)
220
221    def _is_multiline_end_extended_comment(self, line, line_number):
222        m = self._search(self.extended_comment_end_sig_re, line)
223        if m is None:
224            return False
225        text = m['line'][:m['start']]
226        self.ec['text'].append(text)
227        self.ec['text'] = '\n'.join(self.ec['text'])
228        self.ec['end_line'] = line_number
229        self.findings.append(dict(self.ec))
230        self.state = self.state_stack.pop()
231        del self.ec
232        return True
233   
234    def _search(self, regexp, line):
235        '''regular expression search of line, returns a match as a dictionary or None'''
236        m = regexp.search(line)
237        if m is None:
238            return None
239        # TODO: define a parent key somehow
240        d = {
241            'start': m.start(1),
242            'end':   m.end(1),
243            'text':  m.group(1),
244            'line':  line,
245            'filename':  self.filename,
246        }
247        return d
248
249    def __str__(self):
250        s = []
251        for r in self.findings:
252            s.append( '' )
253            t = '%s %s %d %d %s' % ('.. ' + '*'*20, 
254                                    r['objtype'], 
255                                    r['start_line'], 
256                                    r['end_line'], 
257                                    '*'*20)
258            s.append( t )
259            s.append( '' )
260            s.append( r['text'] )
261        return '\n'.join(s)
262
263    def ReST(self):
264        """create the ReStructured Text from what has been found"""
265        if self.state == 'parsed':
266            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
267           
268        s = []
269        declarations = []
270        for r in self.findings:
271            if r['objtype'] == 'extended comment':
272                s.append( '' )
273                s.append( '.. %s %s %d %d' % (self.filename, 
274                                              r['objtype'], 
275                                              r['start_line'], 
276                                              r['end_line']) )
277                s.append( '' )
278                s.append(r['text'])
279            elif r['objtype'] in ('local', 'global', 'constant'):
280                declarations.append(r)      # remember, show this later
281            # TODO: other objtypes
282        if len(declarations) > 0:
283            col_keys = ('text', 'objtype', 'start_line', 'end_line', )
284            widths = dict([( key, len(str(key)) ) for key in col_keys])
285            for d in declarations:
286                widths = dict([( key, max(w, len(str(d[key])))) for key, w in widths.items()])
287            separator = " ".join( ["="*widths[key] for key in col_keys] )
288            fmt = " ".join( ["%%-%ds"%widths[key] for key in col_keys] )
289            s.append( '' )
290#            s.append( '.. rubric:: Variable Declarations:' )
291            s.append( 'Variable Declarations' )
292            s.append( '=====================' )
293            s.append( '' )
294            s.append( separator )
295            s.append( fmt % tuple([str(key) for key in col_keys]) )
296            s.append( separator )
297            for d in declarations:
298                s.append( fmt % tuple([str(d[key]) for key in col_keys]) )
299            s.append( separator )
300        return '\n'.join(s)
301
302
303if __name__ == '__main__':
304    filelist = [
305        '../macros/test-battery.mac',
306        '../macros/cdef-examples.mac',
307        '../macros/shutter.mac',
308    ]
309    for item in filelist:
310        p = SpecMacrofileParser(item)
311        #print p
312        print p.ReST()
Note: See TracBrowser for help on using the repository browser.