diff --git a/README.txt b/README.txt index bc94dae..b2ae0d4 100644 --- a/README.txt +++ b/README.txt @@ -193,7 +193,7 @@ attributes are required. serializing will add any required computable attributes (like 'VERSION') >>> j.serialize() -u'BEGIN:VCARD\r\nVERSION:3.0\r\nEMAIL;TYPE=INTERNET:jeffrey@osafoundation.org\r\nFN:Jeffrey Harris\r\nN:Harris;Jeffrey;;;\r\nEND:VCARD\r\n' +'BEGIN:VCARD\r\nVERSION:3.0\r\nEMAIL;TYPE=INTERNET:jeffrey@osafoundation.org\r\nFN:Jeffrey Harris\r\nN:Harris;Jeffrey;;;\r\nEND:VCARD\r\n' >>> j.prettyPrint() VCARD VERSION: 3.0 diff --git a/setup.py b/setup.py index fb1ca74..d815942 100755 --- a/setup.py +++ b/setup.py @@ -5,11 +5,8 @@ Requires python 2.4 or later, dateutil (http://labix.org/python-dateutil) 1.1 or later. Recent changes: -- Added ORG behavior for vCard, ORG is now treated as a list of organizations -- Fixed UNTIL values in RRULEs to have the right value when a dateutil rruleset is created -- Fixed a problem causing DATE valued RDATEs and EXDATEs to be ignored when interpreting recurrence rules -- Added an ics_diff module and an ics_diff command line script for comparing the VEVENTs and VTODOs in similar iCalendar files - +- Added VAVAILABILITY support +- Improved wrapping of unicode lines, serialize encodes unicode as utf-8 by default For older changes, see http://vobject.skyhouseconsulting.com/history.html or http://websvn.osafoundation.org/listing.php?repname=vobject&path=/trunk/ """ @@ -23,7 +20,7 @@ # Metadata PACKAGE_NAME = "vobject" -PACKAGE_VERSION = "0.5.0" +PACKAGE_VERSION = "0.6.0" ALL_EXTS = ['*.py', '*.ics', '*.txt'] diff --git a/src/vobject/base.py b/src/vobject/base.py index 0fb3a28..abb6a9e 100644 --- a/src/vobject/base.py +++ b/src/vobject/base.py @@ -855,13 +855,35 @@ def dquoteEscape(param): return param def foldOneLine(outbuf, input, lineLength = 75): - if isinstance(input, basestring): input = StringIO.StringIO(input) - input.seek(0) - outbuf.write(input.read(lineLength) + CRLF) - brokenline = input.read(lineLength - 1) - while brokenline: - outbuf.write(' ' + brokenline + CRLF) - brokenline = input.read(lineLength - 1) + # Folding line procedure that ensures multi-byte utf-8 sequences are not broken + # across lines + + if len(input) < lineLength: + # Optimize for unfolded line case + outbuf.write(input) + else: + # Look for valid utf8 range and write that out + start = 0 + written = 0 + while written < len(input): + # Start max length -1 chars on from where we are + offset = start + lineLength - 1 + if offset >= len(input): + line = input[start:] + outbuf.write(line) + written = len(input) + else: + # Check whether next char is valid utf8 lead byte + while (input[offset] > 0x7F) and ((ord(input[offset]) & 0xC0) == 0x80): + # Step back until we have a valid char + offset -= 1 + + line = input[start:offset] + outbuf.write(line) + outbuf.write("\r\n ") + written += offset - start + start = offset + outbuf.write("\r\n") def defaultSerialize(obj, buf, lineLength): """Encode and fold obj and its children, write to buf or return a string.""" @@ -874,12 +896,12 @@ def defaultSerialize(obj, buf, lineLength): else: groupString = obj.group + '.' if obj.useBegin: - foldOneLine(outbuf, groupString + u"BEGIN:" + obj.name, lineLength) + foldOneLine(outbuf, str(groupString + u"BEGIN:" + obj.name), lineLength) for child in obj.getSortedChildren(): #validate is recursive, we only need to validate once child.serialize(outbuf, lineLength, validate=False) if obj.useBegin: - foldOneLine(outbuf, groupString + u"END:" + obj.name, lineLength) + foldOneLine(outbuf, str(groupString + u"END:" + obj.name), lineLength) if DEBUG: logger.debug("Finished %s" % obj.name.upper()) elif isinstance(obj, ContentLine): @@ -887,14 +909,18 @@ def defaultSerialize(obj, buf, lineLength): if obj.behavior and not startedEncoded: obj.behavior.encode(obj) s=StringIO.StringIO() #unfolded buffer if obj.group is not None: - s.write(obj.group + '.') + s.write(str(obj.group + '.')) if DEBUG: logger.debug("Serializing line" + str(obj)) - s.write(obj.name.upper()) + s.write(str(obj.name.upper())) for key, paramvals in obj.params.iteritems(): - s.write(';' + key + '=' + ','.join(map(dquoteEscape, paramvals))) - s.write(':' + obj.value) + s.write(';' + str(key) + '=' + ','.join(map(dquoteEscape, paramvals)).encode("utf-8")) + if isinstance(obj.value, unicode): + strout = obj.value.encode("utf-8") + else: + strout = obj.value + s.write(':' + strout) if obj.behavior and not startedEncoded: obj.behavior.decode(obj) - foldOneLine(outbuf, s, lineLength) + foldOneLine(outbuf, s.getvalue(), lineLength) if DEBUG: logger.debug("Finished %s line" % obj.name.upper()) return buf or outbuf.getvalue() @@ -1057,7 +1083,7 @@ def newFromBehavior(name, id=None): else: obj = ContentLine(name, [], '') obj.behavior = behavior - obj.isNative = True + obj.isNative = False return obj diff --git a/src/vobject/icalendar.py b/src/vobject/icalendar.py index e61a2af..fd18910 100644 --- a/src/vobject/icalendar.py +++ b/src/vobject/icalendar.py @@ -782,20 +782,21 @@ def encode(line): #------------------------ Registered Behavior subclasses ----------------------- class VCalendar2_0(VCalendarComponentBehavior): - """vCalendar 2.0 behavior.""" + """vCalendar 2.0 behavior. With added VAVAILABILITY support.""" name = 'VCALENDAR' description = 'vCalendar 2.0, also known as iCalendar.' versionString = '2.0' sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone') - knownChildren = {'CALSCALE': (0, 1, None),#min, max, behaviorRegistry id - 'METHOD': (0, 1, None), - 'VERSION': (0, 1, None),#required, but auto-generated - 'PRODID': (1, 1, None), - 'VTIMEZONE': (0, None, None), - 'VEVENT': (0, None, None), - 'VTODO': (0, None, None), - 'VJOURNAL': (0, None, None), - 'VFREEBUSY': (0, None, None) + knownChildren = {'CALSCALE': (0, 1, None),#min, max, behaviorRegistry id + 'METHOD': (0, 1, None), + 'VERSION': (0, 1, None),#required, but auto-generated + 'PRODID': (1, 1, None), + 'VTIMEZONE': (0, None, None), + 'VEVENT': (0, None, None), + 'VTODO': (0, None, None), + 'VJOURNAL': (0, None, None), + 'VFREEBUSY': (0, None, None), + 'VAVAILABILITY': (0, None, None), } @classmethod @@ -941,7 +942,7 @@ class VEvent(RecurringBehavior): @classmethod def validate(cls, obj, raiseException, *args): - if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'): + if obj.contents.has_key('dtend') and obj.contents.has_key('duration'): if raiseException: m = "VEVENT components cannot contain both DTEND and DURATION\ components" @@ -996,7 +997,7 @@ class VTodo(RecurringBehavior): @classmethod def validate(cls, obj, raiseException, *args): - if obj.contents.has_key('DUE') and obj.contents.has_key('DURATION'): + if obj.contents.has_key('due') and obj.contents.has_key('duration'): if raiseException: m = "VTODO components cannot contain both DUE and DURATION\ components" @@ -1047,12 +1048,14 @@ class VFreeBusy(VCalendarComponentBehavior): >>> vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) >>> vfb.add('dtend').value = vfb.dtstart.value + twoHours >>> vfb.add('freebusy').value = [(vfb.dtstart.value, twoHours / 2)] + >>> vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)] >>> print vfb.serialize() BEGIN:VFREEBUSY UID:test DTSTART:20060216T010000Z DTEND:20060216T030000Z FREEBUSY:20060216T010000Z/PT1H + FREEBUSY:20060216T010000Z/20060216T030000Z END:VFREEBUSY """ @@ -1172,7 +1175,7 @@ def validate(cls, obj, raiseException, *args): ; and MUST NOT occur more than once description / - if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'): + if obj.contents.has_key('dtend') and obj.contents.has_key('duration'): if raiseException: m = "VEVENT components cannot contain both DTEND and DURATION\ components" @@ -1185,6 +1188,117 @@ def validate(cls, obj, raiseException, *args): registerBehavior(VAlarm) +class VAvailability(VCalendarComponentBehavior): + """Availability state behavior. + + >>> vav = newFromBehavior('VAVAILABILITY') + >>> vav.add('uid').value = 'test' + >>> vav.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + >>> vav.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc) + >>> vav.add('dtend').value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc) + >>> vav.add('busytype').value = "BUSY" + >>> av = newFromBehavior('AVAILABLE') + >>> av.add('uid').value = 'test1' + >>> av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + >>> av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc) + >>> av.add('dtend').value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc) + >>> av.add('summary').value = "Available in the morning" + >>> ignore = vav.add(av) + >>> print vav.serialize() + BEGIN:VAVAILABILITY + UID:test + DTSTART:20060216T000000Z + DTEND:20060217T000000Z + BEGIN:AVAILABLE + UID:test1 + DTSTART:20060216T090000Z + DTEND:20060216T120000Z + DTSTAMP:20060215T000000Z + SUMMARY:Available in the morning + END:AVAILABLE + BUSYTYPE:BUSY + DTSTAMP:20060215T000000Z + END:VAVAILABILITY + + """ + name='VAVAILABILITY' + description='A component used to represent a user\'s available time slots.' + sortFirst = ('uid', 'dtstart', 'duration', 'dtend') + knownChildren = {'UID': (1, 1, None),#min, max, behaviorRegistry id + 'DTSTAMP': (1, 1, None), + 'BUSYTYPE': (0, 1, None), + 'CREATED': (0, 1, None), + 'DTSTART': (0, 1, None), + 'LAST-MODIFIED': (0, 1, None), + 'ORGANIZER': (0, 1, None), + 'SEQUENCE': (0, 1, None), + 'SUMMARY': (0, 1, None), + 'URL': (0, 1, None), + 'DTEND': (0, 1, None), + 'DURATION': (0, 1, None), + 'CATEGORIES': (0, None, None), + 'COMMENT': (0, None, None), + 'CONTACT': (0, None, None), + 'AVAILABLE': (0, None, None), + } + + @classmethod + def validate(cls, obj, raiseException, *args): + if obj.contents.has_key('dtend') and obj.contents.has_key('duration'): + if raiseException: + m = "VAVAILABILITY components cannot contain both DTEND and DURATION\ + components" + raise ValidateError(m) + return False + else: + return super(VAvailability, cls).validate(obj, raiseException, *args) + +registerBehavior(VAvailability) + +class Available(RecurringBehavior): + """Event behavior.""" + name='AVAILABLE' + sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend') + + description='Defines a period of time in which a user is normally available.' + knownChildren = {'DTSTAMP': (1, 1, None),#min, max, behaviorRegistry id + 'DTSTART': (1, 1, None), + 'UID': (1, 1, None), + 'DTEND': (0, 1, None), #NOTE: One of DtEnd or + 'DURATION': (0, 1, None), # Duration must appear, but not both + 'CREATED': (0, 1, None), + 'LAST-MODIFIED':(0, 1, None), + 'RECURRENCE-ID':(0, 1, None), + 'RRULE': (0, 1, None), + 'SUMMARY': (0, 1, None), + 'CATEGORIES': (0, None, None), + 'COMMENT': (0, None, None), + 'CONTACT': (0, None, None), + 'EXDATE': (0, None, None), + 'RDATE': (0, None, None), + } + + @classmethod + def validate(cls, obj, raiseException, *args): + has_dtend = obj.contents.has_key('dtend') + has_duration = obj.contents.has_key('duration') + if has_dtend and has_duration: + if raiseException: + m = "AVAILABLE components cannot contain both DTEND and DURATION\ + properties" + raise ValidateError(m) + return False + elif not (has_dtend or has_duration): + if raiseException: + m = "AVAILABLE components must contain one of DTEND or DURATION\ + properties" + raise ValidateError(m) + return False + else: + return super(Available, cls).validate(obj, raiseException, *args) + +registerBehavior(Available) + class Duration(behavior.Behavior): """Behavior for Duration ContentLines. Transform to datetime.timedelta.""" name = 'DURATION' @@ -1344,7 +1458,7 @@ class RRule(behavior.Behavior): textList = ['CALSCALE', 'METHOD', 'PRODID', 'CLASS', 'COMMENT', 'DESCRIPTION', 'LOCATION', 'STATUS', 'SUMMARY', 'TRANSP', 'CONTACT', 'RELATED-TO', - 'UID', 'ACTION', 'REQUEST-STATUS', 'TZID'] + 'UID', 'ACTION', 'REQUEST-STATUS', 'TZID', 'BUSYTYPE'] map(lambda x: registerBehavior(TextBehavior, x), textList) multiTextList = ['CATEGORIES', 'RESOURCES'] @@ -1678,7 +1792,7 @@ def stringToPeriod(s, tzinfo=None): delta = stringToDurations(valEnd)[0] return (start, delta) else: - return (start, stringToDateTime(valEnd, tzinfo) - start) + return (start, stringToDateTime(valEnd, tzinfo)) def getTransition(transitionTo, year, tzinfo): diff --git a/tests/more_tests.txt b/tests/more_tests.txt index 9062817..628b091 100644 --- a/tests/more_tests.txt +++ b/tests/more_tests.txt @@ -8,9 +8,9 @@ Unicode in vCards >>> card.add('adr').value = vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA') >>> card , , ]> ->>> card.serialize() +>>> card.serialize().decode("utf-8") u'BEGIN:VCARD\r\nVERSION:3.0\r\nADR:;;5\u1234 Nowhere\\, Apt 1;Berkeley;CA;94704;USA\r\nFN:Hello\u1234 World!\r\nN:World;Hello\u1234;;;\r\nEND:VCARD\r\n' ->>> print card.serialize().encode('ascii', 'replace') +>>> print card.serialize().decode("utf-8").encode('ascii', 'replace') BEGIN:VCARD VERSION:3.0 ADR:;;5? Nowhere\, Apt 1;Berkeley;CA;94704;USA diff --git a/tests/tests.py b/tests/tests.py index d2bd647..b36793b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -302,7 +302,7 @@ def _test(): >>> silly.stuff >>> original = silly.serialize() - >>> f3 = StringIO.StringIO(original) + >>> f3 = StringIO.StringIO(original.decode("utf-8")) >>> silly2 = base.readOne(f3) >>> silly2.serialize()==original True @@ -311,7 +311,7 @@ def _test(): >>> ex1 <*unnamed*| [, , , , , ]> >>> ex1.serialize() - u'CN:Babs Jensen\r\nCN:Barbara J Jensen\r\nEMAIL:babs@umich.edu\r\nPHONE:+1 313 747-4454\r\nSN:Jensen\r\nX-ID:1234567890\r\n' + 'CN:Babs Jensen\r\nCN:Barbara J Jensen\r\nEMAIL:babs@umich.edu\r\nPHONE:+1 313 747-4454\r\nSN:Jensen\r\nX-ID:1234567890\r\n' """, "Import icaltest" : @@ -328,7 +328,7 @@ def _test(): >>> c.vevent.valarm.description.value u'Event reminder, with comma\nand line feed' >>> c.vevent.valarm.description.serialize() - u'DESCRIPTION:Event reminder\\, with comma\\nand line feed\r\n' + 'DESCRIPTION:Event reminder\\, with comma\\nand line feed\r\n' >>> vevent = c.vevent.transformFromNative() >>> vevent.rrule @@ -342,11 +342,13 @@ def _test(): >>> icalendar.stringToTextValues('abcd,efgh') ['abcd', 'efgh'] >>> icalendar.stringToPeriod("19970101T180000Z/19970102T070000Z") - (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.timedelta(0, 46800)) + (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc())) + >>> icalendar.stringToPeriod("19970101T180000Z/PT1H") + (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.timedelta(0, 3600)) >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904")) >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H")) - + """, "read failure" : @@ -383,6 +385,7 @@ def _test(): >>> vevent.summary.value u'The title \u3053\u3093\u306b\u3061\u306f\u30ad\u30c6\u30a3' >>> summary = vevent.summary.value + >>> test = str(vevent.serialize()), """, # make sure date valued UNTILs in rrules are in a reasonable timezone, @@ -696,8 +699,8 @@ def _test(): BEGIN:VCARD VERSION:3.0 ACCOUNT;TYPE=HOME:010-1234567-05 - ADR;TYPE=HOME:;;Haight Street 512\;\nEscape\, Test;Novosibirsk;;80214;Gnula - nd + ADR;TYPE=HOME:;;Haight Street 512\;\nEscape\, Test;Novosibirsk;;80214;Gnul + and BDAY;VALUE=date:02-10 FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto) N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto);;; @@ -731,9 +734,9 @@ def _test(): u'home' >>> card.group = card.tel.group = 'new' >>> card.tel.serialize().strip() - u'new.TEL;TYPE=fax,voice,msg:+49 3581 123456' + 'new.TEL;TYPE=fax,voice,msg:+49 3581 123456' >>> card.serialize().splitlines()[0] - u'new.BEGIN:VCARD' + 'new.BEGIN:VCARD' >>> dtstart = base.newFromBehavior('dtstart') >>> dtstart.group = "badgroup" >>> dtstart.serialize()