1.1 --- a/tests/test_calendar_stream.py Sun Nov 02 04:06:23 2008 +0100
1.2 +++ b/tests/test_calendar_stream.py Sun Nov 02 23:06:25 2008 +0100
1.3 @@ -6,7 +6,7 @@
1.4 out = codecs.open("tmp.ics", "w", encoding="utf-8")
1.5 try:
1.6 doc = vCalendar.iterparse(f)
1.7 - w = vCalendar.vCalendarStreamWriter(out)
1.8 + w = vCalendar.iterwrite(out)
1.9 for name, parameters, value in doc:
1.10 print "%r, %r, %r" % (name, parameters, value)
1.11 w.write(name, parameters, value)
2.1 --- a/tests/test_card_stream.py Sun Nov 02 04:06:23 2008 +0100
2.2 +++ b/tests/test_card_stream.py Sun Nov 02 23:06:25 2008 +0100
2.3 @@ -6,7 +6,7 @@
2.4 out = codecs.open("tmp.vcf", "w", encoding="utf-8")
2.5 try:
2.6 doc = vContent.iterparse(f)
2.7 - w = vContent.StreamWriter(out)
2.8 + w = vContent.iterwrite(out)
2.9 for name, parameters, value in doc:
2.10 print "%r, %r, %r" % (name, parameters, value)
2.11 w.write(name, parameters, value)
3.1 --- a/tests/test_reader.py Sun Nov 02 04:06:23 2008 +0100
3.2 +++ b/tests/test_reader.py Sun Nov 02 23:06:25 2008 +0100
3.3 @@ -4,45 +4,50 @@
3.4 import StringIO
3.5
3.6 s = StringIO.StringIO("""PROP:p1=v1;p2
3.7 -=v2;p21;p3=v3;"p4"="v4";"p5=v5
3.8 -;p5=v5":"hello
3.9 -world"
3.10 + =v2;p21;p3=v3;"p4"="v4";"p5=v5
3.11 + ;p5=v5":"hello
3.12 + world\\nend test"
3.13 """)
3.14
3.15 r = vContent.Reader(s)
3.16 +line = r.get_content_line()
3.17 +print repr(line.text)
3.18
3.19 -data = r.read_until(r.SEPARATORS)
3.20 +data = line.search(line.SEPARATORS)
3.21 print data
3.22 assert data == ("PROP", ":")
3.23 -data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.24 +data = line.search(line.SEPARATORS_PLUS_EQUALS)
3.25 print data
3.26 assert data == ("p1", "=")
3.27 -data = r.read_until(r.SEPARATORS)
3.28 +data = line.search(line.SEPARATORS)
3.29 print data
3.30 assert data == ("v1", ";")
3.31 -data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.32 +data = line.search(line.SEPARATORS_PLUS_EQUALS)
3.33 print data
3.34 -assert data == ("p2\n", "=")
3.35 -data = r.read_until(r.SEPARATORS)
3.36 +assert data == ("p2", "=")
3.37 +data = line.search(line.SEPARATORS)
3.38 print data
3.39 assert data == ("v2", ";")
3.40 -data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.41 +data = line.search(line.SEPARATORS_PLUS_EQUALS)
3.42 print data
3.43 assert data == ("p21", ";")
3.44 -data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.45 +data = line.search(line.SEPARATORS_PLUS_EQUALS)
3.46 print data
3.47 assert data == ("p3", "=")
3.48 -data = r.read_until(r.SEPARATORS)
3.49 +data = line.search(line.SEPARATORS)
3.50 print data
3.51 assert data == ("v3", ";")
3.52 -data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.53 +data = line.search(line.SEPARATORS_PLUS_EQUALS)
3.54 print data
3.55 assert data == ('"p4"', "=")
3.56 -data = r.read_until(r.SEPARATORS)
3.57 +data = line.search(line.SEPARATORS)
3.58 print data
3.59 assert data == ('"v4"', ";")
3.60 -data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.61 +data = line.search(line.SEPARATORS_PLUS_EQUALS)
3.62 print data
3.63 -assert data == ('"p5=v5\n;p5=v5"', ":")
3.64 +assert data == ('"p5=v5;p5=v5"', ":")
3.65 +data = line.get_remaining()
3.66 +print repr(data)
3.67 +assert data == '"hello world\\nend test"'
3.68
3.69 # vim: tabstop=4 expandtab shiftwidth=4
5.1 --- a/vContent.py Sun Nov 02 04:06:23 2008 +0100
5.2 +++ b/vContent.py Sun Nov 02 23:06:25 2008 +0100
5.3 @@ -52,9 +52,6 @@
5.4
5.5 "A simple class wrapping a file, providing simple pushback capabilities."
5.6
5.7 - SEPARATORS = re.compile('[;:"]')
5.8 - SEPARATORS_PLUS_EQUALS = re.compile('[=;:"]')
5.9 -
5.10 def __init__(self, f, non_standard_newline=0):
5.11
5.12 """
5.13 @@ -66,7 +63,7 @@
5.14 self.f = f
5.15 self.non_standard_newline = non_standard_newline
5.16 self.lines = []
5.17 - self.line_number = 0
5.18 + self.line_number = 1 # about to read line 1
5.19
5.20 def pushback(self, line):
5.21
5.22 @@ -98,23 +95,69 @@
5.23 else:
5.24 return line
5.25
5.26 - def read_until(self, targets):
5.27 + def read_content_line(self):
5.28
5.29 """
5.30 - Read from the stream until one of the 'targets' is seen. Return the
5.31 - string from the current position up to the target found, along with the
5.32 - target string, using a tuple of the form (string, target). If no target
5.33 - was found, return the entire string together with a target of None.
5.34 + Read an entire content line, itself potentially consisting of many
5.35 + physical lines of text.
5.36 """
5.37
5.38 - # Remember the entire text read and the index of the current line in
5.39 - # that text.
5.40 + line = self.readline()
5.41
5.42 - lines = []
5.43 + # Strip all appropriate whitespace from the right end of each line.
5.44 + # For subsequent lines, remove the first whitespace character.
5.45 + # See section 4.1 of the iCalendar specification.
5.46 +
5.47 + lines = [line.rstrip("\r\n")]
5.48
5.49 line = self.readline()
5.50 - lines.append(line)
5.51 - start = 0
5.52 + while line.startswith(" ") or line.startswith("\t"):
5.53 + lines.append(line[1:].rstrip("\r\n"))
5.54 + line = self.readline()
5.55 +
5.56 + # Since one line too many will have been read, push the line back into
5.57 + # the file.
5.58 +
5.59 + if line:
5.60 + self.pushback(line)
5.61 +
5.62 + return "".join(lines)
5.63 +
5.64 + def get_content_line(self):
5.65 +
5.66 + "Return a content line object for the current line."
5.67 +
5.68 + return ContentLine(self.read_content_line())
5.69 +
5.70 +class ContentLine:
5.71 +
5.72 + "A content line which can be searched."
5.73 +
5.74 + SEPARATORS = re.compile('[;:"]')
5.75 + SEPARATORS_PLUS_EQUALS = re.compile('[=;:"]')
5.76 +
5.77 + def __init__(self, text):
5.78 + self.text = text
5.79 + self.start = 0
5.80 +
5.81 + def get_remaining(self):
5.82 +
5.83 + "Get the remaining text from the content line."
5.84 +
5.85 + return self.text[self.start:]
5.86 +
5.87 + def search(self, targets):
5.88 +
5.89 + """
5.90 + Find one of the 'targets' in the text, returning the string from the
5.91 + current position up to the target found, along with the target string,
5.92 + using a tuple of the form (string, target). If no target was found,
5.93 + return the entire string together with a target of None.
5.94 + """
5.95 +
5.96 + text = self.text
5.97 + start = pos = self.start
5.98 + length = len(text)
5.99
5.100 # Remember the first target.
5.101
5.102 @@ -122,23 +165,21 @@
5.103 first_pos = None
5.104 in_quoted_region = 0
5.105
5.106 - # Process each line, looking for the targets.
5.107 + # Process the text, looking for the targets.
5.108
5.109 - while line != "":
5.110 - match = targets.search(line, start)
5.111 + while pos < length:
5.112 + match = targets.search(text, pos)
5.113
5.114 - # Where nothing matches, get the next line.
5.115 + # Where nothing matches, end the search.
5.116
5.117 if match is None:
5.118 - line = self.readline()
5.119 - lines.append(line)
5.120 - start = 0
5.121 + pos = length
5.122
5.123 # Where a double quote matches, toggle the region state.
5.124
5.125 elif match.group() == '"':
5.126 in_quoted_region = not in_quoted_region
5.127 - start = match.end()
5.128 + pos = match.end()
5.129
5.130 # Where something else matches outside a region, stop searching.
5.131
5.132 @@ -150,25 +191,16 @@
5.133 # Otherwise, keep looking for the end of the region.
5.134
5.135 else:
5.136 - start = match.end()
5.137 + pos = match.end()
5.138
5.139 # Where no more input can provide the targets, return a special result.
5.140
5.141 else:
5.142 - text = "".join(lines)
5.143 - return text, None
5.144 -
5.145 - # Push back the text after the target.
5.146 + self.start = length
5.147 + return text[start:], None
5.148
5.149 - after_target = lines[-1][first_pos + len(first):]
5.150 - self.pushback(after_target)
5.151 -
5.152 - # Produce the lines until the matching line, together with the portion
5.153 - # of the matching line before the target.
5.154 -
5.155 - lines[-1] = lines[-1][:first_pos]
5.156 - text = "".join(lines)
5.157 - return text, first
5.158 + self.start = match.end()
5.159 + return text[start:first_pos], first
5.160
5.161 class StreamParser:
5.162
5.163 @@ -211,24 +243,30 @@
5.164 """
5.165
5.166 f = self.f
5.167 + line_number = f.line_number
5.168 + line = f.get_content_line()
5.169
5.170 - parameters = {}
5.171 - name, sep = f.read_until(f.SEPARATORS)
5.172 + # Read the property name.
5.173
5.174 + name, sep = line.search(line.SEPARATORS)
5.175 name = name.strip()
5.176
5.177 if not name and sep is None:
5.178 raise StopIteration
5.179
5.180 + # Read the parameters.
5.181 +
5.182 + parameters = {}
5.183 +
5.184 while sep == ";":
5.185
5.186 # Find the actual modifier.
5.187
5.188 - parameter_name, sep = f.read_until(f.SEPARATORS_PLUS_EQUALS)
5.189 + parameter_name, sep = line.search(line.SEPARATORS_PLUS_EQUALS)
5.190 parameter_name = parameter_name.strip()
5.191
5.192 if sep == "=":
5.193 - parameter_value, sep = f.read_until(f.SEPARATORS)
5.194 + parameter_value, sep = line.search(line.SEPARATORS)
5.195 parameter_value = parameter_value.strip()
5.196 else:
5.197 parameter_value = None
5.198 @@ -240,27 +278,11 @@
5.199 # Get the value content.
5.200
5.201 if sep != ":":
5.202 - raise ValueError, f.line_number
5.203 -
5.204 - # Strip all appropriate whitespace from the right end of each line.
5.205 - # For subsequent lines, remove the first whitespace character.
5.206 - # See section 4.1 of the iCalendar specification.
5.207 + raise ValueError, line_number
5.208
5.209 - line = f.readline()
5.210 - value_lines = [line.rstrip("\r\n")]
5.211 - line = f.readline()
5.212 - while line != "" and line[0] in [" ", "\t"]:
5.213 - value_lines.append(line.rstrip("\r\n")[1:])
5.214 - line = f.readline()
5.215 + # Obtain and decode the value.
5.216
5.217 - # Since one line too many will have been read, push the line back into the
5.218 - # file.
5.219 -
5.220 - f.pushback(line)
5.221 -
5.222 - # Decode the value.
5.223 -
5.224 - value = self.decode(name, parameters, "".join(value_lines))
5.225 + value = self.decode(name, parameters, line.get_remaining())
5.226
5.227 return name, parameters, value
5.228
5.229 @@ -384,16 +406,65 @@
5.230
5.231 # Writer classes.
5.232
5.233 +class Writer:
5.234 +
5.235 + "A simple class wrapping a file, providing simple output capabilities."
5.236 +
5.237 + default_line_length = 76
5.238 +
5.239 + def __init__(self, f, line_length=None):
5.240 +
5.241 + """
5.242 + Initialise the object with the file 'f'. If 'line_length' is set, the
5.243 + length of written lines will conform to the specified value instead of
5.244 + the default value.
5.245 + """
5.246 +
5.247 + self.f = f
5.248 + self.line_length = line_length or self.default_line_length
5.249 + self.char_offset = 0
5.250 +
5.251 + def write(self, text):
5.252 +
5.253 + "Write the 'text' to the file."
5.254 +
5.255 + f = self.f
5.256 + line_length = self.line_length
5.257 +
5.258 + i = 0
5.259 + remaining = len(text)
5.260 +
5.261 + while remaining:
5.262 + space = line_length - self.char_offset
5.263 + if remaining > space:
5.264 + f.write(text[i:i + space])
5.265 + f.write("\r\n ")
5.266 + self.char_offset = 1
5.267 + i += space
5.268 + remaining -= space
5.269 + else:
5.270 + f.write(text[i:])
5.271 + self.char_offset += remaining
5.272 + i += remaining
5.273 + remaining = 0
5.274 +
5.275 + def end_line(self):
5.276 +
5.277 + "End the current content line."
5.278 +
5.279 + if self.char_offset > 0:
5.280 + self.char_offset = 0
5.281 + self.f.write("\r\n")
5.282 +
5.283 class StreamWriter:
5.284
5.285 "A stream writer for content in vCard/vCalendar/iCalendar-like formats."
5.286
5.287 - def __init__(self, f, line_length=76):
5.288 + def __init__(self, f):
5.289
5.290 "Initialise the parser for the given file 'f'."
5.291
5.292 self.f = f
5.293 - self.line_length = line_length
5.294
5.295 def write(self, name, parameters, value):
5.296
5.297 @@ -405,12 +476,14 @@
5.298 f = self.f
5.299
5.300 f.write(name)
5.301 - self.write_parameters(parameters)
5.302 + for parameter_name, parameter_value in parameters.items():
5.303 + f.write(";")
5.304 + f.write(parameter_name)
5.305 + f.write("=")
5.306 + f.write(parameter_value)
5.307 f.write(":")
5.308 -
5.309 - for line in self.fold(self.encode(name, parameters, value)):
5.310 - f.write(line)
5.311 - f.write("\r\n")
5.312 + f.write(self.encode(name, parameters, value))
5.313 + f.end_line()
5.314
5.315 def encode_content(self, value):
5.316
5.317 @@ -420,18 +493,6 @@
5.318
5.319 # Internal methods.
5.320
5.321 - def write_parameters(self, parameters):
5.322 -
5.323 - "Write the given 'parameters'."
5.324 -
5.325 - f = self.f
5.326 -
5.327 - for parameter_name, parameter_value in parameters.items():
5.328 - f.write(";")
5.329 - f.write(parameter_name)
5.330 - f.write("=")
5.331 - f.write(parameter_value)
5.332 -
5.333 def encode(self, name, parameters, value):
5.334
5.335 "Encode using 'name' and 'parameters' the given 'value'."
5.336 @@ -446,22 +507,6 @@
5.337
5.338 return self.encode_content(value)
5.339
5.340 - def fold(self, text):
5.341 -
5.342 - "Fold the given 'text'."
5.343 -
5.344 - line_length = self.line_length
5.345 - i = 0
5.346 - lines = []
5.347 -
5.348 - line = text[i:i+line_length]
5.349 - while line:
5.350 - lines.append(line)
5.351 - i += line_length
5.352 - line = text[i:i+line_length]
5.353 -
5.354 - return lines
5.355 -
5.356 # Public functions.
5.357
5.358 def parse(f, non_standard_newline=0, parser_cls=None):
5.359 @@ -502,4 +547,9 @@
5.360 parser = (parser_cls or StreamParser)(reader)
5.361 return iter(parser)
5.362
5.363 +def iterwrite(f, line_length=None, writer_cls=None):
5.364 + _writer = Writer(f, line_length)
5.365 + writer = (writer_cls or StreamWriter)(_writer)
5.366 + return writer
5.367 +
5.368 # vim: tabstop=4 expandtab shiftwidth=4