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

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

convert parser to read multiline blocks rather single lines

  • 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: 16.5 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-07-10 21:20:00 +0000 (Tue, 10 Jul 2012) $
5# $Author: jemian $
6# $Revision: 1001 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 1001 2012-07-10 21:20:00Z jemian $
9########### SVN repository information ###################
10
11
12"""
13Construct a SPEC macro source code file parser for
14use by the specdomain for Sphinx.  This parser locates
15code blocks in the SPEC macro source code file across multiple lines.
16
17:copyright: Copyright 2012 by BCDA, Advanced Photon Source, Argonne National Laboratory
18:license: ANL Open Source License, see LICENSE for details.
19"""
20
21import os
22import re
23from pprint import pprint
24
25#   http://www.txt2re.com/index-python.php3
26#  http://regexpal.com/
27
28string_start                = r'^'
29string_end                  = r'$'
30match_all                   = r'.*'
31non_greedy_filler           = match_all + r'?'
32non_greedy_whitespace       = r'\s*?'
33#double_quote_string_match   = r'("' + non_greedy_filler + r'")'
34#prog_name_match             = r'([a-z_]\w*)'
35#word_match                  = r'((?:[a-z_]\w*))'
36#cdef_match                  = r'(cdef)'
37extended_comment_marker     = r'\"{3}'
38extended_comment_match      = r'(' + extended_comment_marker + r')'
39macro_name                  = r'[a-zA-Z_][\w_]*'
40macro_name_match            = r'(' + macro_name + r')'
41arglist_match               = r'(' + match_all + r')'
42non_greedy_filler_match     = r'(' + non_greedy_filler + r')'
43variable_name_match         = r'(@?' + macro_name + r'\[?\]?)'
44
45class SpecMacrofileParser:
46    '''
47    Parse a SPEC macro file for macro definitions,
48    variable declarations, and extended comments.
49
50        Since 2002, SPEC has allowed for triple-quoted
51        strings as extended comments.  Few, if any, have used them.
52        Assume all extended comments contain ReST formatted comments,
53        *including initial section titles or transitions*.
54   
55    Assume macro definitions are not nested (but test for this).
56   
57    Assume macro files are small enough to load completely in memory.
58       
59    An additional step would be to parse for:
60    * def
61    * cdef
62    * rdef
63    * global
64    * local
65    * constant
66    * array
67    * ...
68    '''
69   
70    def __init__(self, macrofile):
71        self.buf = None
72        self.findings = []
73        self.filename = None
74        self.read(macrofile)
75        self.parse_macro_file()
76   
77    def read(self, macrofile):
78        """
79        load the SPEC macro source code file into an internal buffer.
80        Also remember the start and end position of each line.
81       
82        :param str filename: name (with optional path) of SPEC macro file
83            (The path is relative to the ``.rst`` document.)
84        """
85        if not os.path.exists(macrofile):
86            raise RuntimeError, "file not found: " + macrofile
87        self.filename = macrofile
88        buf = open(macrofile, 'r').readlines()
89        offset = 0
90        lines = []
91        for linenumber, line in enumerate(buf):
92            end = offset+len(line)
93            lines.append([linenumber+1, offset, end])
94            offset = end
95        self.buf = ''.join(buf)
96        self.line_positions = lines
97   
98    def std_read(self, macrofile):
99        """
100        load the SPEC macro source code file into an internal buffer
101       
102        :param str filename: name (with optional path) of SPEC macro file
103            (The path is relative to the ``.rst`` document.)
104        """
105        if not os.path.exists(macrofile):
106            raise RuntimeError, "file not found: " + macrofile
107        self.filename = macrofile
108        self.buf = open(macrofile, 'r').read()
109
110    def parse_macro_file(self):
111        self.findings = []
112        self.findings.extend(self.find_extended_comments())
113        self.findings.extend(self.find_def_macro())
114        vd = self.find_variable_descriptions()
115        if len(vd) > 0:
116            self.findings.extend(vd)
117        self.findings.extend(self.find_variables())
118        # TODO: decide the parent for each item, expect all def are at global scope
119        # TODO: decide which macros and variables should not be documented
120       
121    extended_comment_block_sig_re = re.compile(
122                            string_start
123                            + non_greedy_whitespace
124                            + extended_comment_marker
125                            + r'(' + non_greedy_filler + r')'
126                            + extended_comment_marker
127                            + non_greedy_filler
128                            + string_end, 
129                            re.IGNORECASE|re.DOTALL|re.MULTILINE)
130
131    def find_extended_comments(self):
132        """
133        parse the internal buffer for triple-quoted strings, possibly multiline
134        """
135        items = []
136        for mo in self.extended_comment_block_sig_re.finditer(self.buf):
137            start = self.find_line_pos(mo.start(1))
138            end = self.find_line_pos(mo.end(1))
139            text = mo.group(1)
140            items.append({
141                            'start_line': start, 
142                            'end_line':   end, 
143                            'objtype':    'extended comment',
144                            'text':       text,
145                            'parent':     None,
146                          })
147        return items
148       
149    variable_description_re = re.compile(
150                            string_start
151                            + non_greedy_filler
152                            + r'#:'
153                            + non_greedy_whitespace
154                            + r'(' + non_greedy_filler + r')'
155                            + non_greedy_whitespace
156                            + string_end, 
157                            re.IGNORECASE|re.DOTALL|re.MULTILINE)
158
159    def find_variable_descriptions(self):
160        """
161        parse the internal buffer for variable descriptions that look like::
162       
163            #: two-theta, the scattering angle
164            global tth
165        """
166        items = []
167        for mo in self.variable_description_re.finditer(self.buf):
168            start = self.find_line_pos(mo.start(1))
169            end = self.find_line_pos(mo.end(1))
170            items.append({
171                            'start_line': start, 
172                            'end_line':   end, 
173                            'objtype':    'variable description',
174                            'text':       mo.group(1),
175                            'parent':     None,
176                          })
177        return items
178   
179    lgc_variable_sig_re = re.compile(
180                            r''
181                            + string_start
182                            + non_greedy_whitespace
183                            + r'(local|global|constant)'        # 1: object type
184                            + non_greedy_whitespace
185                            + r'(' + non_greedy_filler + r')'   # 2: too complicated to parse all at once
186                            + string_end
187                            , 
188                            re.DOTALL
189                            |re.MULTILINE
190                        )
191   
192    variable_name_re = re.compile(
193                            variable_name_match, 
194                            re.IGNORECASE|re.DOTALL|re.MULTILINE
195                            )
196
197    def find_variables(self):
198        """
199        parse the internal buffer for local, global, and constant variable declarations
200        """
201        items = []
202        for mo in self.lgc_variable_sig_re.finditer(self.buf):
203            start = self.find_line_pos(mo.start(1))
204            end = self.find_line_pos(mo.end(1))
205            objtype = mo.group(1)
206            content = mo.group(2)
207            p = content.find('#')
208            if p >= 0:                              # strip off any comment
209                content = content[:p]
210            content = re.sub('[,;]', ' ', content)  # replace , or ; with blank space
211            if content.find('[') >= 0:
212                content = re.sub('\s*?\[', '[', content)  # remove blank space before [
213            for var in self.variable_name_re.finditer(content):
214                name = var.group(1)
215                if len(name) > 0:
216                    items.append({
217                                    'start_line': start, 
218                                    'end_line':   end, 
219                                    'objtype':    objtype,
220                                    'name':       name,
221                                    'parent':     None,
222                                    'text':     'FIX in find_variables(self):',
223                                  })
224        return items
225
226    spec_macro_declaration_match_re = re.compile(
227                            string_start
228                            + r'\s*?'                           # optional blank space
229                            + r'(r?def)'                        # 1: def_type (rdef | def)
230                            + non_greedy_whitespace
231                            + macro_name_match                  # 2: macro_name
232                            + non_greedy_filler_match           # 3: optional arguments
233                            + r'\'\{?'                          # start body section
234                            + non_greedy_filler_match           # 4: body
235                            + r'\}?\''                          # end body section
236                            + r'(#.*?)?'                        # 5: optional comment
237                            + string_end, 
238                            re.IGNORECASE|re.DOTALL|re.MULTILINE)
239       
240    args_match = re.compile(
241                              r'\('
242                            + arglist_match                     # 1:  argument list
243                            + r'\)', 
244                            re.DOTALL)
245
246    def find_def_macro(self):
247        """
248        parse the internal buffer for def and rdef macro declarations
249        """
250        items = []
251        for mo in self.spec_macro_declaration_match_re.finditer(self.buf):
252            objtype = mo.group(1)
253            start = self.find_line_pos(mo.start(1))
254            end = self.find_line_pos(mo.end(4))
255            args = mo.group(3)
256            if len(args)>2:
257                m = self.args_match.search(args)
258                if m is not None:
259                    objtype = 'function ' + objtype
260                    args = m.group(1)
261            items.append({
262                            'start_line': start, 
263                            'end_line':   end, 
264                            'objtype':    objtype,
265                            'name':       mo.group(2),
266                            'args':       args,
267                            'body':       mo.group(4),
268                            'comment':    mo.group(5),
269                            'parent':     None,
270                          })
271        return items
272
273    def find_line_pos(self, pos):
274        """
275        find the line number that includes *pos*
276       
277        :param int pos: position in the file
278        """
279        # straight search
280        # TODO: optimize using search by bisection
281        linenumber = None
282        for linenumber, start, end in self.line_positions:
283            if pos >= start and pos < end:
284                break
285        return linenumber
286
287    def ReST(self):
288        """create the ReStructured Text from what has been found"""
289#        if not self.state == 'parsed':
290#            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
291        return self._simple_ReST_renderer()
292
293    def _simple_ReST_renderer(self):
294        """create a simple ReStructured Text rendition of the findings"""
295#        if not self.state == 'parsed':
296#            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
297           
298        declarations = []       # variables and constants
299        macros = []             # def, cdef, and rdef macros
300        functions = []          # def and rdef function macros
301        s = []
302        for r in self.findings:
303            if r['objtype'] == 'extended comment':
304                # TODO: apply rules to suppress reporting under certain circumstances
305                s.append( '' )
306                s.append( '.. %s %s %d %d' % (self.filename, 
307                                              r['objtype'], 
308                                              r['start_line'], 
309                                              r['end_line']) )
310                s.append( '' )
311                s.append(r['text'])
312            elif r['objtype'] in ('def', 'rdef', 'cdef', ):
313                # TODO: apply rules to suppress reporting under certain circumstances
314                macros.append(r)
315                s.append( '' )
316                s.append( '.. %s %s %s %d %d' % (self.filename, 
317                                              r['objtype'], 
318                                              r['name'], 
319                                              r['start_line'], 
320                                              r['end_line']) )
321                s.append( '' )
322                # TODO: make this next be part of the signature display (in specdomain)
323                s.append( '.. rubric:: %s macro declaration' % r['objtype']  )
324                s.append( '' )
325                s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name'],) )
326            elif r['objtype'] in ('function def', 'function rdef',):
327                # TODO: apply rules to suppress reporting under certain circumstances
328                functions.append(r)
329                s.append( '' )
330                s.append( '.. %s %s %s %d %d' % (self.filename, 
331                                              r['objtype'], 
332                                              r['name'], 
333                                              r['start_line'], 
334                                              r['end_line']) )
335                s.append( '' )
336                s.append( '.. rubric:: %s macro function declaration' % r['objtype']  )
337                s.append( '' )
338                s.append( '.. spec:%s:: %s(%s)' % ( r['objtype'], r['name'], r['args']) )
339            elif r['objtype'] in ('local', 'global', 'constant'):
340                # TODO: apply rules to suppress reporting under certain circumstances
341                del r['text']
342                declarations.append(r)
343
344        s += report_table('Variable Declarations (%s)' % self.filename, declarations, ('objtype', 'name', 'start_line', ))
345        s += report_table('Macro Declarations (%s)' % self.filename, macros, ('objtype', 'name', 'start_line', 'end_line'))
346        s += report_table('Function Macro Declarations (%s)' % self.filename, functions, ('objtype', 'name', 'start_line', 'end_line', 'args'))
347        #s += report_table('Findings from .mac File', self.findings, ('start_line', 'objtype', 'line',))
348
349        return '\n'.join(s)
350
351
352def report_table(title, itemlist, col_keys = ('objtype', 'start_line', 'end_line', )):
353    """
354    return the itemlist as a reST table
355   
356    :param str title:  section heading above the table
357    :param {str,str} itemlist: database (keyed dictionary) to use for table
358    :param [str] col_keys: column labels (must be keys in the dictionary)
359    :returns [str]: the table (where each list item is a string of reST)
360    """
361    if len(itemlist) == 0:
362        return []
363    rows = []
364    last_line = None
365    for d in itemlist:
366        if d['start_line'] != last_line:
367            rows.append( tuple([str(d[key]).strip() for key in col_keys]) )
368        last_line = d['start_line']
369    return make_table(title, col_keys, rows, '=')
370
371
372def make_table(title, labels, rows, titlechar = '='):
373    """
374    build a reST table (internal routine)
375   
376    :param str title: placed in a section heading above the table
377    :param [str] labels: columns labels
378    :param [[str]] rows: 2-D grid of data, len(labels) == len(data[i]) for all i
379    :param str titlechar: character to use when underlining title as reST section heading
380    :returns [str]: each list item is reST
381    """
382    s = []
383    if len(rows) == 0:
384        return s
385    if len(labels) > 0:
386        columns = zip(labels, *rows)
387    else:
388        columns = zip(*rows)
389    widths = [max([len(item) for item in row]) for row in columns]
390    separator = " ".join( ['='*key for key in widths] )
391    fmt = " ".join( '%%-%ds' % key for key in widths )
392    s.append( '' )
393    s.append( title )
394    s.append( titlechar*len(title) )
395    s.append( '' )
396    s.append( separator )
397    if len(labels) > 0:
398        s.append( fmt % labels )
399        s.append( separator )
400    s.extend( fmt % row for row in rows )
401    s.append( separator )
402    return s
403
404
405TEST_DIR = os.path.join('..', 'macros')
406
407
408if __name__ == '__main__':
409    filelist = [f for f in sorted(os.listdir(TEST_DIR)) if f.endswith('.mac')]
410    for item in filelist:
411        filename = os.path.join(TEST_DIR, item)
412        print filename
413        p = SpecMacrofileParser(filename)
414        print p.ReST()
415        pprint (p.findings)
Note: See TracBrowser for help on using the repository browser.