home *** CD-ROM | disk | FTP | other *** search
/ PC World 2005 June / PCWorld_2005-06_cd.bin / software / vyzkuste / firewally / firewally.exe / framework-2.3.exe / text_file.py < prev    next >
Text File  |  2003-12-30  |  15KB  |  383 lines

  1. """text_file
  2.  
  3. provides the TextFile class, which gives an interface to text files
  4. that (optionally) takes care of stripping comments, ignoring blank
  5. lines, and joining lines with backslashes."""
  6.  
  7. __revision__ = "$Id: text_file.py,v 1.15 2002/11/14 02:25:41 akuchling Exp $"
  8.  
  9. from types import *
  10. import sys, os, string
  11.  
  12.  
  13. class TextFile:
  14.  
  15.     """Provides a file-like object that takes care of all the things you
  16.        commonly want to do when processing a text file that has some
  17.        line-by-line syntax: strip comments (as long as "#" is your
  18.        comment character), skip blank lines, join adjacent lines by
  19.        escaping the newline (ie. backslash at end of line), strip
  20.        leading and/or trailing whitespace.  All of these are optional
  21.        and independently controllable.
  22.  
  23.        Provides a 'warn()' method so you can generate warning messages that
  24.        report physical line number, even if the logical line in question
  25.        spans multiple physical lines.  Also provides 'unreadline()' for
  26.        implementing line-at-a-time lookahead.
  27.  
  28.        Constructor is called as:
  29.  
  30.            TextFile (filename=None, file=None, **options)
  31.  
  32.        It bombs (RuntimeError) if both 'filename' and 'file' are None;
  33.        'filename' should be a string, and 'file' a file object (or
  34.        something that provides 'readline()' and 'close()' methods).  It is
  35.        recommended that you supply at least 'filename', so that TextFile
  36.        can include it in warning messages.  If 'file' is not supplied,
  37.        TextFile creates its own using the 'open()' builtin.
  38.  
  39.        The options are all boolean, and affect the value returned by
  40.        'readline()':
  41.          strip_comments [default: true]
  42.            strip from "#" to end-of-line, as well as any whitespace
  43.            leading up to the "#" -- unless it is escaped by a backslash
  44.          lstrip_ws [default: false]
  45.            strip leading whitespace from each line before returning it
  46.          rstrip_ws [default: true]
  47.            strip trailing whitespace (including line terminator!) from
  48.            each line before returning it
  49.          skip_blanks [default: true}
  50.            skip lines that are empty *after* stripping comments and
  51.            whitespace.  (If both lstrip_ws and rstrip_ws are false,
  52.            then some lines may consist of solely whitespace: these will
  53.            *not* be skipped, even if 'skip_blanks' is true.)
  54.          join_lines [default: false]
  55.            if a backslash is the last non-newline character on a line
  56.            after stripping comments and whitespace, join the following line
  57.            to it to form one "logical line"; if N consecutive lines end
  58.            with a backslash, then N+1 physical lines will be joined to
  59.            form one logical line.
  60.          collapse_join [default: false]
  61.            strip leading whitespace from lines that are joined to their
  62.            predecessor; only matters if (join_lines and not lstrip_ws)
  63.  
  64.        Note that since 'rstrip_ws' can strip the trailing newline, the
  65.        semantics of 'readline()' must differ from those of the builtin file
  66.        object's 'readline()' method!  In particular, 'readline()' returns
  67.        None for end-of-file: an empty string might just be a blank line (or
  68.        an all-whitespace line), if 'rstrip_ws' is true but 'skip_blanks' is
  69.        not."""
  70.  
  71.     default_options = { 'strip_comments': 1,
  72.                         'skip_blanks':    1,
  73.                         'lstrip_ws':      0,
  74.                         'rstrip_ws':      1,
  75.                         'join_lines':     0,
  76.                         'collapse_join':  0,
  77.                       }
  78.  
  79.     def __init__ (self, filename=None, file=None, **options):
  80.         """Construct a new TextFile object.  At least one of 'filename'
  81.            (a string) and 'file' (a file-like object) must be supplied.
  82.            They keyword argument options are described above and affect
  83.            the values returned by 'readline()'."""
  84.  
  85.         if filename is None and file is None:
  86.             raise RuntimeError, \
  87.                   "you must supply either or both of 'filename' and 'file'"
  88.  
  89.         # set values for all options -- either from client option hash
  90.         # or fallback to default_options
  91.         for opt in self.default_options.keys():
  92.             if options.has_key (opt):
  93.                 setattr (self, opt, options[opt])
  94.  
  95.             else:
  96.                 setattr (self, opt, self.default_options[opt])
  97.  
  98.         # sanity check client option hash
  99.         for opt in options.keys():
  100.             if not self.default_options.has_key (opt):
  101.                 raise KeyError, "invalid TextFile option '%s'" % opt
  102.  
  103.         if file is None:
  104.             self.open (filename)
  105.         else:
  106.             self.filename = filename
  107.             self.file = file
  108.             self.current_line = 0       # assuming that file is at BOF!
  109.  
  110.         # 'linebuf' is a stack of lines that will be emptied before we
  111.         # actually read from the file; it's only populated by an
  112.         # 'unreadline()' operation
  113.         self.linebuf = []
  114.  
  115.  
  116.     def open (self, filename):
  117.         """Open a new file named 'filename'.  This overrides both the
  118.            'filename' and 'file' arguments to the constructor."""
  119.  
  120.         self.filename = filename
  121.         self.file = open (self.filename, 'r')
  122.         self.current_line = 0
  123.  
  124.  
  125.     def close (self):
  126.         """Close the current file and forget everything we know about it
  127.            (filename, current line number)."""
  128.  
  129.         self.file.close ()
  130.         self.file = None
  131.         self.filename = None
  132.         self.current_line = None
  133.  
  134.  
  135.     def gen_error (self, msg, line=None):
  136.         outmsg = []
  137.         if line is None:
  138.             line = self.current_line
  139.         outmsg.append(self.filename + ", ")
  140.         if type (line) in (ListType, TupleType):
  141.             outmsg.append("lines %d-%d: " % tuple (line))
  142.         else:
  143.             outmsg.append("line %d: " % line)
  144.         outmsg.append(str(msg))
  145.         return string.join(outmsg, "")
  146.  
  147.  
  148.     def error (self, msg, line=None):
  149.         raise ValueError, "error: " + self.gen_error(msg, line)
  150.  
  151.     def warn (self, msg, line=None):
  152.         """Print (to stderr) a warning message tied to the current logical
  153.            line in the current file.  If the current logical line in the
  154.            file spans multiple physical lines, the warning refers to the
  155.            whole range, eg. "lines 3-5".  If 'line' supplied, it overrides
  156.            the current line number; it may be a list or tuple to indicate a
  157.            range of physical lines, or an integer for a single physical
  158.            line."""
  159.         sys.stderr.write("warning: " + self.gen_error(msg, line) + "\n")
  160.  
  161.  
  162.     def readline (self):
  163.         """Read and return a single logical line from the current file (or
  164.            from an internal buffer if lines have previously been "unread"
  165.            with 'unreadline()').  If the 'join_lines' option is true, this
  166.            may involve reading multiple physical lines concatenated into a
  167.            single string.  Updates the current line number, so calling
  168.            'warn()' after 'readline()' emits a warning about the physical
  169.            line(s) just read.  Returns None on end-of-file, since the empty
  170.            string can occur if 'rstrip_ws' is true but 'strip_blanks' is
  171.            not."""
  172.  
  173.         # If any "unread" lines waiting in 'linebuf', return the top
  174.         # one.  (We don't actually buffer read-ahead data -- lines only
  175.         # get put in 'linebuf' if the client explicitly does an
  176.         # 'unreadline()'.
  177.         if self.linebuf:
  178.             line = self.linebuf[-1]
  179.             del self.linebuf[-1]
  180.             return line
  181.  
  182.         buildup_line = ''
  183.  
  184.         while 1:
  185.             # read the line, make it None if EOF
  186.             line = self.file.readline()
  187.             if line == '': line = None
  188.  
  189.             if self.strip_comments and line:
  190.  
  191.                 # Look for the first "#" in the line.  If none, never
  192.                 # mind.  If we find one and it's the first character, or
  193.                 # is not preceded by "\", then it starts a comment --
  194.                 # strip the comment, strip whitespace before it, and
  195.                 # carry on.  Otherwise, it's just an escaped "#", so
  196.                 # unescape it (and any other escaped "#"'s that might be
  197.                 # lurking in there) and otherwise leave the line alone.
  198.  
  199.                 pos = string.find (line, "#")
  200.                 if pos == -1:           # no "#" -- no comments
  201.                     pass
  202.  
  203.                 # It's definitely a comment -- either "#" is the first
  204.                 # character, or it's elsewhere and unescaped.
  205.                 elif pos == 0 or line[pos-1] != "\\":
  206.                     # Have to preserve the trailing newline, because it's
  207.                     # the job of a later step (rstrip_ws) to remove it --
  208.                     # and if rstrip_ws is false, we'd better preserve it!
  209.                     # (NB. this means that if the final line is all comment
  210.                     # and has no trailing newline, we will think that it's
  211.                     # EOF; I think that's OK.)
  212.                     eol = (line[-1] == '\n') and '\n' or ''
  213.                     line = line[0:pos] + eol
  214.  
  215.                     # If all that's left is whitespace, then skip line
  216.                     # *now*, before we try to join it to 'buildup_line' --
  217.                     # that way constructs like
  218.                     #   hello \\
  219.                     #   # comment that should be ignored
  220.                     #   there
  221.                     # result in "hello there".
  222.                     if string.strip(line) == "":
  223.                         continue
  224.  
  225.                 else:                   # it's an escaped "#"
  226.                     line = string.replace (line, "\\#", "#")
  227.  
  228.  
  229.             # did previous line end with a backslash? then accumulate
  230.             if self.join_lines and buildup_line:
  231.                 # oops: end of file
  232.                 if line is None:
  233.                     self.warn ("continuation line immediately precedes "
  234.                                "end-of-file")
  235.                     return buildup_line
  236.  
  237.                 if self.collapse_join:
  238.                     line = string.lstrip (line)
  239.                 line = buildup_line + line
  240.  
  241.                 # careful: pay attention to line number when incrementing it
  242.                 if type (self.current_line) is ListType:
  243.                     self.current_line[1] = self.current_line[1] + 1
  244.                 else:
  245.                     self.current_line = [self.current_line,
  246.                                          self.current_line+1]
  247.             # just an ordinary line, read it as usual
  248.             else:
  249.                 if line is None:        # eof
  250.                     return None
  251.  
  252.                 # still have to be careful about incrementing the line number!
  253.                 if type (self.current_line) is ListType:
  254.                     self.current_line = self.current_line[1] + 1
  255.                 else:
  256.                     self.current_line = self.current_line + 1
  257.  
  258.  
  259.             # strip whitespace however the client wants (leading and
  260.             # trailing, or one or the other, or neither)
  261.             if self.lstrip_ws and self.rstrip_ws:
  262.                 line = string.strip (line)
  263.             elif self.lstrip_ws:
  264.                 line = string.lstrip (line)
  265.             elif self.rstrip_ws:
  266.                 line = string.rstrip (line)
  267.  
  268.             # blank line (whether we rstrip'ed or not)? skip to next line
  269.             # if appropriate
  270.             if (line == '' or line == '\n') and self.skip_blanks:
  271.                 continue
  272.  
  273.             if self.join_lines:
  274.                 if line[-1] == '\\':
  275.                     buildup_line = line[:-1]
  276.                     continue
  277.  
  278.                 if line[-2:] == '\\\n':
  279.                     buildup_line = line[0:-2] + '\n'
  280.                     continue
  281.  
  282.             # well, I guess there's some actual content there: return it
  283.             return line
  284.  
  285.     # readline ()
  286.  
  287.  
  288.     def readlines (self):
  289.         """Read and return the list of all logical lines remaining in the
  290.            current file."""
  291.  
  292.         lines = []
  293.         while 1:
  294.             line = self.readline()
  295.             if line is None:
  296.                 return lines
  297.             lines.append (line)
  298.  
  299.  
  300.     def unreadline (self, line):
  301.         """Push 'line' (a string) onto an internal buffer that will be
  302.            checked by future 'readline()' calls.  Handy for implementing
  303.            a parser with line-at-a-time lookahead."""
  304.  
  305.         self.linebuf.append (line)
  306.  
  307.  
  308. if __name__ == "__main__":
  309.     test_data = """# test file
  310.  
  311. line 3 \\
  312. # intervening comment
  313.   continues on next line
  314. """
  315.     # result 1: no fancy options
  316.     result1 = map (lambda x: x + "\n", string.split (test_data, "\n")[0:-1])
  317.  
  318.     # result 2: just strip comments
  319.     result2 = ["\n",
  320.                "line 3 \\\n",
  321.                "  continues on next line\n"]
  322.  
  323.     # result 3: just strip blank lines
  324.     result3 = ["# test file\n",
  325.                "line 3 \\\n",
  326.                "# intervening comment\n",
  327.                "  continues on next line\n"]
  328.  
  329.     # result 4: default, strip comments, blank lines, and trailing whitespace
  330.     result4 = ["line 3 \\",
  331.                "  continues on next line"]
  332.  
  333.     # result 5: strip comments and blanks, plus join lines (but don't
  334.     # "collapse" joined lines
  335.     result5 = ["line 3   continues on next line"]
  336.  
  337.     # result 6: strip comments and blanks, plus join lines (and
  338.     # "collapse" joined lines
  339.     result6 = ["line 3 continues on next line"]
  340.  
  341.     def test_input (count, description, file, expected_result):
  342.         result = file.readlines ()
  343.         # result = string.join (result, '')
  344.         if result == expected_result:
  345.             print "ok %d (%s)" % (count, description)
  346.         else:
  347.             print "not ok %d (%s):" % (count, description)
  348.             print "** expected:"
  349.             print expected_result
  350.             print "** received:"
  351.             print result
  352.  
  353.  
  354.     filename = "test.txt"
  355.     out_file = open (filename, "w")
  356.     out_file.write (test_data)
  357.     out_file.close ()
  358.  
  359.     in_file = TextFile (filename, strip_comments=0, skip_blanks=0,
  360.                         lstrip_ws=0, rstrip_ws=0)
  361.     test_input (1, "no processing", in_file, result1)
  362.  
  363.     in_file = TextFile (filename, strip_comments=1, skip_blanks=0,
  364.                         lstrip_ws=0, rstrip_ws=0)
  365.     test_input (2, "strip comments", in_file, result2)
  366.  
  367.     in_file = TextFile (filename, strip_comments=0, skip_blanks=1,
  368.                         lstrip_ws=0, rstrip_ws=0)
  369.     test_input (3, "strip blanks", in_file, result3)
  370.  
  371.     in_file = TextFile (filename)
  372.     test_input (4, "default processing", in_file, result4)
  373.  
  374.     in_file = TextFile (filename, strip_comments=1, skip_blanks=1,
  375.                         join_lines=1, rstrip_ws=1)
  376.     test_input (5, "join lines without collapsing", in_file, result5)
  377.  
  378.     in_file = TextFile (filename, strip_comments=1, skip_blanks=1,
  379.                         join_lines=1, rstrip_ws=1, collapse_join=1)
  380.     test_input (6, "join lines with collapsing", in_file, result6)
  381.  
  382.     os.remove (filename)
  383.