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

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

refs #8, now handle multi-line cdef

  • 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.7 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-07-11 17:58:32 +0000 (Wed, 11 Jul 2012) $
5# $Author: jemian $
6# $Revision: 1002 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 1002 2012-07-11 17:58:32Z 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        ext_com = self.find_extended_comments()
112        desc_com = self.find_descriptive_comments()
113        def_macro = self.find_def_macro()
114        cdef_macro = self.find_cdef_macro()
115        vars = self.find_variables()
116       
117        for linenumber in range(len(self.line_positions)):
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            # walk through the line numbers in the file
121            #  if a def_macro starts, note its name and set the parent field
122            #     of all comments, variables, var_desc, rdef, and cdef within
123            #     its start_line and end_line range
124            #  How to handle descriptive comments?
125            pass
126       
127        self.findings = []
128        for item in (ext_com, desc_com, def_macro, cdef_macro, vars,):
129            if len(item)>0:
130                self.findings.extend(item)
131       
132    extended_comment_block_sig_re = re.compile(
133                            string_start
134                            + non_greedy_whitespace
135                            + extended_comment_marker
136                            + r'(' + non_greedy_filler + r')'
137                            + extended_comment_marker
138                            + non_greedy_filler
139                            + string_end, 
140                            re.IGNORECASE|re.DOTALL|re.MULTILINE)
141
142    def find_extended_comments(self):
143        """
144        parse the internal buffer for triple-quoted strings, possibly multiline
145        """
146        items = []
147        for mo in self.extended_comment_block_sig_re.finditer(self.buf):
148            start = self.find_pos_in_line_number(mo.start(1))
149            end = self.find_pos_in_line_number(mo.end(1))
150            text = mo.group(1)
151            items.append({
152                            'start_line': start, 
153                            'end_line':   end, 
154                            'objtype':    'extended comment',
155                            'text':       text,
156                            'parent':     None,
157                          })
158        return items
159       
160    variable_description_re = re.compile(
161                            string_start
162                            + non_greedy_filler
163                            + r'#:'
164                            + non_greedy_whitespace
165                            + r'(' + non_greedy_filler + r')'
166                            + non_greedy_whitespace
167                            + string_end, 
168                            re.IGNORECASE|re.DOTALL|re.MULTILINE)
169
170    def find_descriptive_comments(self):
171        """
172        Descriptive comments are used to document items that cannot contain
173        extended comments (triple-quoted strings) such as variable declarations
174        or *rdef* or *cdef* macro declarations.  They appear either in-line
175        with the declaration or on the preceding line.
176       
177        Descriptive comment example that documents *tth*, a global variable declaration::
178           
179            global tth    #: two-theta, the scattering angle
180       
181        Descriptive comment example that documents *ccdset_shutter*, a *rdef* declaration::
182       
183            #: clear the ccd shutter handler
184            rdef ccdset_shutter ''
185        """
186        items = []
187        for mo in self.variable_description_re.finditer(self.buf):
188            start = self.find_pos_in_line_number(mo.start(1))
189            end = self.find_pos_in_line_number(mo.end(1))
190            items.append({
191                            'start_line': start, 
192                            'end_line':   end, 
193                            'objtype':    'variable description',
194                            'text':       mo.group(1),
195                            'parent':     None,
196                          })
197        return items
198   
199    lgc_variable_sig_re = re.compile(
200                            r''
201                            + string_start
202                            + non_greedy_whitespace
203                            + r'(local|global|constant)'        # 1: object type
204                            + non_greedy_whitespace
205                            + r'(' + non_greedy_filler + r')'   # 2: too complicated to parse all at once
206                            + string_end
207                            , 
208                            re.DOTALL
209                            |re.MULTILINE
210                        )
211   
212    variable_name_re = re.compile(
213                            variable_name_match, 
214                            re.IGNORECASE|re.DOTALL|re.MULTILINE
215                            )
216
217    def find_variables(self):
218        """
219        parse the internal buffer for local, global, and constant variable declarations
220        """
221        items = []
222        for mo in self.lgc_variable_sig_re.finditer(self.buf):
223            start = self.find_pos_in_line_number(mo.start(1))
224            end = self.find_pos_in_line_number(mo.end(1))
225            objtype = mo.group(1)
226            content = mo.group(2)
227            p = content.find('#')
228            if p >= 0:                              # strip off any comment
229                content = content[:p]
230            content = re.sub('[,;]', ' ', content)  # replace , or ; with blank space
231            if content.find('[') >= 0:
232                content = re.sub('\s*?\[', '[', content)  # remove blank space before [
233            for var in self.variable_name_re.finditer(content):
234                name = var.group(1)
235                if len(name) > 0:
236                    items.append({
237                                    'start_line': start, 
238                                    'end_line':   end, 
239                                    'objtype':    objtype,
240                                    'name':       name,
241                                    'parent':     None,
242                                    'text':     'FIX in find_variables(self):',
243                                  })
244        return items
245
246    spec_macro_declaration_match_re = re.compile(
247                            string_start
248                            + r'\s*?'                           # optional blank space
249                            + r'(r?def)'                        # 1: def_type (rdef | def)
250                            + non_greedy_whitespace
251                            + macro_name_match                  # 2: macro_name
252                            + non_greedy_filler_match           # 3: optional arguments
253                            + r'\'\{?'                          # start body section
254                            + non_greedy_filler_match           # 4: body
255                            + r'\}?\''                          # end body section
256                            + r'(#.*?)?'                        # 5: optional comment
257                            + string_end, 
258                            re.IGNORECASE|re.DOTALL|re.MULTILINE)
259       
260    args_match = re.compile(
261                              r'\('
262                            + arglist_match                     # 1:  argument list
263                            + r'\)', 
264                            re.DOTALL)
265
266    def find_def_macro(self):
267        """
268        parse the internal buffer for def and rdef macro declarations
269        """
270        items = []
271        for mo in self.spec_macro_declaration_match_re.finditer(self.buf):
272            objtype = mo.group(1)
273            start = self.find_pos_in_line_number(mo.start(1))
274            end = self.find_pos_in_line_number(mo.end(4))
275            args = mo.group(3)
276            if len(args)>2:
277                m = self.args_match.search(args)
278                if m is not None:
279                    objtype = 'function ' + objtype
280                    args = m.group(1)
281            # TODO: What if args is multi-line?  flatten.  What if really long?
282            items.append({
283                            'start_line': start, 
284                            'end_line':   end, 
285                            'objtype':    objtype,
286                            'name':       mo.group(2),
287                            'args':       args,
288                            'body':       mo.group(4),
289                            'comment':    mo.group(5),
290                            'parent':     None,
291                          })
292        return items
293
294    def find_cdef_macro(self):
295        """
296        parse the internal buffer for def and rdef macro declarations
297        """
298       
299        # note:  It is not possible to find properly all variations
300        # of the argument list in a cdef declaration using a regular expression,
301        # especially across multiple lines.
302       
303        items = []
304        for mo in re.finditer('cdef\(', self.buf):
305            # look at each potential cdef declaration
306            objtype = 'cdef'
307            s = mo.start()
308            start = self.find_pos_in_line_number(s)
309            p = mo.end()
310            nesting = 1                     # number of nested parentheses
311            sign = {'(': 1, ')': -1}        # increment or decrement
312            while nesting > 0 and p < len(self.buf):
313                if self.buf[p] in sign.keys():
314                    nesting += sign[self.buf[p]]
315                p += 1
316            e = p
317            text = self.buf[s+5:e-1]    # carve it out, and remove cdef( ... ) wrapping
318            end = self.find_pos_in_line_number(e)
319            p = text.find(',')
320            name = text[:p].strip('"')
321            if len(name) == 0:
322                name = '<empty name>'
323            args = text[p+1:]
324            # TODO: parse "args" for content
325            # TODO: What if args is multi-line?  convert \n to ;
326            #   args = ';'.join(args.splitlines())  # WRONG: This converts string content, as well
327            # TODO: What if args is really long?
328            items.append({
329                            'start_line': start, 
330                            'end_line':   end, 
331                            'objtype':    objtype,
332                            'name':       name,
333                            'args':       args,
334#                            'body':       mo.group(4),
335#                            'comment':    mo.group(5),
336                            'parent':     None,
337                          })
338        return items
339
340    def find_pos_in_line_number(self, pos):
341        """
342        find the line number that includes *pos*
343       
344        :param int pos: position in the file
345        """
346        # straight search
347        # TODO: optimize using search by bisection
348        linenumber = None
349        for linenumber, start, end in self.line_positions:
350            if start <= pos < end:
351                break
352        return linenumber
353   
354    #------------------------ reporting section below ----------------------------------
355
356    def ReST(self):
357        """create the ReStructured Text from what has been found"""
358#        if not self.state == 'parsed':
359#            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
360        return self._simple_ReST_renderer()
361
362    def _simple_ReST_renderer(self):
363        """create a simple ReStructured Text rendition of the findings"""
364#        if not self.state == 'parsed':
365#            raise RuntimeWarning, "state = %s, should be 'parsed'" % self.filename
366           
367        declarations = []       # variables and constants
368        macros = []             # def, cdef, and rdef macros
369        functions = []          # def and rdef function macros
370        s = []
371        for r in self.findings:
372            if r['objtype'] == 'extended comment':
373                # TODO: apply rules to suppress reporting under certain circumstances
374                s.append( '' )
375                s.append( '.. %s %s %d %d' % (self.filename, 
376                                              r['objtype'], 
377                                              r['start_line'], 
378                                              r['end_line']) )
379                s.append( '' )
380                s.append(r['text'])
381            elif r['objtype'] in ('def', 'rdef', 'cdef', ):
382                # TODO: apply rules to suppress reporting under certain circumstances
383                macros.append(r)
384                s.append( '' )
385                s.append( '.. %s %s %s %d %d' % (self.filename, 
386                                              r['objtype'], 
387                                              r['name'], 
388                                              r['start_line'], 
389                                              r['end_line']) )
390                s.append( '' )
391                # TODO: make this next be part of the signature display (in specdomain)
392                #s.append( '.. rubric:: %s macro declaration' % r['objtype']  )
393                s.append( '' )
394                s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name'],) )
395            elif r['objtype'] in ('function def', 'function rdef',):
396                # TODO: apply rules to suppress reporting under certain circumstances
397                functions.append(r)
398                s.append( '' )
399                s.append( '.. %s %s %s %d %d' % (self.filename, 
400                                              r['objtype'], 
401                                              r['name'], 
402                                              r['start_line'], 
403                                              r['end_line']) )
404                s.append( '' )
405                #s.append( '.. rubric:: %s macro function declaration' % r['objtype']  )
406                s.append( '' )
407                s.append( '.. spec:%s:: %s(%s)' % ( r['objtype'], r['name'], r['args']) )
408            elif r['objtype'] in ('local', 'global', 'constant'):
409                # TODO: apply rules to suppress reporting under certain circumstances
410                del r['text']
411                declarations.append(r)
412
413        s += report_table('Variable Declarations (%s)' % self.filename, declarations, ('objtype', 'name', 'start_line', ))
414        s += report_table('Macro Declarations (%s)' % self.filename, macros, ('objtype', 'name', 'start_line', 'end_line'))
415        s += report_table('Function Macro Declarations (%s)' % self.filename, functions, ('objtype', 'name', 'start_line', 'end_line', 'args'))
416        #s += report_table('Findings from .mac File', self.findings, ('start_line', 'objtype', 'line',))
417
418        return '\n'.join(s)
419
420
421def report_table(title, itemlist, col_keys = ('objtype', 'start_line', 'end_line', )):
422    """
423    return the itemlist as a reST table
424   
425    :param str title:  section heading above the table
426    :param {str,str} itemlist: database (keyed dictionary) to use for table
427    :param [str] col_keys: column labels (must be keys in the dictionary)
428    :returns [str]: the table (where each list item is a string of reST)
429    """
430    if len(itemlist) == 0:
431        return []
432    rows = []
433    last_line = None
434    for d in itemlist:
435        if d['start_line'] != last_line:
436            rows.append( tuple([str(d[key]).strip() for key in col_keys]) )
437        last_line = d['start_line']
438    return make_table(title, col_keys, rows, '=')
439
440
441def make_table(title, labels, rows, titlechar = '='):
442    """
443    build a reST table (internal routine)
444   
445    :param str title: placed in a section heading above the table
446    :param [str] labels: columns labels
447    :param [[str]] rows: 2-D grid of data, len(labels) == len(data[i]) for all i
448    :param str titlechar: character to use when underlining title as reST section heading
449    :returns [str]: each list item is reST
450    """
451    s = []
452    if len(rows) == 0:
453        return s
454    if len(labels) > 0:
455        columns = zip(labels, *rows)
456    else:
457        columns = zip(*rows)
458    widths = [max([len(item) for item in row]) for row in columns]
459    separator = " ".join( ['='*key for key in widths] )
460    fmt = " ".join( '%%-%ds' % key for key in widths )
461    s.append( '' )
462    s.append( title )
463    s.append( titlechar*len(title) )
464    s.append( '' )
465    s.append( separator )
466    if len(labels) > 0:
467        s.append( fmt % labels )
468        s.append( separator )
469    s.extend( fmt % row for row in rows )
470    s.append( separator )
471    return s
472
473
474TEST_DIR = os.path.join('..', 'macros')
475
476
477if __name__ == '__main__':
478    filelist = [f for f in sorted(os.listdir(TEST_DIR)) if f.endswith('.mac')]
479    for item in filelist:
480        filename = os.path.join(TEST_DIR, item)
481        print filename
482        p = SpecMacrofileParser(filename)
483        print p.ReST()
484        pprint (p.findings)
Note: See TracBrowser for help on using the repository browser.