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

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

fixes #37

  • 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.6 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-10-01 18:33:14 +0000 (Mon, 01 Oct 2012) $
5# $Author: jemian $
6# $Revision: 1125 $
7# $HeadURL: specdomain/trunk/src/specdomain/sphinxcontrib/specmacrofileparser.py $
8# $Id: specmacrofileparser.py 1125 2012-10-01 18:33:14Z 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'[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
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            'function rdef': self.handle_def,
201            'global': self.handle_other,
202            'local': self.handle_other,
203            'rdef': self.handle_other,
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 start == 225 and end == 225:
424                pass
425            if args is not None:
426                if len(args)>2:
427                    m = args_match_re.search(args)
428                    if m is not None:
429                        objtype = 'function ' + objtype
430                        if 'function rdef' == objtype:
431                            pass  # TODO: Should we do something special here?
432                        args = m.group(1)
433                elif args == '()':
434                    objtype = 'function ' + objtype
435            d = {
436                'start_line': start, 
437                'end_line':   end, 
438                'objtype':    objtype,
439                'name':       mo.group(2),
440                'args':       str(args),
441                'body':       mo.group(4),
442                'comment':    mo.group(5),
443                'parent':     None,
444            }
445            items.append(d)
446        return items
447
448    def list_cdef_macros(self):
449        """
450        parse the internal buffer for cdef macro declarations
451        """
452        # too complicated for a regular expression, just look for the initial part
453        items = []
454        for mo in re.finditer('cdef\s*?\(', self.buf):
455            # look at each potential cdef declaration
456            objtype = 'cdef'
457            start = self.find_pos_in_line_number(mo.start())
458            s = p = mo.end()                # offset s for start of args
459            nesting = 1                     # number of nested parentheses
460            sign = {'(': 1, ')': -1}        # increment or decrement
461            while nesting > 0 and p < len(self.buf):
462                if self.buf[p] in sign.keys():
463                    nesting += sign[self.buf[p]]
464                p += 1
465            e = p
466            text = self.buf[s:e-1]    # carve it out, and remove cdef( ... ) wrapping
467            end = self.find_pos_in_line_number(e)
468            p = text.find(',')
469            name = text[:p].strip('"')
470            if len(name) == 0:
471                name = '<empty name>'
472            args = text[p+1:]
473            # TODO: parse "args" for content
474            # TODO: What if args is multi-line?  convert \n to ;
475            #   args = ';'.join(args.splitlines())  # WRONG: This converts string content, as well
476            # TODO: What if args is really long?
477            items.append({
478                            'start_line': start, 
479                            'end_line':   end, 
480                            'objtype':    objtype,
481                            'name':       name,
482                            'args':       args,
483#                            'body':       mo.group(4),
484#                            'comment':    mo.group(5),
485                            'parent':     None,
486                          })
487        return items
488
489    def find_pos_in_line_number(self, pos):
490        """
491        find the line number that includes *pos*
492       
493        :param int pos: position in the file
494        """
495        # TODO: optimize this straight search using a search by bisection
496        linenumber = None
497        for linenumber, start, end in self.line_positions:
498            if start <= pos < end:
499                break
500        return linenumber
501   
502    #------------------------ reporting section below ----------------------------------
503
504    def _simple_ReST_renderer(self):
505        """create a simple ReStructured Text rendition of the findings"""
506        declarations = []       # variables and constants
507        macros = []             # def, cdef, and rdef macros
508        functions = []          # def and rdef function macros
509        s = []
510        for r in self.findings:
511            # TODO: need to define subsections such as these:
512            #    Summary (if present)
513            #    Documentation (if present)
514            #    Declarations
515            #    Tables
516            if r['objtype'] == 'extended comment':
517                # TODO: apply rules to suppress reporting under certain circumstances
518                s.append( '' )
519                s.append( '.. %s %s %d %d' % (self.filename, 
520                                              r['objtype'], 
521                                              r['start_line'], 
522                                              r['end_line']) )
523                s.append( '' )
524                s.append(r['text'])
525                s.append( '' )
526#                s.append( '-'*10 )
527#                s.append( '' )
528            elif r['objtype'] in ('def', 'rdef', 'cdef', ):
529                # TODO: apply rules to suppress reporting under certain circumstances
530                macros.append(r)
531                s.append( '' )
532                s.append( '.. %s %s %s %d %d' % (self.filename, 
533                                              r['objtype'], 
534                                              r['name'], 
535                                              r['start_line'], 
536                                              r['end_line']) )
537                s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name'],) )
538                s.append('')
539                s.append(' '*4 + '*' + r['objtype'] + ' macro declaration*')
540                desc = r.get('description', '')
541                if len(desc) > 0:
542                    s.append('')
543                    for line in desc.splitlines():
544                        s.append(' '*4 + line)
545                s.append( '' )
546            elif r['objtype'] in ('function def', 'function rdef',):
547                # TODO: apply rules to suppress reporting under certain circumstances
548                functions.append(r)
549                objtype = r['objtype'].split()[1]
550                s.append( '' )
551                s.append( '.. %s %s %s %d %d' % (self.filename, 
552                                              objtype, 
553                                              r['name'], 
554                                              r['start_line'], 
555                                              r['end_line']) )
556                s.append( '.. spec:%s:: %s(%s)' % ( objtype, r['name'], r['args']) )
557                s.append('')
558                s.append(' '*4 + '*' + r['objtype'].split()[1] + '() macro function declaration*')
559                desc = r.get('description', '')
560                if len(desc) > 0:
561                    s.append('')
562                    for line in desc.splitlines():
563                        s.append(' '*4 + line)
564                s.append( '' )
565           
566            # Why document local variables in a global scope?
567            elif r['objtype'] in ('global', 'constant'):
568                # TODO: apply rules to suppress reporting under certain circumstances
569                declarations.append(r)
570                if r.get('parent') is None:
571                    s.append( '.. spec:%s:: %s' % ( r['objtype'], r['name']) )
572                    s.append('')
573                    if r['objtype'] in ('constant'):
574                        s.append(' '*4 + '*constant declaration*')
575                    else:
576                        s.append(' '*4 + '*' + r['objtype'] + ' variable declaration*')
577                    desc = r.get('description', '')
578                    if len(desc) > 0:
579                        s.append('')
580                        for line in desc.splitlines():
581                            s.append(' '*4 + line)
582                    s.append( '' )
583
584#        s.append( '-'*10 )
585#        s.append( '' )
586
587        s += _report_table('Variable Declarations (%s)' % self.filename, declarations, 
588                          ('objtype', 'name', 'start_line', 'summary', ))
589        s += _report_table('Macro Declarations (%s)' % self.filename, macros, 
590                          ('objtype', 'name', 'start_line', 'end_line', 'summary', ))
591        s += _report_table('Function Macro Declarations (%s)' % self.filename, functions, 
592                          ('objtype', 'name', 'start_line', 'end_line', 'args', 'summary', ))
593        #s += _report_table('Findings from .mac File', self.findings, ('start_line', 'objtype', 'line', 'summary', ))
594
595        return '\n'.join(s)
596
597    def ReST(self, style = 'simple'):
598        """create the ReStructured Text from what has been found"""
599   
600        # allow for additional renderers, selectable by options
601        renderer_dict =  {'simple': self._simple_ReST_renderer,}
602        if style not in renderer_dict:
603            raise RuntimeWarning, "%s renderer not found, using `simple`" % style
604        return renderer_dict[style]()
605
606
607def _report_table(title, itemlist, col_keys = ('objtype', 'start_line', 'end_line', )):
608    """
609    return the itemlist as a reST table
610   
611    :param str title:  section heading above the table
612    :param {str,str} itemlist: database (keyed dictionary) to use for table
613    :param [str] col_keys: column labels (must be keys in the dictionary)
614    :returns [str]: the table (where each list item is a string of reST)
615    """
616    if len(itemlist) == 0:
617        return []
618    rows = []
619    last_line = None
620    for d in itemlist:
621        if d['start_line'] != last_line:
622            rowdata = [str(d.get(key,'')).strip() for key in col_keys]
623            rows.append( tuple(rowdata) )
624        last_line = d['start_line']
625    return make_rest_table(title, col_keys, rows, '=')
626
627
628def make_rest_table(title, labels, rows, titlechar = '='):
629    """
630    build a reST table
631   
632    :param str title: placed in a section heading above the table
633    :param [str] labels: columns labels
634    :param [[str]] rows: 2-D grid of data, len(labels) == len(data[i]) for all i
635    :param str titlechar: character to use when underlining title as reST section heading
636    :returns [str]: each list item is reST
637    """
638    # this is commented out since it causes a warning when building:
639    #  specmacrofileparser.py:docstring of sphinxcontrib.specmacrofileparser.make_rest_table:14: WARNING: Block quote ends without a blank line; unexpected unindent.
640    # -----
641    #    """
642    #    build a reST table
643    #       
644    #    :param str title: placed in a section heading above the table
645    #    :param [str] labels: columns labels
646    #    :param [[str]] rows: 2-D grid of data, len(labels) == len(data[i]) for all i
647    #    :param str titlechar: character to use when underlining title as reST section heading
648    #    :returns [str]: each list item is reST
649    #
650    #    Example::
651    #       
652    #        title = 'This is a reST table'
653    #        labels = ('name', 'phone', 'email')
654    #        rows = [
655    #                ['Snoopy',           '12345', 'dog@house'],
656    #                ['Red Baron',        '65432', 'fokker@triplane'],
657    #                ['Charlie Brown',    '12345', 'main@house'],
658    #        ]
659    #        print '\n'.join(make_rest_table(title, labels, rows, titlechar='~'))
660    #
661    #    This results in this reST::
662    #   
663    #        This is a reST table
664    #        ~~~~~~~~~~~~~~~~~~~~
665    #       
666    #        ============= ===== ===============
667    #        name          phone email         
668    #        ============= ===== ===============
669    #        Snoopy        12345 dog@house     
670    #        Red Baron     65432 fokker@triplane
671    #        Charlie Brown 12345 main@house     
672    #        ============= ===== ===============
673    #   
674    #    """
675    s = []
676    if len(rows) == 0:
677        return s
678    if len(labels) > 0:
679        columns = zip(labels, *rows)
680    else:
681        columns = zip(*rows)
682    widths = [max([len(item) for item in row]) for row in columns]
683    separator = " ".join( ['='*key for key in widths] )
684    fmt = " ".join( '%%-%ds' % key for key in widths )
685    s.append( '' )
686    s.append( title )
687    s.append( titlechar*len(title) )
688    s.append( '' )
689    s.append( separator )
690    if len(labels) > 0:
691        s.append( fmt % labels )
692        s.append( separator )
693    s.extend( [fmt % tuple(row) for row in rows] )
694    s.append( separator )
695    return s
696
697
698TEST_DIR = os.path.join('..', 'macros')
699
700
701if __name__ == '__main__':
702    filelist = [f for f in sorted(os.listdir(TEST_DIR)) if f.endswith('.mac')]
703    for item in filelist:
704        filename = os.path.join(TEST_DIR, item)
705        print filename
706        p = SpecMacrofileParser(filename)
707        print p.ReST()
708        #pprint (p.findings)
Note: See TracBrowser for help on using the repository browser.