From 41848b24326d85d18b6e03d38e8406f7ce75c30b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 11 Jun 2014 18:09:01 +0200 Subject: [PATCH 1/8] Move Item to utils --- vdirsyncer/storage/base.py | 34 +--------------------------------- vdirsyncer/utils/vobject.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/vdirsyncer/storage/base.py b/vdirsyncer/storage/base.py index e85b8f2..1aafdf7 100644 --- a/vdirsyncer/storage/base.py +++ b/vdirsyncer/storage/base.py @@ -10,41 +10,9 @@ 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 +Item = utils.vobject.Item class Storage(object): diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index 8ffd4b2..13cea8d 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -47,6 +47,40 @@ ICALENDAR_ORIGINAL_ORDER_SUPPORT = \ hasattr(icalendar.caselessdict.CaselessDict, '__reversed__') +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 = hash_item(raw) + self.ident = self.uid or self.hash + + def normalize_item(text, ignore_props=IGNORE_PROPS, use_icalendar=True): try: if not use_icalendar: From 5b9758e669ddf8ade54824d4819f70fa4ac6915c Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 11 Jun 2014 18:12:40 +0200 Subject: [PATCH 2/8] Use cached properties in Item --- vdirsyncer/utils/vobject.py | 70 ++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index 13cea8d..d7ac0b4 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -46,39 +46,69 @@ IGNORE_PROPS = _process_properties( ICALENDAR_ORIGINAL_ORDER_SUPPORT = \ hasattr(icalendar.caselessdict.CaselessDict, '__reversed__') +_missing = object() + + +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 + 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(): + 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.''' + + for line in self.raw.splitlines(): if line.startswith(u'UID:'): uid = line[4:].strip() if uid: - self.uid = uid + return uid - self.raw = raw - self.hash = hash_item(raw) - self.ident = self.uid or self.hash + @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 def normalize_item(text, ignore_props=IGNORE_PROPS, use_icalendar=True): From 764a10ca0ae87041d1e42195ed03875b8641c6e7 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 11 Jun 2014 18:20:34 +0200 Subject: [PATCH 3/8] Create Item.parsed property --- vdirsyncer/utils/vobject.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index d7ac0b4..0d1ab47 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -110,14 +110,23 @@ class Item(object): 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(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()) + +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 From cec742b9e3a678391279d1d6ac1f672a671bfd8b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 11 Jun 2014 18:28:51 +0200 Subject: [PATCH 4/8] Correctly handle multiline UIDs See #74 --- tests/utils/test_vobject.py | 8 ++++++++ vdirsyncer/utils/vobject.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/tests/utils/test_vobject.py b/tests/utils/test_vobject.py index 9ece4ab..70f0911 100644 --- a/tests/utils/test_vobject.py +++ b/tests/utils/test_vobject.py @@ -85,3 +85,11 @@ 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' diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index 0d1ab47..6d9bdb5 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -91,6 +91,15 @@ class Item(object): 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 not component: + 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:'): From 25d30991c6133b27b5589c7b0c60fd429f15e91b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 11 Jun 2014 18:32:59 +0200 Subject: [PATCH 5/8] Move cached_property to general utils. --- vdirsyncer/utils/__init__.py | 23 +++++++++++++++++++++++ vdirsyncer/utils/vobject.py | 25 +------------------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/vdirsyncer/utils/__init__.py b/vdirsyncer/utils/__init__.py index b8197e5..bc5363c 100644 --- a/vdirsyncer/utils/__init__.py +++ b/vdirsyncer/utils/__init__.py @@ -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 diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index 6d9bdb5..0de2394 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -12,6 +12,7 @@ import icalendar.cal import icalendar.parser import icalendar.caselessdict +from . import cached_property from .compat import text_type, itervalues @@ -46,30 +47,6 @@ IGNORE_PROPS = _process_properties( ICALENDAR_ORIGINAL_ORDER_SUPPORT = \ hasattr(icalendar.caselessdict.CaselessDict, '__reversed__') -_missing = object() - - -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 - class Item(object): From 13ecb42e60a0791c4e1221eb5ee569fda91c9bc2 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 11 Jun 2014 18:45:17 +0200 Subject: [PATCH 6/8] Fix import --- vdirsyncer/storage/base.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/vdirsyncer/storage/base.py b/vdirsyncer/storage/base.py index 1aafdf7..653a4d4 100644 --- a/vdirsyncer/storage/base.py +++ b/vdirsyncer/storage/base.py @@ -9,10 +9,7 @@ from .. import exceptions -from .. import utils - - -Item = utils.vobject.Item +from vdirsyncer.utils.vobject import Item # noqa class Storage(object): From e25e4dc0cbb306a03e55cf82bfa70feecb8edb1b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 12 Jun 2014 12:51:20 +0200 Subject: [PATCH 7/8] Fix bug --- tests/utils/test_vobject.py | 42 +++++++++++++++++++++++++++++++++++++ vdirsyncer/utils/vobject.py | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/utils/test_vobject.py b/tests/utils/test_vobject.py index 70f0911..5d347c2 100644 --- a/tests/utils/test_vobject.py +++ b/tests/utils/test_vobject.py @@ -93,3 +93,45 @@ def test_multiline_uid(): 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') diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index 0de2394..f1bca9a 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -71,7 +71,7 @@ class Item(object): stack = [self.parsed] while stack: component = stack.pop() - if not component: + if component is None: continue uid = component.get('UID', None) if uid: From 67d14cd59ad4b962452df13ed129bdbe1179b809 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 12 Jun 2014 13:24:01 +0200 Subject: [PATCH 8/8] Style fix --- tests/utils/test_vobject.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/utils/test_vobject.py b/tests/utils/test_vobject.py index 5d347c2..7462227 100644 --- a/tests/utils/test_vobject.py +++ b/tests/utils/test_vobject.py @@ -94,6 +94,7 @@ def test_multiline_uid(): u'END:FOO\r\n') assert vobject.Item(a).uid == u'123456789abcdefgh' + def test_multiline_uid_complex(): a = u''' BEGIN:VCALENDAR