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

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

refs #8, now shows docstrings, need to improve the directive and role handling now, especially with regard to the signature

  • 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: 22.8 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-07-11 21:20:16 +0000 (Wed, 11 Jul 2012) $
5# $Author: jemian $
6# $Revision: 1004 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 1004 2012-07-11 21:20:16Z 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
45
46       
47extended_comment_block_sig_re = re.compile(
48                        string_start
49                        + non_greedy_whitespace
50                        + extended_comment_marker
51                        + r'(' + non_greedy_filler + r')'
52                        + extended_comment_marker
53                        + non_greedy_filler
54                        + string_end, 
55                        re.IGNORECASE|re.DOTALL|re.MULTILINE)
56
57variable_description_re = re.compile(
58                        string_start
59                        + non_greedy_filler
60                        + r'#:'
61                        + non_greedy_whitespace
62                        + r'(' + non_greedy_filler + r')'
63                        + non_greedy_whitespace
64                        + string_end, 
65                        re.IGNORECASE|re.DOTALL|re.MULTILINE)
66
67   
68lgc_variable_sig_re = re.compile(
69                        r''
70                        + string_start
71                        + non_greedy_whitespace
72                        + r'(local|global|constant)'        # 1: object type
73                        + non_greedy_whitespace
74                        + r'(' + non_greedy_filler + r')'   # 2: too complicated to parse all at once
75                        + string_end
76                        , 
77                        re.DOTALL
78                        |re.MULTILINE
79                    )
80
81variable_name_re = re.compile(
82                        variable_name_match, 
83                        re.IGNORECASE|re.DOTALL|re.MULTILINE
84                        )
85
86spec_macro_declaration_match_re = re.compile(
87                        string_start
88                        + r'\s*?'                           # optional blank space
89                        + r'(r?def)'                        # 1: def_type (rdef | def)
90                        + non_greedy_whitespace
91                        + macro_name_match                  # 2: macro_name
92                        + non_greedy_filler_match           # 3: optional arguments
93                        + r'\'\{?'                          # start body section
94                        + non_greedy_filler_match           # 4: body
95                        + r'\}?\''                          # end body section
96                        + r'(#.*?)?'                        # 5: optional comment
97                        + string_end, 
98                        re.IGNORECASE|re.DOTALL|re.MULTILINE)
99   
100args_match = re.compile(
101                          r'\('
102                        + arglist_match                     # 1:  argument list
103                        + r'\)', 
104                        re.DOTALL)
105
106class SpecMacrofileParser:
107    '''
108    Parse a SPEC macro file for macro definitions,
109    variable declarations, and extended comments.
110
111        Since 2002, SPEC has allowed for triple-quoted
112        strings as extended comments.  Few, if any, have used them.
113        Assume all extended comments contain ReST formatted comments,
114        *including initial section titles or transitions*.
115   
116    Assume macro definitions are not nested (but test for this).
117   
118    Assume macro files are small enough to load completely in memory.
119       
120    An additional step would be to parse for:
121    * def
122    * cdef
123    * rdef
124    * global
125    * local
126    * constant
127    * array
128    * ...
129    '''
130   
131    def __init__(self, macrofile):
132        self.buf = None
133        self.findings = []
134        self.filename = None
135        self.read(macrofile)
136        self.parse_macro_file()
137   
138    def read(self, macrofile):
139        """
140        load the SPEC macro source code file into an internal buffer (self.buf).
141        Also remember the start and end position of each line (self.line_positions).
142       
143        :param str filename: name (with optional path) of SPEC macro file
144            (The path is relative to the ``.rst`` document.)
145        """
146        if not os.path.exists(macrofile):
147            raise RuntimeError, "file not found: " + macrofile
148        self.filename = macrofile
149        buf = open(macrofile, 'r').readlines()
150        offset = 0
151        lines = []
152        for linenumber, line in enumerate(buf):
153            end = offset+len(line)
154            lines.append([linenumber+1, offset, end])
155            offset = end
156        self.buf = ''.join(buf)
157        self.line_positions = lines
158   
159    def std_read(self, macrofile):
160        """
161        load the SPEC macro source code file into an internal buffer
162       
163        :param str filename: name (with optional path) of SPEC macro file
164            (The path is relative to the ``.rst`` document.)
165        """
166        if not os.path.exists(macrofile):
167            raise RuntimeError, "file not found: " + macrofile
168        self.filename = macrofile
169        self.buf = open(macrofile, 'r').read()
170
171    def parse_macro_file(self):
172        """
173        Figure out what can be documented in the file's contents (in self.buf)
174       
175            each of the list_...() methods returns a
176            list of dictionaries where each dictionary
177            has the keys: objtype, start_line, end_line, and others
178        """
179        db = {}
180        # first, the file parsing
181        for func in (self.list_def_macros, 
182                     self.list_cdef_macros,
183                     self.list_variables,
184                     self.list_extended_comments,
185                     self.list_descriptive_comments,
186                     ):
187            for item in func():
188                s = item['start_line']
189                if s not in db.keys():
190                    db[s] = []
191                db[s].append(item)
192       
193        # then, the analysis of what was found
194        self.findings = []
195        description = ''
196        clear_description = False
197        found_first_global_extended_comment = False
198        for linenumber in sorted(db.keys()):
199            #print linenumber, ':', ', '.join([d['objtype'] for d in db[linenumber]])
200           
201            line = db[linenumber]
202            item = line[-1]
203            if item['objtype'] in ('def', 'function def'):
204                # identify all the children of this item
205                parent = item['name']
206                found_first_local_extended_comment = False
207                for row in xrange(item['start_line']+1, item['end_line']-1):
208                    if row in db.keys():
209                        for thing in db[row]:
210                            thing['parent'] = parent
211                            if thing['objtype'] == 'extended comment':
212                                if not found_first_local_extended_comment:
213                                    # TODO: could override this rule with an option
214                                    item['description'] = thing['text']
215                                    found_first_local_extended_comment = False
216                if not item['name'].startswith('_'):
217                    # TODO: could override this rule with an option
218                    self.findings.append(item)
219           
220            if item['objtype'] == 'extended comment':
221                start = item['start_line']
222                if item['parent'] == None:
223                    if not found_first_global_extended_comment:
224                        # TODO: could override this rule with an option
225                        self.findings.append(item)
226                        found_first_global_extended_comment = True
227
228            if item['objtype'] == 'descriptive comment':
229                description = item['text']
230
231            for item in line:
232                if item['objtype'] in ('local', 'global', 'constant', 'rdef', 'cdef'):
233                    if len(description)>0:
234                        item['description'] = description
235                        clear_description = True
236                    if not item['name'].startswith('_'):
237                        # TODO: could override this rule with an option
238                        self.findings.append(item)
239           
240            if clear_description:
241                description, clear_description = '', False
242
243    def list_extended_comments(self):
244        """
245        parse the internal buffer for triple-quoted strings, possibly multiline
246       
247        Usually, an extended comment is used at the top of a macro file
248        to describe the file's contents.  It is also used at the top
249        of a macro definition to describe the macro.  The first line
250        of an extended comment for a macro should be a short summary,
251        followed by a blank line, then either a parameter list or
252        more extensive documentation, as needed.
253        """
254        items = []
255        for mo in extended_comment_block_sig_re.finditer(self.buf):
256            start = self.find_pos_in_line_number(mo.start(1))
257            end = self.find_pos_in_line_number(mo.end(1))
258            text = mo.group(1)
259            items.append({
260                            'start_line': start, 
261                            'end_line':   end, 
262                            'objtype':    'extended comment',
263                            'text':       text,
264                            'parent':     None,
265                          })
266        return items
267
268    def list_descriptive_comments(self):
269        """
270        Descriptive comments are used to document items that cannot contain
271        extended comments (triple-quoted strings) such as variable declarations
272        or *rdef* or *cdef* macro declarations.  They appear either in-line
273        with the declaration or on the preceding line.
274       
275        Descriptive comment example that documents *tth*, a global variable declaration::
276           
277            global tth    #: two-theta, the scattering angle
278       
279        Descriptive comment example that documents *ccdset_shutter*, a *rdef* declaration::
280       
281            #: clear the ccd shutter handler
282            rdef ccdset_shutter ''
283        """
284        items = []
285        for mo in variable_description_re.finditer(self.buf):
286            start = self.find_pos_in_line_number(mo.start(1))
287            end = self.find_pos_in_line_number(mo.end(1))
288            items.append({
289                            'start_line': start, 
290                            'end_line':   end, 
291                            'objtype':    'descriptive comment',
292                            'text':       mo.group(1),
293                            'parent':     None,
294                          })
295        return items
296
297    def list_variables(self):
298        """
299        parse the internal buffer for local, global, and constant variable declarations
300        """
301        items = []
302        for mo in lgc_variable_sig_re.finditer(self.buf):
303            start = self.find_pos_in_line_number(mo.start(1))
304            end = self.find_pos_in_line_number(mo.end(1))
305            objtype = mo.group(1)
306            content = mo.group(2)
307            p = content.find('#')
308            if p >= 0:                                      # strip off any comment
309                content = content[:p]
310            content = re.sub('[,;]', ' ', content)          # replace , or ; with blank space
311            if content.find('[') >= 0:
312                content = re.sub('\s*?\[', '[', content)    # remove blank space before [
313            for var in variable_name_re.finditer(content):
314                name = var.group(1)
315                if len(name) > 0:
316                    items.append({
317                                    'start_line': start, 
318                                    'end_line':   end, 
319                                    'objtype':    objtype,
320                                    'name':       name,
321                                    'parent':     None,
322                                  })
323        return items
324
325    def list_def_macros(self):
326        """
327        parse the internal buffer for def and rdef macro declarations
328        """
329        items = []
330        for mo in spec_macro_declaration_match_re.finditer(self.buf):
331            objtype = mo.group(1)
332            start = self.find_pos_in_line_number(mo.start(1))
333            end = self.find_pos_in_line_number(mo.end(4))
334            args = mo.group(3)
335            if len(args)>2:
336                m = args_match.search(args)
337                if m is not None:
338                    objtype = 'function ' + objtype
339                    args = m.group(1)
340            # TODO: What if args is multi-line?  flatten.  What if really long?
341            items.append({
342                            'start_line': start, 
343                            'end_line':   end, 
344                            'objtype':    objtype,
345                            'name':       mo.group(2),
346                            'args':       args,
347                            'body':       mo.group(4),
348                            'comment':    mo.group(5),
349                            'parent':     None,
350                          })
351        return items
352
353    def list_cdef_macros(self):
354        """
355        parse the internal buffer for def and rdef macro declarations
356        """
357        # too complicated for a regular expression, just look for the initial part
358        items = []
359        for mo in re.finditer('cdef\s*?\(', self.buf):
360            # look at each potential cdef declaration
361            objtype = 'cdef'
362            start = self.find_pos_in_line_number(mo.start())
363            s = p = mo.end()                # offset s for start of args
364            nesting = 1                     # number of nested parentheses
365            sign = {'(': 1, ')': -1}        # increment or decrement
366            while nesting > 0 and p < len(self.buf):
367                if self.buf[p] in sign.keys():
368                    nesting += sign[self.buf[p]]
369                p += 1
370            e = p
371            text = self.buf[s:e-1]    # carve it out, and remove cdef( ... ) wrapping
372            end = self.find_pos_in_line_number(e)
373            p = text.find(',')
374            name = text[:p].strip('"')
375            if len(name) == 0:
376                name = '<empty name>'
377            args = text[p+1:]
378            # TODO: parse "args" for content
379            # TODO: What if args is multi-line?  convert \n to ;
380            #   args = ';'.join(args.splitlines())  # WRONG: This converts string content, as well
381            # TODO: What if args is really long?
382            items.append({
383                            'start_line': start, 
384                            'end_line':   end, 
385                            'objtype':    objtype,
386                            'name':       name,
387                            'args':       args,
388#                            'body':       mo.group(4),
389#                            'comment':    mo.group(5),
390                            'parent':     None,
391                          })
392        return items
393
394    def find_pos_in_line_number(self, pos):
395        """
396        find the line number that includes *pos*
397       
398        :param int pos: position in the file
399        """
400        # TODO: optimize this straight search using a search by bisection
401        linenumber = None
402        for linenumber, start, end in self.line_positions:
403            if start <= pos < end:
404                break
405        return linenumber
406   
407    #------------------------ reporting section below ----------------------------------
408
409    def ReST(self):
410        """create the ReStructured Text from what has been found"""
411        return self._simple_ReST_renderer()
412
413    def _simple_ReST_renderer(self):
414        """create a simple ReStructured Text rendition of the findings"""
415        declarations = []       # variables and constants
416        macros = []             # def, cdef, and rdef macros
417        functions = []          # def and rdef function macros
418        s = []
419        for r in self.findings:
420            if r['objtype'] == 'extended comment':
421                # TODO: apply rules to suppress reporting under certain circumstances
422                s.append( '' )
423                s.append( '.. %s %s %d %d' % (self.filename, 
424                                              r['objtype'], 
425                                              r['start_line'], 
426                                              r['end_line']) )
427                s.append( '' )
428                s.append(r['text'])
429            elif r['objtype'] in ('def', 'rdef', 'cdef', ):
430                # TODO: apply rules to suppress reporting under certain circumstances
431                macros.append(r)
432                s.append( '' )
433                s.append( '.. %s %s %s %d %d' % (self.filename, 
434                                              r['objtype'], 
435                                              r['name'], 
436                                              r['start_line'], 
437                                              r['end_line']) )
438                s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name'],) )
439                desc = r.get('description', '')
440                if len(desc) > 0:
441                    s.append('')
442                    for line in desc.splitlines():
443                        s.append(' '*4 + line)
444            elif r['objtype'] in ('function def', 'function rdef',):
445                # TODO: apply rules to suppress reporting under certain circumstances
446                functions.append(r)
447                objtype = r['objtype'].split()[1]
448                s.append( '' )
449                s.append( '.. %s %s %s %d %d' % (self.filename, 
450                                              objtype, 
451                                              r['name'], 
452                                              r['start_line'], 
453                                              r['end_line']) )
454                s.append( '.. spec:%s:: %s(%s)' % ( objtype, r['name'], r['args']) )
455                desc = r.get('description', '')
456                if len(desc) > 0:
457                    s.append('')
458                    for line in desc.splitlines():
459                        s.append(' '*4 + line)
460            elif r['objtype'] in ('local', 'global', 'constant'):
461                # TODO: apply rules to suppress reporting under certain circumstances
462                declarations.append(r)
463                s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name']) )
464                desc = r.get('description', '')
465                if len(desc) > 0:
466                    s.append('')
467                    for line in desc.splitlines():
468                        s.append(' '*4 + line)
469
470        s += _report_table('Variable Declarations (%s)' % self.filename, declarations, 
471                          ('objtype', 'name', 'start_line',))
472        s += _report_table('Macro Declarations (%s)' % self.filename, macros, 
473                          ('objtype', 'name', 'start_line', 'end_line'))
474        s += _report_table('Function Macro Declarations (%s)' % self.filename, functions, 
475                          ('objtype', 'name', 'start_line', 'end_line', 'args'))
476        #s += _report_table('Findings from .mac File', self.findings, ('start_line', 'objtype', 'line',))
477
478        return '\n'.join(s)
479
480
481def _report_table(title, itemlist, col_keys = ('objtype', 'start_line', 'end_line', )):
482    """
483    return the itemlist as a reST table
484   
485    :param str title:  section heading above the table
486    :param {str,str} itemlist: database (keyed dictionary) to use for table
487    :param [str] col_keys: column labels (must be keys in the dictionary)
488    :returns [str]: the table (where each list item is a string of reST)
489    """
490    if len(itemlist) == 0:
491        return []
492    rows = []
493    last_line = None
494    for d in itemlist:
495        if d['start_line'] != last_line:
496            rows.append( tuple([str(d[key]).strip() for key in col_keys]) )
497        last_line = d['start_line']
498    return make_rest_table(title, col_keys, rows, '=')
499
500
501def make_rest_table(title, labels, rows, titlechar = '='):
502    """
503    build a reST table
504   
505    :param str title: placed in a section heading above the table
506    :param [str] labels: columns labels
507    :param [[str]] rows: 2-D grid of data, len(labels) == len(data[i]) for all i
508    :param str titlechar: character to use when underlining title as reST section heading
509    :returns [str]: each list item is reST
510   
511    Example::
512
513        title = 'This is a reST table'
514        labels = ('name', 'phone', 'email')
515        rows = [
516                ['Snoopy',           '12345', 'dog@house'],
517                ['Red Baron',        '65432', 'fokker@triplane'],
518                ['Charlie Brown',    '12345', 'main@house'],
519                ]
520        print '\n'.join(make_rest_table(title, labels, rows))
521   
522    This results in this reST::
523   
524        This is a reST table
525        ====================
526       
527        ============= ===== ===============
528        name          phone email         
529        ============= ===== ===============
530        Snoopy        12345 dog@house     
531        Red Baron     65432 fokker@triplane
532        Charlie Brown 12345 main@house     
533        ============= ===== ===============
534    """
535    s = []
536    if len(rows) == 0:
537        return s
538    if len(labels) > 0:
539        columns = zip(labels, *rows)
540    else:
541        columns = zip(*rows)
542    widths = [max([len(item) for item in row]) for row in columns]
543    separator = " ".join( ['='*key for key in widths] )
544    fmt = " ".join( '%%-%ds' % key for key in widths )
545    s.append( '' )
546    s.append( title )
547    s.append( titlechar*len(title) )
548    s.append( '' )
549    s.append( separator )
550    if len(labels) > 0:
551        s.append( fmt % labels )
552        s.append( separator )
553    s.extend( [fmt % tuple(row) for row in rows] )
554    s.append( separator )
555    return s
556
557
558TEST_DIR = os.path.join('..', 'macros')
559
560
561if __name__ == '__main__':
562    filelist = [f for f in sorted(os.listdir(TEST_DIR)) if f.endswith('.mac')]
563    for item in filelist:
564        filename = os.path.join(TEST_DIR, item)
565        print filename
566        p = SpecMacrofileParser(filename)
567        print p.ReST()
568        #pprint (p.findings)
Note: See TracBrowser for help on using the repository browser.