diff options
author | Jelmer Vernooij <jelmer@debian.org> | 2016-09-04 16:45:20 +0000 |
---|---|---|
committer | Jelmer Vernooij <jelmer@debian.org> | 2016-09-04 16:45:20 +0000 |
commit | 679a86643caa9c35ff54b06b971560823c6f5f27 (patch) | |
tree | e1dcd0d397460b2a15a7961558d51681716332b5 | |
parent | dacd5347ed80eaf24adc1790d369ee334810af40 (diff) | |
parent | f2c6c8c4c36c6f75072e1015cd0213bde2aaa086 (diff) |
Merge tag 'upstream/0.9.3'
Upstream version 0.9.3
-rw-r--r-- | README.md | 11 | ||||
-rwxr-xr-x | setup.py | 6 | ||||
-rw-r--r-- | test_files/simple_3_0_test.ics | 2 | ||||
-rw-r--r-- | test_files/tz_us_eastern.ics | 31 | ||||
-rw-r--r-- | tests.py | 43 | ||||
-rw-r--r-- | vobject/base.py | 3 | ||||
-rw-r--r-- | vobject/icalendar.py | 75 | ||||
-rw-r--r-- | vobject/vcard.py | 7 |
8 files changed, 151 insertions, 27 deletions
@@ -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 <class 'vobject.icalendar.VCalendar2_0'> ``` @@ -217,8 +217,12 @@ attributes are required. <EMAIL{}> >>> j.email.value = 'jeffrey@osafoundation.org' >>> j.email.type_param = 'INTERNET' +>>> j.add('org') + <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: @@ -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 @@ -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 |