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

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

refs #8, still need to understand how the index is configured in the domain

  • 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: 19.1 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-07-02 20:13:44 +0000 (Mon, 02 Jul 2012) $
5# $Author: jemian $
6# $Revision: 989 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 989 2012-07-02 20:13:44Z 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 (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        return self._simple_ReST_renderer()
372
373    def _simple_ReST_renderer(self):
374        """create a simple ReStructured Text rendition of the findings"""
375        if not self.state == 'parsed':
376            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
377           
378        declarations = []       # variables and constants
379        macros = []             # def, cdef, and rdef macros
380        functions = []          # def and rdef function macros
381        #title = 'Extended Comments'
382        #s = ['', title, '='*len(title), ]
383        s = []
384        for r in self.findings:
385            if r['objtype'] == 'extended comment':
386                s.append( '' )
387                s.append( '.. %s %s %d %d' % (self.filename, 
388                                              r['objtype'], 
389                                              r['start_line'], 
390                                              r['end_line']) )
391                s.append( '' )
392                s.append(r['text'])
393            elif r['objtype'] in ('def', 'rdef', 'cdef', ):
394                macros.append(r)
395                s.append( '' )
396                s.append( '.. %s %s %s %d %d' % (self.filename, 
397                                              r['objtype'], 
398                                              r['name'], 
399                                              r['start_line'], 
400                                              r['end_line']) )
401                s.append( '' )
402                s.append( '.. rubric:: %s macro declaration' % r['objtype']  )
403                s.append( '' )
404                #s.append( '.. spec:%s:: %s %s' % ( r['objtype'], r['name'], r['args'],) )
405                s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name'],) )
406            elif r['objtype'] in ('function def', 'function rdef',):
407                # FIXME:  not getting here, such as for kohzuE_cmd()
408                functions.append(r)
409                s.append( '' )
410                s.append( '.. %s %s %s %d %d' % (self.filename, 
411                                              r['objtype'], 
412                                              r['name'], 
413                                              r['start_line'], 
414                                              r['end_line']) )
415                s.append( '' )
416                s.append( '.. rubric:: %s macro function declaration' % r['objtype']  )
417                s.append( '' )
418                s.append( '.. spec:%s:: %s(%s)' % ( r['objtype'], r['name'], r['args']) )
419            elif r['objtype'] in ('local', 'global', 'constant'):
420                declarations.append(r)
421
422        s += self._report_table('Variable Declarations', declarations)
423        s += self._report_table('Macro Declarations', macros, ('start_line', 'name', 'line',))
424        s += self._report_table('Function Macro Declarations', functions)
425
426        return '\n'.join(s)
427   
428    def _report_table(self, title, itemlist, col_keys = ('start_line', 'line',)):
429        """ return the itemlist as a reST table """
430        s = []
431        if len(itemlist) == 0:
432            return s
433        widths = dict([( key, len(str(key)) ) for key in col_keys])
434        for d in itemlist:
435            widths = dict([( key, max(w, len(str(d[key])))) for key, w in widths.items()])
436        separator = " ".join( ["="*widths[key] for key in col_keys] )
437        fmt = " ".join( ["%%-%ds"%widths[key] for key in col_keys] )
438        s.append( '' )
439        s.append( title )
440        s.append( '='*len(title) )
441        s.append( '' )
442        s.append( separator )
443        s.append( fmt % tuple([str(key.strip()) for key in col_keys]) )
444        s.append( separator )
445        last_line = -1
446        for d in itemlist:
447            if d['start_line'] != last_line:
448                s.append( fmt % tuple([str(d[key]).strip() for key in col_keys]) )
449            last_line = d['start_line']
450        s.append( separator )
451        return s
452
453
454TEST_DIR = os.path.join('..', 'macros')
455
456
457if __name__ == '__main__':
458    filelist = [f for f in sorted(os.listdir(TEST_DIR)) if f.endswith('.mac')]
459    for item in filelist:
460        filename = os.path.join(TEST_DIR, item)
461        print filename
462        p = SpecMacrofileParser(filename)
463        print p.ReST()
Note: See TracBrowser for help on using the repository browser.