]>
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
)