Writing and Reading Using QText Stream

The code for writing in text format using QTextStream is very similar to the code we used for writing using QDataStream.

def saveQTextStream(self): error = None fh = None try:

if not fh.open(QIODevice.WriteOnly):

raise IOError, unicode(fh.errorString()) stream = QTextStream(fh) stream.setCodec(CODEC)

for key, movie in self._movies:

stream << "{{MOVIE}} " << movie.title << "\n" \

<< movie.year << " " << movie.minutes << " " \ << movie.acquired.toString(Qt.ISODate) \ << "\n{NOTES}" if not movie.notes.isEmpty():

stream << "\n" << movie.notes stream << "\n{{ENDMOVIE}}\n"

There are two crucial points to note. First we must specify the encoding we want to use. We are using UTF-8 in all cases; (CODEC holds the text UTF-8). If we do not do this, PyQt will use the local 8-bit encoding, which could be any of ASCII, Latin-1, or UTF-8 in the United States, Latin-1 or UTF-8 in Western Europe, and EUC-JP, JIS, Shift-JIS, or UTF-8 in Japan. By specifying the encoding, we ensure that we always write and read using the encoding we have specified so that characters are not misinterpreted. Unfortunately, we cannot guarantee that users will edit our text file using the correct encoding. If the files are likely to be edited, we could write the encoding on the first line in ASCII—for example, as encoding="UTF-8" in a similar way to XML—to at least provide a hint to the editor. This problem should diminish in the coming years since UTF-8 is becoming the de facto global standard for encoding text files.

The second point should be obvious: All data must be written as text. QTextStream overloads operator << to handle Booleans, numbers, and QStrings automatically, but other data types must be converted to their string representations. For dates we have chosen to use ISO (YYYY-MM-DD) format. We have also chosen to avoid having a blank line after the {NOTES} marker if the notes are empty.

We have omitted the code for the except and finally blocks since it is the same as we have seen a few times before—for example, in the saveQDataStream() method.

Although writing in text format is straightforward, reading it back is not so easy. For one thing, we will have to read each integer (year, minutes, and components of the acquired date) as text and convert it to the integer the text represents. But the main issue is that we must correctly parse the file to pick out each movie's record attributes.

Handling integers is not too difficult since QString provides a toInt() method; but the method returns a success/failure flag rather than raising an exception, and checking for this every time we handle a number will mean that we need three lines of code per number instead of one. For this reason, we have created a more Pythonic wrapper function for reading integers.

def intFromQStr(qstr): i, ok = qstr.toInt() if not ok:

raise ValueError, unicode(qstr) return i

This function simply calls QString.toInt() and raises an exception on failure, or returns the integer on success.

Figure 8.5 The My Movies text format's finite state automaton for each movie

Figure 8.5 The My Movies text format's finite state automaton for each movie

To parse our movies text file we will use a finite state automaton to gather each movie's data. The automaton for parsing a single movie is illustrated in Figure 8.5. This just means that before we read each line, we have an expectation of what the line will contain. If the expectation is not met, we have an error; otherwise, we read the expected data and set the expectation for what the next line should contain.

def loadQTextStream(self): error = None fh = None try:

if not fh.open(QIODevice.ReadOnly):

raise IOError, unicode(fh.errorString()) stream = QTextStream(fh) stream.setCodec(CODEC) self.clear(False) lino = 0

The method begins familiarly enough. Once we have opened the file, created the QTextStream, and set the codec, we clear the existing movie data, and are ready to read in the data from disk.

For each movie we first expect a "title" line containing {{MOVIE}} followed by a space and the movie's title, then a "numbers" line that will have the year, minutes, and acquired date, then a "notes" line that just contains {NOTES}, then zero or more lines of notes text, and finally an "end" line containing just {{ENDMOVIE}}. We begin by expecting a "title" line.

To help the user find format errors we keep track of the current line number in the lino variable, which we will use in error messages.

The body of the while loop that we use to read through the file is quite long, so we will look at it in parts.

while not stream.atEnd():

title = year = minutes = acquired = notes = None line = stream.readLine() lino += 1

raise ValueError, "no movie record found" else:

title = line.mid(len("{{MOVIE}}")).trimmed()

We begin by initializing the variables that will hold one movie's attributes to None so that it is easy to tell whether we have read them all.

We iterate over each line in the file. Unlike the Python standard library's file.readline() method, PyQt's QTextStream.readLine() strips off the line's trailing newline. Each time we read a line we increment lino.

The first line we expect for any movie is one beginning with the {{MOVIE}} marker. If the line is wrong we raise an exception with an error message; the exception handler will add the line number in the message passed up to the user. If we have a correct line, we extract the movie's title by reading the text that follows the {{MOVIE}} marker at the beginning of the line, stripping off any leading and trailing whitespace.

The QString.mid(n) method is the equivalent of unicode[n:], and QString. trimmed() is the same as unicode.strip().

Now we are ready to read the "numbers" line.

if stream.atEnd():

raise ValueError, "premature end of file" line = stream.readLine() lino += 1

parts = line.split(" ") if parts.count() != 3:

raise ValueError, "invalid numeric data"

year = intFromQStr(parts[0]) minutes = intFromQStr(parts[1]) ymd = parts[2].split("-") if ymd.count() != 3:

raise ValueError, "invalid acquired date" acquired = QDate(intFromQStr(ymd[0]), intFromQStr(ymd[1]), intFromQStr(ymd[2]))

We begin by checking that we haven't prematurely reached the end of the file, and if we have, we raise an exception. Otherwise, we read in the "numbers" line. This line should have an integer (the year), a space, an integer (the minutes), a space, and the acquired date in YYYY-MM-DD format. We initially split the line on the space character and this should give us three strings, year, minutes, and acquired date. We use our intFromQStr() function to convert the text to the integer it represents; if any conversion fails an exception is raised and handled in this method's exception handler. We convert the year and minutes directly, but for the acquired date we must split the string again, this time on the hyphen character, and then construct a QDate using the integer values extracted from each part.

Now we are ready to read the {NOTES} marker, optionally followed by lines of notes, and finally the {{ENDMOVIE}} marker.

if stream.atEnd():

raise ValueError, "premature end of file" line = stream.readLine() lino += 1

raise ValueError, "notes expected" notes = QString() while not stream.atEnd(): line = stream.readLine() lino += 1

if title is None or year is None or \

minutes is None or acquired is None or \ notes is None: raise ValueError, "incomplete record" self.add(Movie(title, year, minutes, acquired, notes.trimmed()))

break else:

else:

raise ValueError, "missing endmovie marker"

Table 8.2 Selected QTextStream Methods

Syntax Description s.atEnd() Returns True if theendof QTextStream s has been reached s.setCodec(c) Sets QTextStream s's text codec to the one specified in c—this can be a string (e.g., "UTF-8"), or a QTextCodec object s << x Writes object x to QTextStream s; x can be of type bool, float, int, long, QString, str, unicode, and a few others s.readLine() Reads one line, returning it as a QString stripped of any end-of-line characters s.readAll() Reads the entire file, returning it as a QString while loop's else clause

We expect to get a single line containing the {NOTES} marker. At this point, we set the notes variable to be an empty QString. Even if no notes text is added, the fact that we have a QString rather than a None is enough to tell us that we read the notes, even if they were empty.

Now there are two possibilities. Either we have the {{ENDMOVIE}} marker, or we are reading a line of notes. In the latter case we simply append the line to the notes we have accumulated so far, adding back the newline that PyQt's readLine() method stripped off. Then we loop, and have either the {{ENDMOVIE}} marker or another line of notes.

If we get the marker, we check that none of our variables is None to ensure that we have read all the data for a movie record, and then we create and add a new movie with the data we have gathered. Now we break out of the inner while loop ready to read another movie, or to finish if the one just read was the last one in the file.

If we never get the {{ENDMOVIE}} marker, at some point the end of the file will be reached and the inner while loop will terminate. If this occurs, the while loop's else suite will execute and raise a suitable exception. A while or for loop's else suite is executed only if the loop completes, not if it is terminated by a break statement.

except (IOError, OSError, ValueError), e:

error = "Failed to load: %s on line %d" % (e, lino) finally:

if fh is not None:

fh.close() if error is not None:

return False, error self._dirty = False return True, "Loaded %d movie records from %s" % (

len(self._movies),

QFileInfo(self._fname).fileName())

The error handling is almost identical to what we have seen before, only this time we include the line number where the error occurred.

Was this article helpful?

0 0
Tube Jacker

Tube Jacker

Download Tube Jacker And Discover Everything You Need To Know About Jacking Unlimited Traffic From The Video Giant. The drop dead easy way to create winning video campaigns that will FLOOD your website with unstoppable FREE traffic, all on complete and total autopilot. How to exploit a sneaky method of boosting exposure and getting your videos to the top of Google within 72 hours, guaranteed.

Get My Free Ebook


Responses

  • jennifer
    How to convert qdatastream to qtextstream?
    8 years ago

Post a comment