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

Last change on this file since 986 was 986, 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: 17.5 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-06-28 23:14:58 +0000 (Thu, 28 Jun 2012) $
5# $Author: jemian $
6# $Revision: 986 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 986 2012-06-28 23:14:58Z 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*?'
32#double_quote_string_match   = r'("' + non_greedy_filler + r'")'
33#prog_name_match             = r'([a-z_]\w*)'
34#word_match                  = r'((?:[a-z_]\w*))'
35#cdef_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                for thing in (self._is_def_macro,
116                              self._is_cdef_macro,
117                              self._is_function_macro,
118                              self._is_lgc_variable,
119                              self._is_one_line_extended_comment,
120                              self._is_multiline_start_extended_comment
121                              ):
122                    if thing(line, line_number):
123                        break
124            elif self.state == 'extended comment':
125                if not self._is_multiline_end_extended_comment(line, line_number):
126                    # multiline extended comment continues
127                    self.ec['text'].append(line)
128                continue
129            elif self.state == 'def macro':
130                pass
131            elif self.state == 'cdef macro':
132                pass
133            elif self.state == 'rdef macro':
134                pass
135       
136        if len(self.state_stack) > 0:
137            fmt = "encountered EOF while parsing %s, line %d, in state %s, stack=%s"
138            msg = fmt % (self.filename, line_number, self.state, self.state_stack)
139            #raise RuntimeWarning, msg
140            print msg
141
142        self.state = 'parsed'
143       
144    lgc_variable_sig_re = re.compile(string_start
145                                        + non_greedy_whitespace
146                                        + r'(local|global|constant)'
147                                        + r'((?:,?\s*@?[\w.eE+-]+\[?\]?)*)'
148                                        + non_greedy_whitespace
149                                        + r'#' + non_greedy_filler
150                                        + string_end, 
151                                        re.VERBOSE)
152
153    def _is_lgc_variable(self, line, line_number):
154        ''' local, global, or constant variable declaration '''
155        m = self._search(self.lgc_variable_sig_re, line)
156        if m is None:
157            return False
158       
159        objtype, args = self.lgc_variable_sig_re.match(line).groups()
160        pos = args.find('#')
161        if pos > -1:
162            args = args[:pos]
163        m['objtype'] = objtype
164        m['start_line'] = m['end_line'] = line_number
165        del m['start'], m['end']
166        if objtype == 'constant':
167            if not len(args.split()) == 2:
168                print "line_number, args: ", line_number, args
169            var, _ = args.split()
170            m['text'] = var.rstrip(',')
171            self.findings.append(dict(m))
172        else:
173            # TODO: consider not indexing "global" inside a def
174            # TODO: consider not indexing "local" at global level
175            #      or leave these decisions for later, including some kind of analyzer
176            for var in args.split():
177                m['text'] = var.rstrip(',')
178                self.findings.append(dict(m))
179                # TODO: to what is this local?  (remember the def it belongs to)
180        return True
181   
182    extended_comment_block_sig_re = re.compile(string_start
183                                                + non_greedy_whitespace
184                                                + extended_comment_marker
185                                                + r'(' + non_greedy_filler + r')'
186                                                + extended_comment_marker
187                                                + non_greedy_filler
188                                                + string_end, 
189                                                re.IGNORECASE|re.DOTALL|re.MULTILINE)
190
191    def _is_one_line_extended_comment(self, line, line_number):
192        m = self._search(self.extended_comment_block_sig_re, line)
193        if m is None:
194            return False
195        line = m['line']
196        del m['start'], m['end']
197        m['objtype'] = 'extended comment'
198        m['start_line'] = m['end_line'] = line_number
199        m['text'] = m['text'].strip()
200        self.findings.append(dict(m))
201        return True
202
203    extended_comment_start_sig_re = re.compile(string_start
204                                                + non_greedy_whitespace
205                                                + extended_comment_match, 
206                                                re.IGNORECASE|re.VERBOSE)
207   
208    def _is_multiline_start_extended_comment(self, line, line_number):
209        m = self._search(self.extended_comment_start_sig_re, line)
210        if m is None:
211            return False
212        line = m['line']
213        text = m['line'][m['end']:]
214        del m['start'], m['end']
215        m['objtype'] = 'extended comment'
216        m['start_line'] = line_number
217        self.ec = dict(m)    # container for extended comment data
218        self.ec['text'] = [text]
219        self.state_stack.append(self.state)
220        self.state = 'extended comment'
221        return True
222
223    extended_comment_end_sig_re = re.compile(non_greedy_whitespace
224                                                + extended_comment_match
225                                                + non_greedy_whitespace
226                                                + r'#' + non_greedy_filler
227                                                + string_end,
228                                                re.IGNORECASE|re.VERBOSE)
229
230    def _is_multiline_end_extended_comment(self, line, line_number):
231        m = self._search(self.extended_comment_end_sig_re, line)
232        if m is None:
233            return False
234        text = m['line'][:m['start']]
235        self.ec['text'].append(text)
236        self.ec['text'] = '\n'.join(self.ec['text'])
237        self.ec['end_line'] = line_number
238        self.findings.append(dict(self.ec))
239        self.state = self.state_stack.pop()
240        del self.ec
241        return True
242
243    spec_macro_declaration_match_re = re.compile(
244                              r'^'                      # line start
245                            + r'\s*?'                   # optional blank space
246                            + r'(r?def)'                # 0: def_type (rdef | def)
247                            + r'\s*?'                   # optional blank space
248                            + r'([a-zA-Z_][\w_]*)'      # 1: macro_name
249                            + r'(.*?)'                  # 2: optional arguments
250                            + r'(#.*?)?'                # 3: optional comment
251                            + r'$'                      # line end
252                        )
253
254    def _is_def_macro(self, line, line_number):
255        m = self._search(self.spec_macro_declaration_match_re, line)
256        if m is None:
257            return False
258        self.ec = dict(m)
259        del self.ec['text']
260        m = self.spec_macro_declaration_match_re.match(line)
261        macrotype, name, args, comment = m.groups()
262        self.ec['start_line'] = line_number
263        self.ec['end_line'] = line_number       # TODO: consider the multiline definition later
264        self.ec['objtype'] = macrotype
265        self.ec['name'] = name
266        self.ec['args'] = args
267        self.ec['comment'] = comment
268        self.findings.append(dict(self.ec))
269        del self.ec
270        return True
271
272    spec_cdef_declaration_match_re = re.compile(
273                              r'^'                      # line start
274                            + r'.*?'                    # optional any kind of preceding stuff, was \s*? (optional blank space)
275                            + r'(cdef)'                 # 0: cdef
276                            + r'\('                     # opening parenthesis
277                            + r'(.*?)'                  # 1: args (anything between the parentheses)
278                            + r'\)'                     # closing parenthesis
279                            + r'.*?'                    # optional any kind of stuff
280                            + r'(#.*?)?'                # 2: optional comment with content
281                            + r'$'                      # line end
282                        )
283
284    def _is_cdef_macro(self, line, line_number):
285        m = self._search(self.spec_cdef_declaration_match_re, line)
286        if m is None:
287            return False
288        self.ec = dict(m)
289        del self.ec['text']
290        m = self.spec_cdef_declaration_match_re.match(line)
291        macrotype, args, comment = m.groups()
292        name = args.split(',')[0].strip('"')
293        self.ec['start_line'] = line_number
294        self.ec['end_line'] = line_number       # TODO: consider the multiline definition later
295        self.ec['objtype'] = macrotype
296        self.ec['name'] = name
297        self.ec['args'] = args
298        self.ec['comment'] = comment
299        self.findings.append(dict(self.ec))
300        del self.ec
301        return True
302
303    spec_function_declaration_match_re = re.compile(
304                              r'^'                      # line start
305                            + r'\s*?'                   # optional blank space
306                            + r'(r?def)'                # 0: def_type (rdef | def)
307                            + r'\s*?'                   # optional blank space
308                            + r'([a-zA-Z_][\w_]*)'      # 1: function_name
309                            + r'\('                     # opening parenthesis
310                            + r'(.*?)'                  # 2: args (anything between the parentheses)
311                            + r'\)'                     # closing parenthesis
312                            + r'\s*?'                   # optional blank space
313                            + r'\''                     # open macro content
314                            + r'(.*?)'                  # 3: content, optional
315                            + r'(#.*?)?'                # 4: optional comment
316                            + r'$'                      # line end
317                        )
318
319    def _is_function_macro(self, line, line_number):
320        m = self._search(self.spec_function_declaration_match_re, line)
321        if m is None:
322            return False
323        self.ec = dict(m)
324        del self.ec['text']
325        m = self.spec_function_declaration_match_re.match(line)
326        macrotype, name, args, content, comment = m.groups()
327        self.ec['start_line'] = line_number
328        self.ec['end_line'] = line_number       # TODO: consider the multiline definition later
329        self.ec['objtype'] = 'function ' + macrotype
330        self.ec['name'] = name
331        self.ec['args'] = args
332        self.ec['content'] = content
333        self.ec['comment'] = comment
334        self.findings.append(dict(self.ec))
335        del self.ec
336        return True
337
338    def _search(self, regexp, line):
339        '''regular expression search of line, returns a match as a dictionary or None'''
340        m = regexp.search(line)
341        if m is None:
342            return None
343        # TODO: define a parent key somehow
344        d = {
345            'start': m.start(1),
346            'end':   m.end(1),
347            'text':  m.group(1),
348            'line':  line,
349            'filename':  self.filename,
350        }
351        return d
352
353    def __str__(self):
354        s = []
355        for r in self.findings:
356            s.append( '' )
357            t = '%s %s %d %d %s' % ('.. ' + '*'*20, 
358                                    r['objtype'], 
359                                    r['start_line'], 
360                                    r['end_line'], 
361                                    '*'*20)
362            s.append( t )
363            s.append( '' )
364            s.append( r['text'] )
365        return '\n'.join(s)
366
367    def ReST(self):
368        """create the ReStructured Text from what has been found"""
369        if not self.state == 'parsed':
370            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
371           
372        s = []
373        declarations = []       # variables and constants
374        macros = []             # def, cdef, and rdef macros
375        functions = []          # def and rdef function macros
376        for r in self.findings:
377            if r['objtype'] == 'extended comment':
378                s.append( '' )
379                s.append( '.. %s %s %d %d' % (self.filename, 
380                                              r['objtype'], 
381                                              r['start_line'], 
382                                              r['end_line']) )
383                s.append( '' )
384                s.append(r['text'])
385            elif r['objtype'] in ('def', 'rdef', 'cdef'):
386                macros.append(r)
387            elif r['objtype'] in ('function def', 'function rdef',):
388                functions.append(r)
389            elif r['objtype'] in ('local', 'global', 'constant'):
390                declarations.append(r)
391
392        s += self._report_table('Variable Declarations', declarations)
393        s += self._report_table('Macro Declarations', macros, ('start_line', 'name', 'line',))
394        s += self._report_table('Function Macro Declarations', functions)
395
396        return '\n'.join(s)
397   
398    def _report_table(self, title, itemlist, col_keys = ('start_line', 'line',)):
399        """ return the itemlist as a reST table """
400        s = []
401        if len(itemlist) == 0:
402            return s
403        widths = dict([( key, len(str(key)) ) for key in col_keys])
404        for d in itemlist:
405            widths = dict([( key, max(w, len(str(d[key])))) for key, w in widths.items()])
406        separator = " ".join( ["="*widths[key] for key in col_keys] )
407        fmt = " ".join( ["%%-%ds"%widths[key] for key in col_keys] )
408        s.append( '' )
409        s.append( title )
410        s.append( '='*len(title) )
411        s.append( '' )
412        s.append( separator )
413        s.append( fmt % tuple([str(key.strip()) for key in col_keys]) )
414        s.append( separator )
415        last_line = -1
416        for d in itemlist:
417            if d['start_line'] != last_line:
418                s.append( fmt % tuple([str(d[key]).strip() for key in col_keys]) )
419            last_line = d['start_line']
420        s.append( separator )
421        return s
422
423
424TEST_DIR = os.path.join('..', 'macros')
425
426
427if __name__ == '__main__':
428    filelist = [f for f in sorted(os.listdir(TEST_DIR)) if f.endswith('.mac')]
429    for item in filelist:
430        filename = os.path.join(TEST_DIR, item)
431        print filename
432        p = SpecMacrofileParser(filename)
433        print p.ReST()
Note: See TracBrowser for help on using the repository browser.