summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJelmer Vernooij <jelmer@debian.org>2016-09-04 16:45:20 +0000
committerJelmer Vernooij <jelmer@debian.org>2016-09-04 16:45:20 +0000
commit679a86643caa9c35ff54b06b971560823c6f5f27 (patch)
treee1dcd0d397460b2a15a7961558d51681716332b5
parentdacd5347ed80eaf24adc1790d369ee334810af40 (diff)
parentf2c6c8c4c36c6f75072e1015cd0213bde2aaa086 (diff)
Merge tag 'upstream/0.9.3'
Upstream version 0.9.3
-rw-r--r--README.md11
-rwxr-xr-xsetup.py6
-rw-r--r--test_files/simple_3_0_test.ics2
-rw-r--r--test_files/tz_us_eastern.ics31
-rw-r--r--tests.py43
-rw-r--r--vobject/base.py3
-rw-r--r--vobject/icalendar.py75
-rw-r--r--vobject/vcard.py7
8 files changed, 151 insertions, 27 deletions
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
->>> &gt; 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:
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