diff options
author | Pearu Peterson <pearu.peterson@gmail.com> | 2006-05-24 07:39:50 +0000 |
---|---|---|
committer | Pearu Peterson <pearu.peterson@gmail.com> | 2006-05-24 07:39:50 +0000 |
commit | 0c7da8ae1e9464b9ad8b7458690513622b17e4ff (patch) | |
tree | d9aa9c010d701bb320c1ee56589f4f5ac6590f4e /numpy/f2py/lib/readfortran.py | |
parent | 5c82fafac476980e1d487ca4aa06621390aa6c26 (diff) | |
download | numpy-0c7da8ae1e9464b9ad8b7458690513622b17e4ff.tar.gz |
Impl. new and improved fortran file reader (remembers line numbers, comments, gracefully reports errors and warnings etc.).
Diffstat (limited to 'numpy/f2py/lib/readfortran.py')
-rw-r--r-- | numpy/f2py/lib/readfortran.py | 588 |
1 files changed, 588 insertions, 0 deletions
diff --git a/numpy/f2py/lib/readfortran.py b/numpy/f2py/lib/readfortran.py new file mode 100644 index 000000000..aa0210380 --- /dev/null +++ b/numpy/f2py/lib/readfortran.py @@ -0,0 +1,588 @@ +#!/usr/bin/env python +""" +Defines FortranReader classes for reading Fortran codes from +files and strings. FortranReader handles comments and line continuations +of both fix and free format Fortran codes. + +Copyright 2006 Pearu Peterson all rights reserved, +Pearu Peterson <pearu@cens.ioc.ee> +Permission to use, modify, and distribute this software is given under the +terms of the NumPy License. See http://scipy.org. + +NO WARRANTY IS EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. +Pearu Peterson +""" + +import re +import sys +import tempfile +from cStringIO import StringIO + +from numpy.distutils.misc_util import yellow_text, red_text, blue_text + +from sourceinfo import get_source_info +from splitline import LineSplitter, String + +_spacedigits=' 0123456789' +_cf2py_re = re.compile(r'(?P<indent>\s*)!f2py(?P<rest>.*)',re.I) +_is_fix_cont = lambda line: line and len(line)>5 and line[5]!=' ' and line[0]==' ' +_is_f90_cont = lambda line: line and '&' in line and line.rstrip()[-1]=='&' + + +class FortranReaderError: # TODO: may be derive it from Exception + def __init__(self, message): + self.message = message + print >> sys.stderr,message + +class Line: + """ Holds a Fortran source line. + """ + def __init__(self, line, linenospan, label, reader): + self.line = line + self.span = linenospan + self.label = label + self.reader = reader + def __repr__(self): + return self.__class__.__name__+'(%r,%s,%r)' \ + % (self.line, self.span, self.label) + def isempty(self): + return not (self.line.strip() or (self.label and self.label.strip())) + +class SyntaxErrorLine(Line, FortranReaderError): + def __init__(self, line, linenospan, label, reader, message): + Line.__init__(self, line, linenospan, label, reader) + FortranReaderError.__init__(self, message) + +class Comment: + """ Holds Fortran comment. + """ + def __init__(self, comment, linenospan, reader): + self.comment = comment + self.span = linenospan + self.reader = reader + def __repr__(self): + return self.__class__.__name__+'(%r,%s)' \ + % (self.comment, self.span) + def isempty(self): + return len(self.comment)<2 # comment includes comment character + +class MultiLine: + """ Holds (prefix, line list, suffix) representing multiline + syntax in .pyf files: + prefix+'''+lines+'''+suffix. + """ + def __init__(self, prefix, block, suffix, linenospan, reader): + self.prefix = prefix + self.block = block + self.suffix = suffix + self.span = linenospan + self.reader = reader + def __repr__(self): + return self.__class__.__name__+'(%r,%r,%r,%s)' \ + % (self.prefix,self.block,self.suffix, + self.span) + def isempty(self): + return not (self.prefix or self.block or self.suffix) + +class SyntaxErrorMultiLine(MultiLine, FortranReaderError): + def __init__(self, prefix, block, suffix, linenospan, reader, message): + MultiLine.__init__(self, prefix, block, suffix, linenospan, reader) + FortranReaderError.__init__(self, message) + + +class FortranReaderBase: + + def __init__(self, source, isfree, isstrict): + """ + source - file-like object with .next() method + used to retrive a line. + source may contain + - Fortran 77 code + - fixed format Fortran 90 code + - free format Fortran 90 code + - .pyf signatures - extended free format Fortran 90 syntax + """ + self.isfree90 = isfree and not isstrict + self.isfix90 = not isfree and not isstrict + self.isfix77 = not isfree and isstrict + self.ispyf = isfree and isstrict + self.isfree = isfree + self.isfix = not isfree + + self.linecount = 0 + self.source = source + self.isclosed = False + + self.filo_line = [] + self.fifo_item = [] + self.source_lines = [] + + def close_source(self): + pass + + # For handling raw source lines: + + def put_single_line(self, line): + self.filo_line.append(line) + self.linecount -= 1 + + def get_single_line(self): + try: + line = self.filo_line.pop() + self.linecount += 1 + return line + except IndexError: + pass + if self.isclosed: + return None + try: + line = self.source.next() + except StopIteration: + self.isclosed = True + self.close_source() + return None + self.linecount += 1 + # expand tabs, replace special symbols, get rid of nl characters + line = line.expandtabs().replace('\xa0',' ').rstrip('\n\r\f') + self.source_lines.append(line) + return line + + def get_next_line(self): + line = self.get_single_line() + if line is None: return + self.put_single_line(line) + return line + + # Iterator methods: + + def __iter__(self): + return self + + def next(self): + fifo_item_pop = self.fifo_item.pop + while 1: + try: + item = fifo_item_pop(0) + except IndexError: + item = self.get_source_item() + if item is None: + raise StopIteration + if not item.isempty(): + break + # else ignore empty lines and comments + if not isinstance(item, Comment): + return item + # collect subsequent comments to one comment instance + comments = [] + start = item.span[0] + while isinstance(item, Comment): + comments.append(item.comment) + end = item.span[1] + while 1: + try: + item = fifo_item_pop(0) + except IndexError: + item = self.get_source_item() + if item is None or not item.isempty(): + break + if item is None: + break # hold raising StopIteration for the next call. + if item is not None: + self.fifo_item.insert(0,item) + return self.comment_item('\n'.join(comments), start, end) + + # Interface to returned items: + + def line_item(self, line, startlineno, endlineno, label, errmessage=None): + if errmessage is None: + return Line(line, (startlineno, endlineno), label, self) + return SyntaxErrorLine(line, (startlineno, endlineno), + label, self, errmessage) + + def multiline_item(self, prefix, lines, suffix, + startlineno, endlineno, errmessage=None): + if errmessage is None: + return MultiLine(prefix, lines, suffix, (startlineno, endlineno), self) + return SyntaxErrorMultiLine(prefix, lines, suffix, + (startlineno, endlineno), self, errmessage) + + def comment_item(self, comment, startlineno, endlineno): + return Comment(comment, (startlineno, endlineno), self) + + # For handling messages: + + def format_message(self, kind, message, startlineno, endlineno, + startcolno=0, endcolno=-1): + r = ['%s while processing %s..' % (kind, self.source)] + for i in range(max(1,startlineno-3),startlineno): + r.append('%5d:%s' % (i,self.source_lines[i-1])) + for i in range(startlineno,min(endlineno+3,len(self.source_lines))+1): + linenostr = '%5d:' % (i) + if i==endlineno: + sourceline = self.source_lines[i-1] + l0 = linenostr+sourceline[:startcolno] + if endcolno==-1: + l1 = sourceline[startcolno:] + l2 = '' + else: + l1 = sourceline[startcolno:endcolno] + l2 = sourceline[endcolno:] + r.append('%s%s%s <== %s' % (l0,yellow_text(l1),l2,red_text(message))) + else: + r.append(linenostr+ self.source_lines[i-1]) + return '\n'.join(r) + + def format_error_message(self, message, startlineno, endlineno, + startcolno=0, endcolno=-1): + return self.format_message('ERROR',message, startlineno, + endlineno, startcolno, endcolno) + + def format_warning_message(self, message, startlineno, endlineno, + startcolno=0, endcolno=-1): + return self.format_message('WARNING',message, startlineno, + endlineno, startcolno, endcolno) + + # Auxiliary methods for processing raw source lines: + + def handle_cf2py_start(self, line): + """ + f2py directives can be used only in Fortran codes. + They are ignored when used inside .pyf files. + """ + if not line or self.ispyf: return line + if self.isfix: + if line[0] in '*cC!#': + if line[1:5].lower() == 'f2py': + line = 5*' ' + line[5:] + if self.isfix77: + return line + m = _cf2py_re.match(line) + if m: + newline = m.group('indent')+5*' '+m.group('rest') + assert len(newline)==len(line),`newlinel,line` + return newline + return line + + def handle_inline_comment(self, line, lineno, quotechar=None): + if quotechar is None and '!' not in line and \ + '"' not in line and "'" not in line: + return line, quotechar + i = line.find('!') + put_item = self.fifo_item.append + if quotechar is None and i!=-1: + # first try a quick method + newline = line[:i] + if '"' not in newline and '\'' not in newline: + put_item(self.comment_item(line[i:], lineno, lineno)) + return newline, quotechar + # handle cases where comment char may be a part of a character content + splitter = LineSplitter(line, quotechar) + items = [item for item in splitter] + newquotechar = splitter.quotechar + noncomment_items = [] + noncomment_items_append = noncomment_items.append + n = len(items) + for i in range(n): + item = items[i] + if isinstance(item, String) or '!' not in item: + noncomment_items_append(item) + continue + j = item.find('!') + noncomment_items_append(item[:j]) + items[i] = item[j:] + put_item(self.comment_item(''.join(items[i:]), lineno, lineno)) + break + return ''.join(noncomment_items), newquotechar + + def handle_multilines(self, line, startlineno, mlstr): + i = line.find(mlstr) + if i != -1: + prefix = line[:i] + # skip fake multiline starts + p,k = prefix,0 + while p.endswith('\\'): + p,k = p[:-1],k+1 + if k % 2: return + if i != -1 and '!' not in prefix: + # Note character constans like 'abc"""123', + # so multiline prefix should better not contain `'' or `"' not `!'. + for quote in '"\'': + if prefix.count(quote) % 2: + message = self.format_warning_message(\ + 'multiline prefix contains odd number of %r characters' \ + % (quote), startlineno, startlineno, + 0, len(prefix)) + print >> sys.stderr, message + + suffix = None + multilines = [] + line = line[i+3:] + while line is not None: + j = line.find(mlstr) + if j != -1 and '!' not in line[:j]: + multilines.append(line[:j]) + suffix = line[j+3:] + break + multilines.append(line) + line = self.get_single_line() + if line is None: + message = self.format_error_message(\ + 'multiline block never ends', startlineno, + startlineno, i) + return self.multiline_item(\ + prefix,multilines,suffix,\ + startlineno, self.linecount, message) + suffix,qc = self.handle_inline_comment(suffix, self.linecount) + # no line continuation allowed in mulitline suffix + if qc is not None: + message = self.format_message(\ + 'ASSERTION FAILURE(pyf)', + 'following character continuation: %r, expected None.' % (qc), + startlineno, self.linecount) + print >> sys.stderr, message + # XXX: should we do line.replace('\\'+mlstr[0],mlstr[0]) + # for line in multilines? + return self.multiline_item(prefix,multilines,suffix, + startlineno, self.linecount) + + # The main method of interpreting raw source lines within + # the following contexts: f77, fixed f90, free f90, pyf. + + def get_source_item(self): + """ + a source item is .. + - a fortran line + - a list of continued fortran lines + - a multiline - lines inside triple-qoutes, only when in ispyf mode + """ + get_single_line = self.get_single_line + line = get_single_line() + if line is None: return + startlineno = self.linecount + line = self.handle_cf2py_start(line) + + if self.ispyf: + # handle multilines + for mlstr in ['"""',"'''"]: + r = self.handle_multilines(line, startlineno, mlstr) + if r: return r + + if self.isfix: + label = line[:5] + if not line.strip(): + # empty line + return self.line_item(line[6:],startlineno,self.linecount,label) + if line[0] in '*cC!': + return self.comment_item(line, startlineno, startlineno) + for i in range(5): + if line[i] not in _spacedigits: + message = 'non-space/digit char %r found in column %i'\ + ' of fixed Fortran code' % (line[i],i+1) + if self.isfix90: + message = message + ', switching to free format mode' + message = self.format_warning_message(\ + message,startlineno, self.linecount) + print >> sys.stderr, message + self.isfree = True + self.isfix90 = False + self.isfree90 = True + else: + return self.line_item(line[6:], startlineno, self.linecount, + label, self.format_error_message(\ + message, startlineno, self.linecount)) + if self.isfix77: + lines = [line[6:72]] + while _is_fix_cont(self.get_next_line()): + # handle fix format line continuations for F77 code + line = get_single_line() + lines.append(line[6:72]) + return self.line_item(''.join(lines),startlineno,self.linecount,label) + + handle_inline_comment = self.handle_inline_comment + + if self.isfix90 and _is_fix_cont(self.get_next_line()): + # handle inline comment + newline,qc = handle_inline_comment(line[6:], startlineno) + lines = [newline] + while _is_fix_cont(self.get_next_line()): + # handle fix format line continuations for F90 code. + # mixing fix format and f90 line continuations is not allowed + # nor detected, just eject warnings. + line = get_single_line() + newline,qc = self.handle_inline_comment(line[6:], self.linecount, qc) + lines.append(newline) + # no character continuation should follows now + if qc is not None: + message = self.format_message(\ + 'ASSERTION FAILURE(fix90)', + 'following character continuation: %r, expected None.' % (qc), + startlineno, self.linecount) + print >> sys.stderr, message + for i in range(len(lines)): + l = lines[i] + if l.rstrip().endswith('&'): + message = self.format_warning_message(\ + 'f90 line continuation character `&\' detected'\ + ' in fix format code', + startlineno + i, startlineno + i, l.rfind('&')+5) + print >> sys.stderr, message + return self.line_item(''.join(lines),startlineno,self.linecount,label) + + start_index = 0 + if self.isfix90: + start_index = 6 + + lines = [] + lines_append = lines.append + put_item = self.fifo_item.append + qc = None + while line is not None: + if start_index: # fix format code + line,qc = handle_inline_comment(line[start_index:], + self.linecount,qc) + else: + line_lstrip = line.lstrip() + if lines and line_lstrip.startswith('!'): + # check for comment line within line continuation + put_item(self.comment_item(line_lstrip, + self.linecount, self.linecount)) + line = get_single_line() + continue + line,qc = handle_inline_comment(line, self.linecount, qc) + + i = line.rfind('&') + if i!=-1: + line_i1_rstrip = line[i+1:].rstrip() + if not lines: + # first line + if i == -1 or line_i1_rstrip: + lines_append(line) + break + lines_append(line[:i]) + line = get_single_line() + continue + if i == -1 or line_i1_rstrip: + # no line continuation follows + i = len(line) + k = -1 + if i != -1: + # handle the beggining of continued line + k = line[:i].find('&') + if k != 1 and line[:k].lstrip(): + k = -1 + lines_append(line[k+1:i]) + if i==len(line): + break + line = get_single_line() + + if qc is not None: + message = self.format_message('ASSERTION FAILURE(free)', + 'following character continuation: %r, expected None.' % (qc), + startlineno, self.linecount) + print >> sys.stderr, message + return self.line_item(''.join(lines),startlineno,self.linecount,None) + + ## FortranReaderBase + +class FortranFileReader(FortranReaderBase): + + def __init__(self, filename): + isfree, isstrict = get_source_info(filename) + self.file = open(filename,'r') + FortranReaderBase.__init__(self, self.file, isfree, isstrict) + + def close_source(self): + self.file.close() + +class FortranStringReader(FortranReaderBase): + + def __init__(self, string, isfree, isstrict): + source = StringIO(string) + FortranReaderBase.__init__(self, source, isfree, isstrict) + +def test_f77(): + string_f77 = """ +c12346 comment + subroutine foo + call foo + 'bar +a 'g + abc=2 +cf2py call me ! hey + call you ! hi + end + '""" + reader = FortranStringReader(string_f77,False,True) + for item in reader: + print item + + filename = tempfile.mktemp()+'.f' + f = open(filename,'w') + f.write(string_f77) + f.close() + + reader = FortranFileReader(filename) + for item in reader: + print item + +def test_pyf(): + string_pyf = """\ +python module foo + interface + beginml '''1st line + 2nd line + end line'''endml='tere!fake comment'!should be a comment + a = 2 + 'charc\"onstant' ''' single line mline '''a='hi!fake comment'!should be a comment + a=\\\\\\\\\\'''not a multiline''' + !blah='''never ending multiline + b=3! hey, fake line continuation:& + c=4& !line cont + &45 + end interface +end python module foo +! end of file +""" + reader = FortranStringReader(string_pyf,True, True) + for item in reader: + print item + +def test_fix90(): + string_fix90 = """\ + subroutine foo +cComment + 1234 a = 3 !inline comment + b = 3 +! + !4!line cont. with comment symbol + &5 + + end subroutine foo +""" + reader = FortranStringReader(string_fix90,False, False) + for item in reader: + print item + +def simple_main(): + for filename in sys.argv[1:]: + print 'Processing',filename + reader = FortranFileReader(filename) + for item in reader: + #print item + pass + +def profile_main(): + import hotshot, hotshot.stats + prof = hotshot.Profile("readfortran.prof") + prof.runcall(simple_main) + prof.close() + stats = hotshot.stats.load("readfortran.prof") + stats.strip_dirs() + stats.sort_stats('time', 'calls') + stats.print_stats(30) + +if __name__ == "__main__": + #test_pyf() + #test_fix90() + #profile_main() + simple_main() |