From f2c6c8c4c36c6f75072e1015cd0213bde2aaa086 Mon Sep 17 00:00:00 2001 From: Jelmer Vernooij Date: Sun, 4 Sep 2016 16:45:20 +0000 Subject: New upstream version 0.9.3 --- README.md | 11 +++++-- setup.py | 6 ++-- test_files/simple_3_0_test.ics | 2 +- test_files/tz_us_eastern.ics | 31 +++++++++++++++++ tests.py | 43 +++++++++++++++++++++++- vobject/base.py | 3 +- vobject/icalendar.py | 75 ++++++++++++++++++++++++++++++++---------- vobject/vcard.py | 7 ++-- 8 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 test_files/tz_us_eastern.ics diff --git a/README.md b/README.md index 8058817..5711960 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ To create an object that already has a behavior defined, run: ``` >>> import vobject ->>> > cal = vobject.newFromBehavior('vcalendar') +>>> cal = vobject.newFromBehavior('vcalendar') >>> cal.behavior ``` @@ -217,8 +217,12 @@ attributes are required. >>> j.email.value = 'jeffrey@osafoundation.org' >>> j.email.type_param = 'INTERNET' +>>> j.add('org') + +>>> j.org.value = ['Open Source Applications Foundation'] >>> j.prettyPrint() VCARD + ORG: ['Open Source Applications Foundation'] EMAIL: jeffrey@osafoundation.org params for EMAIL: TYPE ['INTERNET'] @@ -230,9 +234,10 @@ serializing will add any required computable attributes (like 'VERSION') ``` >>> j.serialize() -'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\nORG:Open Source Applications Foundation\r\nEND:VCARD\r\n' >>> j.prettyPrint() VCARD + ORG: Open Source Applications Foundation VERSION: 3.0 EMAIL: jeffrey@osafoundation.org params for EMAIL: @@ -248,6 +253,7 @@ serializing will add any required computable attributes (like 'VERSION') ... BEGIN:VCARD ... VERSION:3.0 ... EMAIL;TYPE=INTERNET:jeffrey@osafoundation.org +... ORG:Open Source Applications Foundation ... FN:Jeffrey Harris ... N:Harris;Jeffrey;;; ... END:VCARD @@ -255,6 +261,7 @@ serializing will add any required computable attributes (like 'VERSION') >>> v = vobject.readOne( s ) >>> v.prettyPrint() VCARD + ORG: Open Source Applications Foundation VERSION: 3.0 EMAIL: jeffrey@osafoundation.org params for EMAIL: diff --git a/setup.py b/setup.py index 9c21ad8..a99507b 100755 --- a/setup.py +++ b/setup.py @@ -28,10 +28,10 @@ For older changes, see from setuptools import setup, find_packages -doclines = __doc__.splitlines() +doclines = (__doc__ or '').splitlines() setup(name = "vobject", - version = "0.9.2", + version = "0.9.3", author = "Jeffrey Harris", author_email = "jeffrey@osafoundation.org", maintainer = "Sameen Karim", @@ -39,7 +39,7 @@ setup(name = "vobject", license = "Apache", zip_safe = True, url = "http://eventable.github.io/vobject/", - download_url = 'https://github.com/eventable/vobject/tarball/0.9.1', + download_url = 'https://github.com/eventable/vobject/tarball/0.9.3', bugtrack_url = "https://github.com/eventable/vobject/issues", entry_points = { 'console_scripts': [ diff --git a/test_files/simple_3_0_test.ics b/test_files/simple_3_0_test.ics index d5e4642..1faf80d 100644 --- a/test_files/simple_3_0_test.ics +++ b/test_files/simple_3_0_test.ics @@ -9,5 +9,5 @@ TEL;type=CELL:+01-(0)5-555.55.55 ACCOUNT;type=HOME:010-1234567-05 ADR;type=HOME:;;Haight Street 512\;\nEscape\, Test;Novosibirsk;;80214;Gnuland TEL;type=HOME:+01-(0)2-876.54.32 -ORG:University of Novosibirsk, Department of Octopus Parthenogenesis +ORG:University of Novosibirsk;Department of Octopus Parthenogenesis END:VCARD diff --git a/test_files/tz_us_eastern.ics b/test_files/tz_us_eastern.ics new file mode 100644 index 0000000..e611477 --- /dev/null +++ b/test_files/tz_us_eastern.ics @@ -0,0 +1,31 @@ +BEGIN:VTIMEZONE +TZID:US/Eastern +BEGIN:STANDARD +DTSTART:20001029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;UNTIL=20061029T060000Z +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:STANDARD +DTSTART:20071104T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:20000402T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:DAYLIGHT +DTSTART:20070311T020000 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +END:VTIMEZONE diff --git a/tests.py b/tests.py index 2073930..c5d95d3 100644 --- a/tests.py +++ b/tests.py @@ -516,9 +516,14 @@ class TestVcards(unittest.TestCase): #) self.assertEqual( card.org.value, - "University of Novosibirsk, Department of Octopus Parthenogenesis" + ["University of Novosibirsk", "Department of Octopus Parthenogenesis"] ) + for _ in range(3): + new_card = base.readOne(card.serialize()) + self.assertEqual(new_card.org.value, card.org.value) + card = new_card + class TestIcalendar(unittest.TestCase): """ @@ -656,6 +661,42 @@ class TestIcalendar(unittest.TestCase): apple = tzs.get('America/Montreal') ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple) + def test_pytz_timezone_serializing(self): + """ + Serializing with timezones from pytz test + """ + try: + import pytz + except ImportError: + return self.skipTest("pytz not installed") # NOQA + + # Avoid conflicting cached tzinfo from other tests + def unregister_tzid(tzid): + """Clear tzid from icalendar TZID registry""" + if icalendar.getTzid(tzid, False): + icalendar.registerTzid(tzid, None) + + unregister_tzid('US/Eastern') + eastern = pytz.timezone('US/Eastern') + cal = base.Component('VCALENDAR') + cal.setBehavior(icalendar.VCalendar2_0) + ev = cal.add('vevent') + ev.add('dtstart').value = eastern.localize( + datetime.datetime(2008, 10, 12, 9)) + serialized = cal.serialize() + + expected_vtimezone = get_test_file("tz_us_eastern.ics") + self.assertIn( + expected_vtimezone.replace('\r\n', '\n'), + serialized.replace('\r\n', '\n') + ) + + # Exhaustively test all zones (just looking for no errors) + for tzname in pytz.all_timezones: + unregister_tzid(tzname) + tz = icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname)) + tz.serialize() + def test_freeBusy(self): """ Test freebusy components diff --git a/vobject/base.py b/vobject/base.py index 03fe38e..6131d97 100644 --- a/vobject/base.py +++ b/vobject/base.py @@ -7,6 +7,7 @@ import logging import re import six import sys +import codecs # ------------------------------------ Python 2/3 compatibility challenges ---- # Python 3 no longer has a basestring type, so.... @@ -333,7 +334,7 @@ class ContentLine(VBase): qp = True self.singletonparams.remove('QUOTED-PRINTABLE') if qp: - self.value = self.value.decode('quoted-printable') + self.value = codecs.decode(self.value.encode("utf-8"), "quoted-printable").decode(self.params['ENCODING']) @classmethod def duplicate(clz, copyit): diff --git a/vobject/icalendar.py b/vobject/icalendar.py index c49dbbb..bfb00df 100644 --- a/vobject/icalendar.py +++ b/vobject/icalendar.py @@ -2,18 +2,36 @@ from __future__ import print_function +import codecs import datetime +import logging import random # for generating a UID -import six import socket import string from dateutil import rrule, tz +import six + +try: + import pytz +except ImportError: + class Pytz: + """fake pytz module (pytz is not required)""" + + class AmbiguousTimeError(Exception): + """pytz error for ambiguous times + during transition daylight->standard""" + + class NonExistentTimeError(Exception): + """pytz error for non-existent times + during transition standard->daylight""" + + pytz = Pytz # keeps quantifiedcode happy from . import behavior from .base import (VObjectError, NativeError, ValidateError, ParseError, - Component, ContentLine, logger, registerBehavior, - backslashEscape, foldOneLine, str_) + Component, ContentLine, logger, registerBehavior, + backslashEscape, foldOneLine, str_) # ------------------------------- Constants ------------------------------------ @@ -59,9 +77,9 @@ def getTzid(tzid, smart=True): tz = timezone(tzid) registerTzid(toUnicode(tzid), tz) except UnknownTimeZoneError as e: - print("Error: {0}".format(e)) + logging.error(e) except ImportError as e: - print("Error: {0}".format(e)) + logging.error(e) return tz utc = tz.tzutc() @@ -203,17 +221,26 @@ class TimezoneComponent(Component): working[transitionTo] = None else: # an offset transition was found - old_offset = tzinfo.utcoffset(transition - twoHours) + try: + old_offset = tzinfo.utcoffset(transition - twoHours) + name = tzinfo.tzname(transition) + offset = tzinfo.utcoffset(transition) + except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError): + # guaranteed that tzinfo is a pytz timezone + is_dst = (transitionTo == "daylight") + old_offset = tzinfo.utcoffset(transition - twoHours, is_dst=is_dst) + name = tzinfo.tzname(transition, is_dst=is_dst) + offset = tzinfo.utcoffset(transition, is_dst=is_dst) rule = {'end' : None, # None, or an integer year 'start' : transition, # the datetime of transition 'month' : transition.month, 'weekday' : transition.weekday(), 'hour' : transition.hour, - 'name' : tzinfo.tzname(transition), + 'name' : name, 'plus' : int( (transition.day - 1)/ 7 + 1), # nth week of the month 'minus' : fromLastWeek(transition), # nth from last week - 'offset' : tzinfo.utcoffset(transition), + 'offset' : offset, 'offsetfrom' : old_offset} if oldrule is None: @@ -402,11 +429,11 @@ class RecurringComponent(Component): dtstart = self.due.value else: # if there's no dtstart, just return None - print('failed to get dtstart with VTODO') + logging.error('failed to get dtstart with VTODO') return None except (AttributeError, KeyError): # if there's no due, just return None - print('failed to find DUE at all.') + logging.error('failed to find DUE at all.') return None # a Ruby iCalendar library escapes semi-colons in rrules, @@ -606,7 +633,7 @@ class TextBehavior(behavior.Behavior): if line.encoded: encoding = getattr(line, 'encoding_param', None) if encoding and encoding.upper() == cls.base64string: - line.value = line.value.decode('base64') + line.value = codecs.decode(self.value.encode("utf-8"), "base64").decode(encoding) else: line.value = stringToTextValues(line.value)[0] line.encoded=False @@ -619,7 +646,7 @@ class TextBehavior(behavior.Behavior): if not line.encoded: encoding = getattr(line, 'encoding_param', None) if encoding and encoding.upper() == cls.base64string: - line.value = line.value.encode('base64').replace('\n', '') + line.value = codecs.encode(self.value.encode(encoding), "base64").decode("utf-8") else: line.value = backslashEscape(str_(line.value)) line.encoded=True @@ -1620,7 +1647,7 @@ def stringToTextValues(s, listSeparator=',', charList=None, strict=False): if strict: raise ParseError(msg) else: - print(msg) + logging.error(msg) # vars which control state machine charIterator = enumerate(s) @@ -1672,7 +1699,8 @@ def stringToTextValues(s, listSeparator=',', charList=None, strict=False): else: state = "error" - error("error: unknown state: '{0!s}' reached in {1!s}".format(state, s)) + error("unknown state: '{0!s}' reached in {1!s}".format(state, s)) + def stringToDurations(s, strict=False): """ @@ -1789,7 +1817,8 @@ def stringToDurations(s, strict=False): else: state = "error" - error("error: unknown state: '{0!s}' reached in {1!s}".format(state, s)) + error("unknown state: '{0!s}' reached in {1!s}".format(state, s)) + def parseDtstart(contentline, allowSignatureMismatch=False): """ @@ -1862,9 +1891,21 @@ def getTransition(transitionTo, year, tzinfo): assert transitionTo in ('daylight', 'standard') if transitionTo == 'daylight': - def test(dt): return tzinfo.dst(dt) != zeroDelta + def test(dt): + try: + return tzinfo.dst(dt) != zeroDelta + except pytz.NonExistentTimeError: + return True # entering daylight time + except pytz.AmbiguousTimeError: + return False # entering standard time elif transitionTo == 'standard': - def test(dt): return tzinfo.dst(dt) == zeroDelta + def test(dt): + try: + return tzinfo.dst(dt) == zeroDelta + except pytz.NonExistentTimeError: + return False # entering daylight time + except pytz.AmbiguousTimeError: + return True # entering standard time newyear = datetime.datetime(year, 1, 1) monthDt = firstTransition(generateDates(year), test) if monthDt is None: diff --git a/vobject/vcard.py b/vobject/vcard.py index dd60b73..d4fe513 100644 --- a/vobject/vcard.py +++ b/vobject/vcard.py @@ -1,5 +1,7 @@ """Definitions and behavior for vCard 3.0""" +import codecs + from . import behavior from .base import ContentLine, registerBehavior, backslashEscape @@ -134,7 +136,7 @@ class VCardTextBehavior(behavior.Behavior): line.encoding_param = cls.base64string encoding = getattr(line, 'encoding_param', None) if encoding: - line.value = line.value.decode('base64') + line.value = codecs.decode(line.value.encode("utf-8"), "base64") else: line.value = stringToTextValues(line.value)[0] line.encoded=False @@ -147,7 +149,7 @@ class VCardTextBehavior(behavior.Behavior): if not line.encoded: encoding = getattr(line, 'encoding_param', None) if encoding and encoding.upper() == cls.base64string: - line.value = line.value.encode('base64').replace('\n', '') + line.value = codecs.encode(line.value.encode(coding), "base64").decode("utf-8") else: line.value = backslashEscape(line.value) line.encoded=True @@ -338,6 +340,7 @@ class OrgBehavior(VCardBehavior): if obj.isNative: return obj obj.isNative = True + obj.value = splitFields(obj.value) return obj @staticmethod -- cgit v1.2.3