source: specdomain/src/specdomain/sphinxcontrib/specmacrofileparser.py @ 966

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

refs #8, commas allowed in variable declarations as delimiter, reduce code smell in parser state engine, aggregate test macro suite

  • 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.5 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-06-25 02:31:10 +0000 (Mon, 25 Jun 2012) $
5# $Author: jemian $
6# $Revision: 966 $
7# $HeadURL: specdomain/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 966 2012-06-25 02:31:10Z 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# TODO: handle "#: " indicating a description of a variable on the preceding line
82
83class SpecMacrofileParser:
84    '''
85    Parse a SPEC macro file for macro definitions,
86    variable declarations, and extended comments.
87
88        Since 2002, SPEC has allowed for triple-quoted
89        strings as extended comments.  Few, if any, have used them.
90        Assume all extended comments contain ReST formatted comments,
91        *including initial section titles or transitions*.
92        The first and simplest thing to do is to read the .mac file and only extract
93        all the extended comments and add them as nodes to the current document.
94       
95    An additional step would be to parse for:
96    * def
97    * cdef
98    * rdef
99    * global    (done)
100    * local    (done)
101    * constant    (done)
102    * array
103    * ...
104    '''
105
106    # consider using:  docutils.statemachine here
107    states = (                  # assume SPEC def macros cannot be nested
108        'global',               # the level that provides the SPEC command prompt
109        'extended comment',     # inside a multiline extended comment
110        'def macro',            # inside a multiline def macro definition
111        'rdef macro',           # inside a multiline rdef macro definition
112        'cdef macro',           # inside a multiline cdef macro definition
113    )
114
115    def __init__(self, macrofile):
116        '''
117        Constructor
118        '''
119        self.buf = None
120        self.findings = []
121        self.filename = None
122        self.read(macrofile)
123        self.parse_macro_file()
124   
125    def read(self, filename):
126        """
127        load the SPEC macro source code file into an internal buffer
128       
129        :param str filename: name (with optional path) of SPEC macro file
130            (The path is relative to the ``.rst`` document.)
131        """
132        if not os.path.exists(filename):
133            raise RuntimeError, "file not found: " + filename
134        self.filename = filename
135        self.buf = open(filename, 'r').read()
136
137    def parse_macro_file(self):
138        """
139        parse the internal buffer
140        """
141        line_number = 0
142        self.state = 'global'
143        self.state_stack = []
144        for line in self.buf.split('\n'):
145            m = None
146            if self.state not in self.states:
147                raise RuntimeError, "unexpected parser state: " + self.state
148            line_number += 1
149            if self.state == 'global':
150
151                if self._is_lgc_variable(line, line_number):
152                    continue
153
154                if self._is_one_line_extended_comment(line, line_number):
155                    continue
156
157                if self._is_multiline_start_extended_comment(line, line_number):
158                    continue
159
160            elif self.state == 'extended comment':
161
162                if not self._is_multiline_end_extended_comment(line, line_number):
163                    # multiline extended comment continues
164                    self.ec['text'].append(line)
165                continue
166
167            elif self.state == 'def macro':
168                pass
169
170            elif self.state == 'cdef macro':
171                pass
172
173            elif self.state == 'rdef macro':
174                pass
175   
176    def _is_lgc_variable(self, line, line_number):
177        ''' local, global, or constant variable declaration '''
178        m = self._match(lgc_variable_sig_re, line)
179        if m is None:
180            return False
181        objtype, args = lgc_variable_sig_re.match(line).groups()
182        pos = args.find('#')
183        if pos > -1:
184            args = args[:pos]
185        m['objtype'] = objtype
186        m['start_line'] = m['end_line'] = line_number
187        del m['start'], m['end'], m['line']
188        if objtype == 'constant':
189            var, _ = args.split()
190            m['text'] = var.rstrip(',')
191            self.findings.append(dict(m))
192        else:
193            # TODO: consider not indexing "global" inside a def
194            # TODO: consider not indexing "local" at global level
195            for var in args.split():
196                m['text'] = var.rstrip(',')
197                self.findings.append(dict(m))
198                # TODO: to what is this local?  (remember the def it belongs to)
199        return True
200   
201    def _is_one_line_extended_comment(self, line, line_number):
202        m = self._match(extended_comment_block_sig_re, line)
203        if m is None:
204            return False
205        del m['start'], m['end'], m['line']
206        m['objtype'] = 'extended comment'
207        m['start_line'] = m['end_line'] = line_number
208        m['text'] = m['text'].strip()
209        self.findings.append(dict(m))
210        return True
211   
212    def _is_multiline_start_extended_comment(self, line, line_number):
213        m = self._match(extended_comment_start_sig_re, line)
214        if m is None:
215            return False
216        text = m['line'][m['end']:]
217        del m['start'], m['end'], m['line']
218        m['objtype'] = 'extended comment'
219        m['start_line'] = line_number
220        self.ec = dict(m)    # container for extended comment data
221        self.ec['text'] = [text]
222        self.state_stack.append(self.state)
223        self.state = 'extended comment'
224        return True
225
226    def _is_multiline_end_extended_comment(self, line, line_number):
227        m = self._match(extended_comment_end_sig_re, line)
228        if m is None:
229            return False
230        text = m['line'][:m['start']]
231        self.ec['text'].append(text)
232        self.ec['text'] = '\n'.join(self.ec['text'])
233        self.ec['end_line'] = line_number
234        self.findings.append(dict(self.ec))
235        self.state = self.state_stack.pop()
236        del self.ec
237        return True
238   
239    def _match(self, regexp, line):
240        m = regexp.search(line)
241        if m is None:
242            return None
243        d = {
244            'start': m.start(1),
245            'end':   m.end(1),
246            'text':  m.group(1),
247            'line':  line,
248            'filename':  self.filename,
249        }
250        return d
251
252    def __str__(self):
253        s = []
254        for r in self.findings:
255            s.append( '' )
256            t = '%s %s %d %d %s' % ('.. ' + '*'*20, 
257                                    r['objtype'], 
258                                    r['start_line'], 
259                                    r['end_line'], 
260                                    '*'*20)
261            s.append( t )
262            s.append( '' )
263            s.append( r['text'] )
264        return '\n'.join(s)
265
266    def ReST(self):
267        """create the ReStructured Text from what has been found"""
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 = {}
285            for key in col_keys:
286                widths[key] = len( str(key) )
287            for d in declarations:
288                for key, w in widths.items():
289                    widths[key] = max(w, len( str(d[key]) ) )
290            separator = " ".join( ["="*widths[key] for key in col_keys] )
291            format = " ".join( ["%%-%ds"%widths[key] for key in col_keys] )
292            s.append( '' )
293            s.append( '.. rubric:: Variable Declarations:' )
294            s.append( '' )
295            s.append( separator )
296            #s.append( " ".join( [str(key) for key in col_keys]) )
297            s.append( format % tuple([str(key) for key in col_keys]) )
298            s.append( separator )
299            for d in declarations:
300                s.append( format % tuple([str(d[key]) for key in col_keys]) )
301            s.append( separator )
302        return '\n'.join(s)
303
304
305if __name__ == '__main__':
306    filelist = [
307        '../macros/test-battery.mac',
308        '../macros/cdef-examples.mac',
309        '../macros/shutter.mac',
310    ]
311    for item in filelist:
312        p = SpecMacrofileParser(item)
313        #print p
314        print p.ReST()
Note: See TracBrowser for help on using the repository browser.