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

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

refs #8, add autospecdir, no options supported yet

  • 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: 20.3 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-07-06 19:15:32 +0000 (Fri, 06 Jul 2012) $
5# $Author: jemian $
6# $Revision: 994 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 994 2012-07-06 19:15: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*?'
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   
52    Assume macro definitions are not nested (but test for this).
53   
54    Assume macro files are small enough to load completely in memory.
55       
56    An additional step would be to parse for:
57    * def    (done)
58    * cdef    (done)
59    * rdef    (done)
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 (
116                              self._is_function_macro,
117                              self._is_def_macro,
118                              self._is_cdef_macro,
119                              self._is_lgc_variable,
120                              self._is_one_line_extended_comment,
121                              self._is_multiline_start_extended_comment
122                              ):
123                    if thing(line, line_number):
124                        break
125            elif self.state == 'extended comment':
126                if not self._is_multiline_end_extended_comment(line, line_number):
127                    # multiline extended comment continues
128                    self.ec['text'].append(line)
129                continue
130            elif self.state == 'def macro':
131                pass
132            elif self.state == 'cdef macro':
133                pass
134            elif self.state == 'rdef macro':
135                pass
136       
137        if len(self.state_stack) > 0:
138            fmt = "encountered EOF while parsing %s, line %d, in state %s, stack=%s"
139            msg = fmt % (self.filename, line_number, self.state, self.state_stack)
140            #raise RuntimeWarning, msg
141            print msg
142
143        self.state = 'parsed'
144       
145    lgc_variable_sig_re = re.compile(string_start
146                                        + non_greedy_whitespace
147                                        + r'(local|global|constant)'
148                                        + r'((?:,?\s*@?[\w.eE+-]+\[?\]?)*)'
149                                        + non_greedy_whitespace
150                                        + r'#' + non_greedy_filler
151                                        + string_end, 
152                                        re.VERBOSE)
153
154    def _is_lgc_variable(self, line, line_number):
155        ''' local, global, or constant variable declaration '''
156        m = self._search(self.lgc_variable_sig_re, line)
157        if m is None:
158            return False
159       
160        objtype, args = self.lgc_variable_sig_re.match(line).groups()
161        pos = args.find('#')
162        if pos > -1:
163            args = args[:pos]
164        m['objtype'] = objtype
165        m['start_line'] = m['end_line'] = line_number
166        del m['start'], m['end']
167        if objtype == 'constant':
168            if not len(args.split()) == 2:
169                print "line_number, args: ", line_number, args
170            var, _ = args.split()
171            m['name'] = var.rstrip(',')
172            self.findings.append(dict(m))
173        else:
174            # TODO: consider not indexing "global" inside a def
175            # TODO: consider not indexing "local" at global level
176            #      or leave these decisions for later, including some kind of analyzer
177            for var in args.split():
178                m['name'] = var.rstrip(',')
179                self.findings.append(dict(m))
180                # TODO: to what is this local?  (remember the def it belongs to)
181        return True
182   
183    extended_comment_block_sig_re = re.compile(string_start
184                                                + non_greedy_whitespace
185                                                + extended_comment_marker
186                                                + r'(' + non_greedy_filler + r')'
187                                                + extended_comment_marker
188                                                + non_greedy_filler
189                                                + string_end, 
190                                                re.IGNORECASE|re.DOTALL|re.MULTILINE)
191
192    def _is_one_line_extended_comment(self, line, line_number):
193        m = self._search(self.extended_comment_block_sig_re, line)
194        if m is None:
195            return False
196        line = m['line']
197        del m['start'], m['end']
198        m['objtype'] = 'extended comment'
199        m['start_line'] = m['end_line'] = line_number
200        m['text'] = m['text'].strip()
201        self.findings.append(dict(m))
202        return True
203
204    extended_comment_start_sig_re = re.compile(string_start
205                                                + non_greedy_whitespace
206                                                + extended_comment_match, 
207                                                re.IGNORECASE|re.VERBOSE)
208   
209    def _is_multiline_start_extended_comment(self, line, line_number):
210        m = self._search(self.extended_comment_start_sig_re, line)
211        if m is None:
212            return False
213        line = m['line']
214        text = m['line'][m['end']:]
215        del m['start'], m['end']
216        m['objtype'] = 'extended comment'
217        m['start_line'] = line_number
218        self.ec = dict(m)    # container for extended comment data
219        self.ec['text'] = [text]
220        self.state_stack.append(self.state)
221        self.state = 'extended comment'
222        return True
223
224    extended_comment_end_sig_re = re.compile(non_greedy_whitespace
225                                                + extended_comment_match
226                                                + non_greedy_whitespace
227                                                + r'#' + non_greedy_filler
228                                                + string_end,
229                                                re.IGNORECASE|re.VERBOSE)
230
231    def _is_multiline_end_extended_comment(self, line, line_number):
232        m = self._search(self.extended_comment_end_sig_re, line)
233        if m is None:
234            return False
235        text = m['line'][:m['start']]
236        self.ec['text'].append(text)
237        self.ec['text'] = '\n'.join(self.ec['text'])
238        self.ec['end_line'] = line_number
239        self.findings.append(dict(self.ec))
240        self.state = self.state_stack.pop()
241        del self.ec
242        return True
243
244    spec_macro_declaration_match_re = re.compile(
245                              r'^'                      # line start
246                            + r'\s*?'                   # optional blank space
247                            + r'(r?def)'                # 0: def_type (rdef | def)
248                            + r'\s*?'                   # optional blank space
249                            + r'([a-zA-Z_][\w_]*)'      # 1: macro_name
250                            + r'(.*?)'                  # 2: optional arguments
251                            + r'(#.*?)?'                # 3: optional comment
252                            + r'$'                      # line end
253                        )
254
255    def _is_def_macro(self, line, line_number):
256        m = self._search(self.spec_macro_declaration_match_re, line)
257        if m is None:
258            return False
259        self.ec = dict(m)
260        del self.ec['text']
261        m = self.spec_macro_declaration_match_re.match(line)
262        macrotype, name, args, comment = m.groups()
263        self.ec['start_line'] = line_number
264        self.ec['end_line'] = line_number       # TODO: consider the multiline definition later
265        self.ec['objtype'] = macrotype
266        self.ec['name'] = name
267        self.ec['args'] = args
268        self.ec['comment'] = comment
269        self.findings.append(dict(self.ec))
270        del self.ec
271        return True
272
273    spec_cdef_declaration_match_re = re.compile(
274                              r'^'                      # line start
275                            + r'.*?'                    # optional any kind of preceding stuff, was \s*? (optional blank space)
276                            + r'(cdef)'                 # 0: cdef
277                            + r'\('                     # opening parenthesis
278                            + r'(.*?)'                  # 1: args (anything between the parentheses)
279                            + r'\)'                     # closing parenthesis
280                            + r'.*?'                    # optional any kind of stuff
281                            + r'(#.*?)?'                # 2: optional comment with content
282                            + r'$'                      # line end
283                        )
284
285    def _is_cdef_macro(self, line, line_number):
286        m = self._search(self.spec_cdef_declaration_match_re, line)
287        if m is None:
288            return False
289        self.ec = dict(m)
290        del self.ec['text']
291        m = self.spec_cdef_declaration_match_re.match(line)
292        macrotype, args, comment = m.groups()
293        name = args.split(',')[0].strip('"')
294        self.ec['start_line'] = line_number
295        self.ec['end_line'] = line_number       # TODO: consider the multiline definition later
296        self.ec['objtype'] = macrotype
297        self.ec['name'] = name
298        self.ec['args'] = args
299        self.ec['comment'] = comment
300        self.findings.append(dict(self.ec))
301        del self.ec
302        return True
303
304    spec_function_declaration_match_re = re.compile(
305                              r'^'                      # line start
306                            + r'\s*?'                   # optional blank space
307                            + r'(r?def)'                # 0: def_type (rdef | def)
308                            + r'\s*?'                   # optional blank space
309                            + r'([a-zA-Z_][\w_]*)'      # 1: function_name
310                            + r'\s*?'                   # optional blank space
311                            + r'\('                     # opening parenthesis
312                            + r'(.*?)'                  # 2: args (anything between the parentheses)
313                            + r'\)'                     # closing parenthesis
314                            + r'\s*?'                   # optional blank space
315                            + r'\''                     # open macro content
316                            + r'(.*?)'                  # 3: content, optional
317                            + r'(#.*?)?'                # 4: optional comment
318                            + r'$'                      # line end
319                        )
320
321    def _is_function_macro(self, line, line_number):
322        m = self._search(self.spec_function_declaration_match_re, line)
323        if m is None:
324            return False
325        self.ec = dict(m)
326        del self.ec['text']
327        m = self.spec_function_declaration_match_re.match(line)
328        macrotype, name, args, content, comment = m.groups()
329        self.ec['start_line'] = line_number
330        self.ec['end_line'] = line_number       # TODO: consider the multiline definition later
331        self.ec['objtype'] = 'function ' + macrotype
332        self.ec['name'] = name
333        self.ec['args'] = args
334        self.ec['content'] = content
335        self.ec['comment'] = comment
336        self.findings.append(dict(self.ec))
337        del self.ec
338        return True
339
340    def _search(self, regexp, line):
341        '''regular expression search of line, returns a match as a dictionary or None'''
342        m = regexp.search(line)
343        if m is None:
344            return None
345        # TODO: define a parent key somehow
346        d = {
347            'start': m.start(1),
348            'end':   m.end(1),
349            'text':  m.group(1),
350            'line':  line,
351            'filename':  self.filename,
352        }
353        return d
354
355    def __str__(self):
356        s = []
357        for r in self.findings:
358            s.append( '' )
359            t = '%s %s %d %d %s' % ('.. ' + '*'*20, 
360                                    r['objtype'], 
361                                    r['start_line'], 
362                                    r['end_line'], 
363                                    '*'*20)
364            s.append( t )
365            s.append( '' )
366            s.append( r['text'] )
367        return '\n'.join(s)
368
369    def ReST(self):
370        """create the ReStructured Text from what has been found"""
371        if not self.state == 'parsed':
372            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
373        return self._simple_ReST_renderer()
374
375    def _simple_ReST_renderer(self):
376        """create a simple ReStructured Text rendition of the findings"""
377        if not self.state == 'parsed':
378            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
379           
380        declarations = []       # variables and constants
381        macros = []             # def, cdef, and rdef macros
382        functions = []          # def and rdef function macros
383        s = []
384        for r in self.findings:
385            if r['objtype'] == 'extended comment':
386                # TODO: apply rules to suppress reporting under certain circumstances
387                s.append( '' )
388                s.append( '.. %s %s %d %d' % (self.filename, 
389                                              r['objtype'], 
390                                              r['start_line'], 
391                                              r['end_line']) )
392                s.append( '' )
393                s.append(r['text'])
394            elif r['objtype'] in ('def', 'rdef', 'cdef', ):
395                # TODO: apply rules to suppress reporting under certain circumstances
396                macros.append(r)
397                s.append( '' )
398                s.append( '.. %s %s %s %d %d' % (self.filename, 
399                                              r['objtype'], 
400                                              r['name'], 
401                                              r['start_line'], 
402                                              r['end_line']) )
403                s.append( '' )
404                # TODO: make this next be part of the signature display (in specdomain)
405                s.append( '.. rubric:: %s macro declaration' % r['objtype']  )
406                s.append( '' )
407                s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name'],) )
408            elif r['objtype'] in ('function def', 'function rdef',):
409                # TODO: apply rules to suppress reporting under certain circumstances
410                functions.append(r)
411                s.append( '' )
412                s.append( '.. %s %s %s %d %d' % (self.filename, 
413                                              r['objtype'], 
414                                              r['name'], 
415                                              r['start_line'], 
416                                              r['end_line']) )
417                s.append( '' )
418                s.append( '.. rubric:: %s macro function declaration' % r['objtype']  )
419                s.append( '' )
420                s.append( '.. spec:%s:: %s(%s)' % ( r['objtype'], r['name'], r['args']) )
421            elif r['objtype'] in ('local', 'global', 'constant'):
422                # TODO: apply rules to suppress reporting under certain circumstances
423                del r['text']
424                declarations.append(r)
425
426        s += report_table('Variable Declarations (%s)' % self.filename, declarations, ('start_line', 'objtype', 'name', 'line',))
427        s += report_table('Macro Declarations (%s)' % self.filename, macros, ('start_line', 'name', 'line',))
428        s += report_table('Function Macro Declarations (%s)' % self.filename, functions)
429        #s += report_table('Findings from .mac File', self.findings, ('start_line', 'objtype', 'line',))
430
431        return '\n'.join(s)
432
433
434def report_table(title, itemlist, col_keys = ('start_line', 'line',)):
435    """
436    return the itemlist as a reST table
437   
438    :param str title:  section heading above the table
439    :param {str,str} itemlist: database (keyed dictionary) to use for table
440    :param [str] col_keys: column labels (must be keys in the dictionary)
441    :returns [str]: the table (where each list item is a string of reST)
442    """
443    if len(itemlist) == 0:
444        return []
445    rows = []
446    last_line = None
447    for d in itemlist:
448        if d['start_line'] != last_line:
449            rows.append( tuple([str(d[key]).strip() for key in col_keys]) )
450        last_line = d['start_line']
451    return make_table(title, col_keys, rows, '=')
452
453
454def make_table(title, labels, rows, titlechar = '='):
455    """
456    build a reST table (internal routine)
457   
458    :param str title: placed in a section heading above the table
459    :param [str] labels: columns labels
460    :param [[str]] rows: 2-D grid of data, len(labels) == len(data[i]) for all i
461    :param str titlechar: character to use when underlining title as reST section heading
462    :returns [str]: each list item is reST
463    """
464    s = []
465    if len(rows) == 0:
466        return s
467    if len(labels) > 0:
468        columns = zip(labels, *rows)
469    else:
470        columns = zip(*rows)
471    widths = [max([len(item) for item in row]) for row in columns]
472    separator = " ".join( ['='*key for key in widths] )
473    fmt = " ".join( '%%-%ds' % key for key in widths )
474    s.append( '' )
475    s.append( title )
476    s.append( titlechar*len(title) )
477    s.append( '' )
478    s.append( separator )
479    if len(labels) > 0:
480        s.append( fmt % labels )
481        s.append( separator )
482    s.extend( fmt % row for row in rows )
483    s.append( separator )
484    return s
485
486
487TEST_DIR = os.path.join('..', 'macros')
488
489
490if __name__ == '__main__':
491    filelist = [f for f in sorted(os.listdir(TEST_DIR)) if f.endswith('.mac')]
492    for item in filelist:
493        filename = os.path.join(TEST_DIR, item)
494        print filename
495        p = SpecMacrofileParser(filename)
496        print p.ReST()
Note: See TracBrowser for help on using the repository browser.