Merge pull request #75 from untitaker/multiline_uids

Multiline uids

Fix #74
This commit is contained in:
Markus Unterwaditzer 2014-06-12 13:27:30 +02:00
commit 967540fe62
4 changed files with 141 additions and 43 deletions

View file

@ -85,3 +85,54 @@ def test_hash_item():
b = u'\n'.join(line for line in a.splitlines()
if u'PRODID' not in line and u'VERSION' not in line)
assert vobject.hash_item(a) == vobject.hash_item(b)
def test_multiline_uid():
a = (u'BEGIN:FOO\r\n'
u'UID:123456789abcd\r\n'
u' efgh\r\n'
u'END:FOO\r\n')
assert vobject.Item(a).uid == u'123456789abcdefgh'
def test_multiline_uid_complex():
a = u'''
BEGIN:VCALENDAR
BEGIN:VTIMEZONE
TZID:Europe/Rome
X-LIC-LOCATION:Europe/Rome
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART:20140124T133000Z
DTEND:20140124T143000Z
DTSTAMP:20140612T090652Z
UID:040000008200E00074C5B7101A82E0080000000050AAABEEF50DCF0100000000000000
001000000062548482FA830A46B9EA62114AC9F0EF
CREATED:20140110T102231Z
DESCRIPTION:Test.
LAST-MODIFIED:20140123T095221Z
LOCATION:25.12.01.51
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Präsentation
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
'''.strip()
assert vobject.Item(a).uid == (u'040000008200E00074C5B7101A82E008000000005'
u'0AAABEEF50DCF0100000000000000001000000062'
u'548482FA830A46B9EA62114AC9F0EF')

View file

@ -9,42 +9,7 @@
from .. import exceptions
from .. import utils
from ..utils.compat import text_type
class Item(object):
'''should-be-immutable wrapper class for VCALENDAR (VEVENT, VTODO) and
VCARD'''
uid = None
'''Global identifier of the item, across storages, doesn't change after a
modification of the item.'''
raw = None
'''Raw content of the item, which vdirsyncer doesn't validate in any
way.'''
hash = None
'''Hash of self.raw, used for etags.'''
ident = None
'''Used for generating hrefs and matching up items during synchronization.
This is either the UID or the hash of the item's content.'''
def __init__(self, raw):
assert isinstance(raw, text_type)
for line in raw.splitlines():
if line.startswith(u'UID:'):
uid = line[4:].strip()
if uid:
self.uid = uid
self.raw = raw
self.hash = utils.vobject.hash_item(raw)
self.ident = self.uid or self.hash
from vdirsyncer.utils.vobject import Item # noqa
class Storage(object):

View file

@ -15,6 +15,7 @@ from .compat import urlparse, get_raw_input
logger = log.get(__name__)
_missing = object()
try:
@ -288,3 +289,25 @@ def checkfile(path, create=False):
'True in your configuration to automatically '
'create it, or create it '
'yourself.'.format(path))
class cached_property(object):
'''
Copied from Werkzeug.
Copyright 2007-2014 Armin Ronacher
'''
def __init__(self, func, name=None, doc=None):
self.__name__ = name or func.__name__
self.__module__ = func.__module__
self.__doc__ = doc or func.__doc__
self.func = func
def __get__(self, obj, type=None):
if obj is None:
return self
value = obj.__dict__.get(self.__name__, _missing)
if value is _missing:
value = self.func(obj)
obj.__dict__[self.__name__] = value
return value

View file

@ -12,6 +12,7 @@ import icalendar.cal
import icalendar.parser
import icalendar.caselessdict
from . import cached_property
from .compat import text_type, itervalues
@ -47,13 +48,71 @@ ICALENDAR_ORIGINAL_ORDER_SUPPORT = \
hasattr(icalendar.caselessdict.CaselessDict, '__reversed__')
def normalize_item(text, ignore_props=IGNORE_PROPS, use_icalendar=True):
try:
if not use_icalendar:
raise Exception()
lines = to_unicode_lines(icalendar.cal.Component.from_ical(text))
except Exception:
lines = sorted(text.splitlines())
class Item(object):
'''should-be-immutable wrapper class for VCALENDAR (VEVENT, VTODO) and
VCARD'''
def __init__(self, raw):
assert isinstance(raw, text_type)
self._raw = raw
@cached_property
def raw(self):
'''Raw content of the item, which vdirsyncer doesn't validate in any
way.'''
return self._raw
@cached_property
def uid(self):
'''Global identifier of the item, across storages, doesn't change after
a modification of the item.'''
stack = [self.parsed]
while stack:
component = stack.pop()
if component is None:
continue
uid = component.get('UID', None)
if uid:
return uid
stack.extend(component.subcomponents)
for line in self.raw.splitlines():
if line.startswith(u'UID:'):
uid = line[4:].strip()
if uid:
return uid
@cached_property
def hash(self):
'''Hash of self.raw, used for etags.'''
return hash_item(self.raw)
@cached_property
def ident(self):
'''Used for generating hrefs and matching up items during
synchronization. This is either the UID or the hash of the item's
content.'''
return self.uid or self.hash
@cached_property
def parsed(self):
try:
return icalendar.cal.Component.from_ical(self.raw)
except Exception:
return None
def normalize_item(item, ignore_props=IGNORE_PROPS, use_icalendar=True):
if not isinstance(item, Item):
item = Item(item)
if use_icalendar and item.parsed is not None:
# We have to explicitly check "is not None" here because VCALENDARS
# with only subcomponents and no own properties are also false-ish.
lines = to_unicode_lines(item.parsed)
else:
lines = sorted(item.raw.splitlines())
return u'\r\n'.join(line.strip()
for line in lines