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

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

fixes #35 and #36

  • 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: 28.5 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-10-01 18:08:46 +0000 (Mon, 01 Oct 2012) $
5# $Author: jemian $
6# $Revision: 1124 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 1124 2012-10-01 18:08:46Z 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        #@UnusedImport
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'\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
86arg_list_match = r'(\(' + non_greedy_filler + r'\))?'
87
88spec_macro_declaration_match_re = re.compile(
89                        string_start
90                        + r'\s*?'                           # optional blank space
91                        + r'(r?def)\s'                      # 1: def_type (rdef | def)
92                        + non_greedy_whitespace
93                        + macro_name_match                  # 2: macro_name
94                        + arg_list_match                    # 3: optional arguments
95                        + non_greedy_whitespace
96                        + r"[\\']+"                          # start body section
97                        + non_greedy_filler_match           # 4: body
98                        + r"[\\']+"                          # end body section
99                        + non_greedy_whitespace
100                        + r'(#.*?)?'                        # 5: optional comment
101                        + string_end
102                        , 
103                        re.IGNORECASE|re.DOTALL|re.MULTILINE)
104
105
106
107args_match_re = re.compile(
108                          r'\('
109                        + arglist_match                     # 1:  argument list
110                        + r'\)', 
111                        re.DOTALL)
112
113class SpecMacrofileParser:
114    '''
115    Parse a SPEC macro file for macro definitions,
116    variable declarations, and extended comments.
117
118        Since 2002, SPEC has allowed for triple-quoted
119        strings as extended comments.  Few, if any, have used them.
120        Assume all extended comments contain ReST formatted comments,
121        *including initial section titles or transitions*.
122   
123    Assume macro definitions are not nested (but test for this).
124   
125    Assume macro files are small enough to load completely in memory.
126       
127    An additional step would be to parse for:
128    * def
129    * cdef
130    * rdef
131    * global
132    * local
133    * constant
134    * array
135    * ...
136    '''
137   
138    def __init__(self, macrofile):
139        self.buf = None
140        self.findings = []
141        self.filename = None
142        self.read(macrofile)
143        self.parse_macro_file()
144        self.description = ''
145        self.clear_description = False
146        self.found_first_global_extended_comment = False
147   
148    def read(self, macrofile):
149        """
150        load the SPEC macro source code file into an internal buffer (self.buf).
151        Also remember the start and end position of each line (self.line_positions).
152       
153        :param str filename: name (with optional path) of SPEC macro file
154            (The path is relative to the ``.rst`` document.)
155        """
156        if not os.path.exists(macrofile):
157            raise RuntimeError, "file not found: " + macrofile
158        self.filename = macrofile
159        buf = open(macrofile, 'r').readlines()
160        offset = 0
161        lines = []
162        for linenumber, line in enumerate(buf):
163            end = offset+len(line)
164            lines.append([linenumber+1, offset, end])
165            offset = end
166        self.buf = ''.join(buf)
167        self.line_positions = lines
168   
169    def std_read(self, macrofile):
170        """
171        load the SPEC macro source code file into an internal buffer
172       
173        :param str filename: name (with optional path) of SPEC macro file
174            (The path is relative to the ``.rst`` document.)
175        """
176        if not os.path.exists(macrofile):
177            raise RuntimeError, "file not found: " + macrofile
178        self.filename = macrofile
179        self.buf = open(macrofile, 'r').read()
180
181    def parse_macro_file(self):
182        """
183        Figure out what can be documented in the file's contents (in self.buf)
184       
185            each of the list_something() methods returns a
186            list of dictionaries where each dictionary
187            has the keys: objtype, start_line, end_line, and others
188        """
189        db = self._make_db()        # first, the file parsing
190       
191        # Build a dict with objecttype for keys and methods for values
192        # each method handles that particular spec macro file structure
193        handler_method = {
194            'cdef': self.handle_other,
195            'constant': self.handle_other,
196            'def': self.handle_def,
197            'descriptive comment': self.handle_descriptive_comment,
198            'extended comment': self.handle_extended_comment,
199            'function def': self.handle_def,
200            'global': self.handle_other,
201            'local': self.handle_other,
202            'rdef': self.handle_other,
203            'function rdef': self.handle_def,
204        }
205        process_first_list = ('descriptive comment', )
206       
207        # then analyze what was found
208        # proceed line-by-line in order
209        # TODO: could override this rule with a sort-order option
210        self.findings = []
211        self.description = ''
212        self.clear_description = False
213        self.found_first_global_extended_comment = False
214        for linenumber in sorted(db.keys()):
215            # Diagnostic line for development only
216            #print linenumber, ':', ' '.join(['<%s>' % d['objtype'] for d in db[linenumber]])
217           
218            # process any descriptive comment first
219            for item in db[linenumber]:
220                if item['objtype'] in process_first_list:
221                    handler_method[item['objtype']](item, db)
222            # now process the others
223            for item in db[linenumber]:
224                if item['objtype'] not in process_first_list:
225                    if 'function rdef' == item['objtype']:
226                        pass
227                    handler_method[item['objtype']](item, db)
228           
229            if self.clear_description:
230                self.description, self.clear_description = '', False
231   
232    def _make_db(self):
233        """build the db dict by parsing for each type of structure"""
234        db = {}
235        # first, the file parsing
236        for func in (self.list_def_macros, 
237                     self.list_cdef_macros,
238                     self.list_variables,
239                     self.list_extended_comments,
240                     self.list_descriptive_comments,
241                     ):
242            for item in func():
243                s = item['start_line']
244                if s not in db.keys():
245                    db[s] = []
246                db[s].append(item)
247        return db
248
249    def handle_def(self, node, db):
250        """document SPEC def structures"""
251        # identify all the children of this node
252        parent = node['name']
253        self.found_first_local_extended_comment = False
254        if node.get('comment') is not None:
255            node['description'] = node.get('comment').lstrip('#:').strip()
256        if len(self.description)>0:
257            node['description'] = self.description
258        for row in xrange(node['start_line']+1, node['end_line']-1):
259            if row in db.keys():
260                for item in db[row]:
261                    item['parent'] = parent
262                    if item['objtype'] == 'extended comment':
263                        if not self.found_first_local_extended_comment:
264                            # TODO: could override this rule with an option
265                            node['description'] = item['text']
266                            self.found_first_local_extended_comment = False
267        if not node['name'].startswith('_'):
268            # TODO: could override this rule with an option
269            self.findings.append(node)
270        node['summary'] = self._extract_summary(node.get('description', ''))
271        self.clear_description = True
272   
273    def handle_descriptive_comment(self, node, db):
274        """document SPEC descriptive comment structures"""
275        self.description = node['text']
276   
277    def handle_extended_comment(self, node, db):
278        """document SPEC extended comment structures"""
279        #start = node['start_line']
280        if node['parent'] == None:
281            if not self.found_first_global_extended_comment:
282                # TODO: could override this rule with an option
283                self.findings.append(node)
284                self.found_first_global_extended_comment = True
285   
286    def handle_other(self, node, db):
287        """document SPEC cdef, constant, global, local, and rdef structures"""
288        if len(self.description)>0:
289            node['description'] = self.description
290            node['summary'] = self._extract_summary(self.description)
291            self.clear_description = True
292        if not node['name'].startswith('_'):
293            # TODO: could override this rule with an option
294            self.findings.append(node)
295   
296#    def _handle_ignore(self, node, db):
297#        """call this handler to ignore an identified SPEC macro file structure"""
298#        pass
299
300    def _extract_summary(self, description):
301        """
302        return the short summary line from the item description text
303       
304        The summary line is the first line in the docstring,
305        such as the line above.
306       
307        For our purposes now, we return the first paragraph,
308        if it is not a parameter block such as ``:param var: ...``.
309        """
310        if len(description) == 0:
311            return ''
312        text = []
313        for line in description.strip().splitlines():
314            if len(line.strip()) == 0:
315                break
316            if not line.strip().startswith(':'):
317                text.append(line)
318        return ' '.join(text)
319
320    def list_extended_comments(self):
321        """
322        parse the internal buffer for triple-quoted strings, possibly multiline
323       
324        Usually, an extended comment is used at the top of a macro file
325        to describe the file's contents.  It is also used at the top
326        of a macro definition to describe the macro.  The first line
327        of an extended comment for a macro should be a short summary,
328        followed by a blank line, then either a parameter list or
329        more extensive documentation, as needed.
330        """
331        items = []
332        for mo in extended_comment_block_sig_re.finditer(self.buf):
333            start = self.find_pos_in_line_number(mo.start(1))
334            end = self.find_pos_in_line_number(mo.end(1))
335            text = mo.group(1)
336            items.append({
337                            'start_line': start, 
338                            'end_line':   end, 
339                            'objtype':    'extended comment',
340                            'text':       text,
341                            'parent':     None,
342                          })
343        return items
344
345    def list_descriptive_comments(self):
346        """
347        Descriptive comments are used to document items that cannot contain
348        extended comments (triple-quoted strings) such as variable declarations
349        or *rdef* or *cdef* macro declarations.  They appear either in-line
350        with the declaration or on the preceding line.
351       
352        Descriptive comment example that documents *tth*, a global variable declaration::
353           
354            global tth    #: two-theta, the scattering angle
355       
356        Descriptive comment example that documents *ccdset_shutter*, a *rdef* declaration::
357       
358            #: clear the ccd shutter handler
359            rdef ccdset_shutter ''
360        """
361        items = []
362        for mo in variable_description_re.finditer(self.buf):
363            start = self.find_pos_in_line_number(mo.start(1))
364            end = self.find_pos_in_line_number(mo.end(1))
365            items.append({
366                            'start_line': start, 
367                            'end_line':   end, 
368                            'objtype':    'descriptive comment',
369                            'text':       mo.group(1),
370                            'parent':     None,
371                          })
372        return items
373
374    def list_variables(self):
375        """
376        parse the internal buffer for local, global, and constant variable declarations
377        """
378        items = []
379        for mo in lgc_variable_sig_re.finditer(self.buf):
380            start = self.find_pos_in_line_number(mo.start(1))
381            end = self.find_pos_in_line_number(mo.end(1))
382            objtype = mo.group(1)
383            content = mo.group(2)
384            p = content.find('#')
385            if p >= 0:                                      # strip off any comment
386                content = content[:p]
387            content = re.sub('[,;]', ' ', content)          # replace , or ; with blank space
388            if content.find('[') >= 0:
389                content = re.sub('\s*?\[', '[', content)    # remove blank space before [
390            if objtype in ('constant'):
391                name = content.strip().split()[0]
392                items.append({
393                                'start_line': start, 
394                                'end_line':   end, 
395                                'objtype':    objtype,
396                                'name':       name,
397                                'parent':     None,
398                              })
399            else:
400                for var in variable_name_re.finditer(content):
401                    name = var.group(1)
402                    if len(name) > 0:
403                        items.append({
404                                        'start_line': start, 
405                                        'end_line':   end, 
406                                        'objtype':    objtype,
407                                        'name':       name,
408                                        'parent':     None,
409                                      })
410        return items
411
412    def list_def_macros(self):
413        """
414        parse the internal buffer for def and rdef macro declarations
415        """
416        items = []
417        for mo in spec_macro_declaration_match_re.finditer(self.buf):
418            objtype = mo.group(1)
419            start = self.find_pos_in_line_number(mo.start(1))
420            end = self.find_pos_in_line_number(mo.end(4))
421            args = mo.group(3)
422            # TODO: What if args is multi-line?  flatten.  What if really long?
423            if args is not None:
424                if len(args)>2:
425                    m = args_match_re.search(args)
426                    if m is not None:
427                        objtype = 'function ' + objtype
428                        if 'function rdef' == objtype:
429                            pass  # TODO: Should we do something special here?
430                        args = m.group(1)
431            d = {
432                'start_line': start, 
433                'end_line':   end, 
434                'objtype':    objtype,
435                'name':       mo.group(2),
436                'args':       str(args),
437                'body':       mo.group(4),
438                'comment':    mo.group(5),
439                'parent':     None,
440            }
441            items.append(d)
442        return items
443
444    def list_cdef_macros(self):
445        """
446        parse the internal buffer for cdef macro declarations
447        """
448        # too complicated for a regular expression, just look for the initial part
449        items = []
450        for mo in re.finditer('cdef\s*?\(', self.buf):
451            # look at each potential cdef declaration
452            objtype = 'cdef'
453            start = self.find_pos_in_line_number(mo.start())
454            s = p = mo.end()                # offset s for start of args
455            nesting = 1                     # number of nested parentheses
456            sign = {'(': 1, ')': -1}        # increment or decrement
457            while nesting > 0 and p < len(self.buf):
458                if self.buf[p] in sign.keys():
459                    nesting += sign[self.buf[p]]
460                p += 1
461            e = p
462            text = self.buf[s:e-1]    # carve it out, and remove cdef( ... ) wrapping
463            end = self.find_pos_in_line_number(e)
464            p = text.find(',')
465            name = text[:p].strip('"')
466            if len(name) == 0:
467                name = '<empty name>'
468            args = text[p+1:]
469            # TODO: parse "args" for content
470            # TODO: What if args is multi-line?  convert \n to ;
471            #   args = ';'.join(args.splitlines())  # WRONG: This converts string content, as well
472            # TODO: What if args is really long?
473            items.append({
474                            'start_line': start, 
475                            'end_line':   end, 
476                            'objtype':    objtype,
477                            'name':       name,
478                            'args':       args,
479#                            'body':       mo.group(4),
480#                            'comment':    mo.group(5),
481                            'parent':     None,
482                          })
483        return items
484
485    def find_pos_in_line_number(self, pos):
486        """
487        find the line number that includes *pos*
488       
489        :param int pos: position in the file
490        """
491        # TODO: optimize this straight search using a search by bisection
492        linenumber = None
493        for linenumber, start, end in self.line_positions:
494            if start <= pos < end:
495                break
496        return linenumber
497   
498    #------------------------ reporting section below ----------------------------------
499
500    def _simple_ReST_renderer(self):
501        """create a simple ReStructured Text rendition of the findings"""
502        declarations = []       # variables and constants
503        macros = []             # def, cdef, and rdef macros
504        functions = []          # def and rdef function macros
505        s = []
506        for r in self.findings:
507            # TODO: need to define subsections such as these:
508            #    Summary (if present)
509            #    Documentation (if present)
510            #    Declarations
511            #    Tables
512            if r['objtype'] == 'extended comment':
513                # TODO: apply rules to suppress reporting under certain circumstances
514                s.append( '' )
515                s.append( '.. %s %s %d %d' % (self.filename, 
516                                              r['objtype'], 
517                                              r['start_line'], 
518                                              r['end_line']) )
519                s.append( '' )
520                s.append(r['text'])
521                s.append( '' )
522#                s.append( '-'*10 )
523#                s.append( '' )
524            elif r['objtype'] in ('def', 'rdef', 'cdef', ):
525                # TODO: apply rules to suppress reporting under certain circumstances
526                macros.append(r)
527                s.append( '' )
528                s.append( '.. %s %s %s %d %d' % (self.filename, 
529                                              r['objtype'], 
530                                              r['name'], 
531                                              r['start_line'], 
532                                              r['end_line']) )
533                s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name'],) )
534                s.append('')
535                s.append(' '*4 + '*' + r['objtype'] + ' macro declaration*')
536                desc = r.get('description', '')
537                if len(desc) > 0:
538                    s.append('')
539                    for line in desc.splitlines():
540                        s.append(' '*4 + line)
541                s.append( '' )
542            elif r['objtype'] in ('function def', 'function rdef',):
543                # TODO: apply rules to suppress reporting under certain circumstances
544                functions.append(r)
545                objtype = r['objtype'].split()[1]
546                s.append( '' )
547                s.append( '.. %s %s %s %d %d' % (self.filename, 
548                                              objtype, 
549                                              r['name'], 
550                                              r['start_line'], 
551                                              r['end_line']) )
552                s.append( '.. spec:%s:: %s(%s)' % ( objtype, r['name'], r['args']) )
553                s.append('')
554                s.append(' '*4 + '*' + r['objtype'].split()[1] + '() macro function declaration*')
555                desc = r.get('description', '')
556                if len(desc) > 0:
557                    s.append('')
558                    for line in desc.splitlines():
559                        s.append(' '*4 + line)
560                s.append( '' )
561           
562            # Why document local variables in a global scope?
563            elif r['objtype'] in ('global', 'constant'):
564                # TODO: apply rules to suppress reporting under certain circumstances
565                declarations.append(r)
566                if r.get('parent') is None:
567                    s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name']) )
568                    s.append('')
569                    if r['objtype'] in ('constant'):
570                        s.append(' '*4 + '*constant declaration*')
571                    else:
572                        s.append(' '*4 + '*' + r['objtype'] + ' variable declaration*')
573                    desc = r.get('description', '')
574                    if len(desc) > 0:
575                        s.append('')
576                        for line in desc.splitlines():
577                            s.append(' '*4 + line)
578                    s.append( '' )
579
580#        s.append( '-'*10 )
581#        s.append( '' )
582
583        s += _report_table('Variable Declarations (%s)' % self.filename, declarations, 
584                          ('objtype', 'name', 'start_line', 'summary', ))
585        s += _report_table('Macro Declarations (%s)' % self.filename, macros, 
586                          ('objtype', 'name', 'start_line', 'end_line', 'summary', ))
587        s += _report_table('Function Macro Declarations (%s)' % self.filename, functions, 
588                          ('objtype', 'name', 'start_line', 'end_line', 'args', 'summary', ))
589        #s += _report_table('Findings from .mac File', self.findings, ('start_line', 'objtype', 'line', 'summary', ))
590
591        return '\n'.join(s)
592
593    def ReST(self, style = 'simple'):
594        """create the ReStructured Text from what has been found"""
595   
596        # allow for additional renderers, selectable by options
597        renderer_dict =  {'simple': self._simple_ReST_renderer,}
598        if style not in renderer_dict:
599            raise RuntimeWarning, "%s renderer not found, using `simple`" % style
600        return renderer_dict[style]()
601
602
603def _report_table(title, itemlist, col_keys = ('objtype', 'start_line', 'end_line', )):
604    """
605    return the itemlist as a reST table
606   
607    :param str title:  section heading above the table
608    :param {str,str} itemlist: database (keyed dictionary) to use for table
609    :param [str] col_keys: column labels (must be keys in the dictionary)
610    :returns [str]: the table (where each list item is a string of reST)
611    """
612    if len(itemlist) == 0:
613        return []
614    rows = []
615    last_line = None
616    for d in itemlist:
617        if d['start_line'] != last_line:
618            rowdata = [str(d.get(key,'')).strip() for key in col_keys]
619            rows.append( tuple(rowdata) )
620        last_line = d['start_line']
621    return make_rest_table(title, col_keys, rows, '=')
622
623
624def make_rest_table(title, labels, rows, titlechar = '='):
625    """
626    build a reST table
627   
628    :param str title: placed in a section heading above the table
629    :param [str] labels: columns labels
630    :param [[str]] rows: 2-D grid of data, len(labels) == len(data[i]) for all i
631    :param str titlechar: character to use when underlining title as reST section heading
632    :returns [str]: each list item is reST
633    """
634    # this is commented out since it causes a warning when building:
635    #  specmacrofileparser.py:docstring of sphinxcontrib.specmacrofileparser.make_rest_table:14: WARNING: Block quote ends without a blank line; unexpected unindent.
636    # -----
637    #    """
638    #    build a reST table
639    #       
640    #    :param str title: placed in a section heading above the table
641    #    :param [str] labels: columns labels
642    #    :param [[str]] rows: 2-D grid of data, len(labels) == len(data[i]) for all i
643    #    :param str titlechar: character to use when underlining title as reST section heading
644    #    :returns [str]: each list item is reST
645    #
646    #    Example::
647    #       
648    #        title = 'This is a reST table'
649    #        labels = ('name', 'phone', 'email')
650    #        rows = [
651    #                ['Snoopy',           '12345', 'dog@house'],
652    #                ['Red Baron',        '65432', 'fokker@triplane'],
653    #                ['Charlie Brown',    '12345', 'main@house'],
654    #        ]
655    #        print '\n'.join(make_rest_table(title, labels, rows, titlechar='~'))
656    #
657    #    This results in this reST::
658    #   
659    #        This is a reST table
660    #        ~~~~~~~~~~~~~~~~~~~~
661    #       
662    #        ============= ===== ===============
663    #        name          phone email         
664    #        ============= ===== ===============
665    #        Snoopy        12345 dog@house     
666    #        Red Baron     65432 fokker@triplane
667    #        Charlie Brown 12345 main@house     
668    #        ============= ===== ===============
669    #   
670    #    """
671    s = []
672    if len(rows) == 0:
673        return s
674    if len(labels) > 0:
675        columns = zip(labels, *rows)
676    else:
677        columns = zip(*rows)
678    widths = [max([len(item) for item in row]) for row in columns]
679    separator = " ".join( ['='*key for key in widths] )
680    fmt = " ".join( '%%-%ds' % key for key in widths )
681    s.append( '' )
682    s.append( title )
683    s.append( titlechar*len(title) )
684    s.append( '' )
685    s.append( separator )
686    if len(labels) > 0:
687        s.append( fmt % labels )
688        s.append( separator )
689    s.extend( [fmt % tuple(row) for row in rows] )
690    s.append( separator )
691    return s
692
693
694TEST_DIR = os.path.join('..', 'macros')
695
696
697if __name__ == '__main__':
698    filelist = [f for f in sorted(os.listdir(TEST_DIR)) if f.endswith('.mac')]
699    for item in filelist:
700        filename = os.path.join(TEST_DIR, item)
701        print filename
702        p = SpecMacrofileParser(filename)
703        print p.ReST()
704        #pprint (p.findings)
Note: See TracBrowser for help on using the repository browser.