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

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

cleanups

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