]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/distutils/text_file.py
   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.""" 
  10 import sys
, os
, string
 
  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. 
  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. 
  28        Constructor is called as: 
  30            TextFile (filename=None, file=None, **options) 
  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. 
  39        The options are all boolean, and affect the value returned by 
  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) 
  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 
  71     default_options 
= { 'strip_comments': 1, 
  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()'.""" 
  85         if filename 
is None and file is None: 
  87                   "you must supply either or both of 'filename' and 'file'" 
  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
]) 
  96                 setattr (self
, opt
, self
.default_options
[opt
]) 
  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
 
 106             self
.filename 
= filename
 
 108             self
.current_line 
= 0       # assuming that file is at BOF! 
 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 
 116     def open (self
, filename
): 
 117         """Open a new file named 'filename'.  This overrides both the 
 118            'filename' and 'file' arguments to the constructor.""" 
 120         self
.filename 
= filename
 
 121         self
.file = open (self
.filename
, 'r') 
 122         self
.current_line 
= 0 
 126         """Close the current file and forget everything we know about it 
 127            (filename, current line number).""" 
 132         self
.current_line 
= None 
 135     def gen_error (self
, msg
, line
=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
)) 
 143             outmsg
.append("line %d: " % line
) 
 144         outmsg
.append(str(msg
)) 
 145         return string
.join(outmsg
, "") 
 148     def error (self
, msg
, line
=None): 
 149         raise ValueError, "error: " + self
.gen_error(msg
, line
) 
 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 
 159         sys
.stderr
.write("warning: " + self
.gen_error(msg
, line
) + "\n") 
 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 
 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 
 178             line 
= self
.linebuf
[-1] 
 185             # read the line, make it None if EOF 
 186             line 
= self
.file.readline() 
 187             if line 
== '': line 
= None 
 189             if self
.strip_comments 
and line
: 
 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. 
 199                 pos 
= string
.find (line
, "#") 
 200                 if pos 
== -1:           # no "#" -- no comments 
 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
 
 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 
 219                     #   # comment that should be ignored 
 221                     # result in "hello there". 
 222                     if string
.strip(line
) == "": 
 225                 else:                   # it's an escaped "#" 
 226                     line 
= string
.replace (line
, "\\#", "#") 
 229             # did previous line end with a backslash? then accumulate 
 230             if self
.join_lines 
and buildup_line
: 
 233                     self
.warn ("continuation line immediately precedes " 
 237                 if self
.collapse_join
: 
 238                     line 
= string
.lstrip (line
) 
 239                 line 
= buildup_line 
+ line
 
 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 
 245                     self
.current_line 
= [self
.current_line
, 
 247             # just an ordinary line, read it as usual 
 249                 if line 
is None:        # eof 
 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 
 256                     self
.current_line 
= self
.current_line 
+ 1 
 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
) 
 264                 line 
= string
.lstrip (line
) 
 266                 line 
= string
.rstrip (line
) 
 268             # blank line (whether we rstrip'ed or not)? skip to next line 
 270             if (line 
== '' or line 
== '\n') and self
.skip_blanks
: 
 275                     buildup_line 
= line
[:-1] 
 278                 if line
[-2:] == '\\\n': 
 279                     buildup_line 
= line
[0:-2] + '\n' 
 282             # well, I guess there's some actual content there: return it 
 288     def readlines (self
): 
 289         """Read and return the list of all logical lines remaining in the 
 294             line 
= self
.readline() 
 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.""" 
 305         self
.linebuf
.append (line
) 
 308 if __name__ 
== "__main__": 
 309     test_data 
= """# test file 
 312 # intervening comment 
 313   continues on next line 
 315     # result 1: no fancy options 
 316     result1 
= map (lambda x
: x 
+ "\n", string
.split (test_data
, "\n")[0:-1]) 
 318     # result 2: just strip comments 
 321                "  continues on next line\n"] 
 323     # result 3: just strip blank lines 
 324     result3 
= ["# test file\n", 
 326                "# intervening comment\n", 
 327                "  continues on next line\n"] 
 329     # result 4: default, strip comments, blank lines, and trailing whitespace 
 330     result4 
= ["line 3 \\", 
 331                "  continues on next line"] 
 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"] 
 337     # result 6: strip comments and blanks, plus join lines (and 
 338     # "collapse" joined lines 
 339     result6 
= ["line 3 continues on next line"] 
 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
) 
 347             print "not ok %d (%s):" % (count
, description
) 
 349             print expected_result
 
 354     filename 
= "test.txt" 
 355     out_file 
= open (filename
, "w") 
 356     out_file
.write (test_data
) 
 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
) 
 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
) 
 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
) 
 371     in_file 
= TextFile (filename
) 
 372     test_input (4, "default processing", in_file
, result4
) 
 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
) 
 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
)