From 8d5fed48bc009647e772b000362cc2ec8b6478d0 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sat, 7 Mar 2015 18:24:27 +0100 Subject: [PATCH] Remove icalendar --- .travis.yml | 2 +- CHANGELOG.rst | 1 + setup.py | 1 - tests/utils/test_vobject.py | 58 ++---------------- vdirsyncer/cli/tasks.py | 4 +- vdirsyncer/utils/vobject.py | 113 ++++++++++++++++++++++-------------- 6 files changed, 80 insertions(+), 99 deletions(-) diff --git a/.travis.yml b/.travis.yml index c9b1c88..e3273fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ env: # Radicale with filesystem storage (default) - BUILD=test DAV_SERVER=radicale RADICALE_BACKEND=filesystem REQUIREMENTS=release - PKGS='icalendar==3.6 lxml==3.0 requests==2.4.1 requests_toolbelt==0.3.0 click==3.1' + PKGS='lxml==3.0 requests==2.4.1 requests_toolbelt==0.3.0 click==3.1' # Minimal requirements #- BUILD=test DAV_SERVER=radicale RADICALE_BACKEND=filesystem REQUIREMENTS=devel diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0be08f0..3991581 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,7 @@ Version 0.4.4 - Support for client certificates via the new ``auth_cert`` parameter, see :gh:`182` and :ghpr:`183`. +- The ``icalendar`` package is now not a dependency anymore. Version 0.4.3 ============= diff --git a/setup.py b/setup.py index 3c9fef3..8ce0882 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,6 @@ setup( 'click>=3.1', 'requests', 'lxml>=3.0', - 'icalendar>=3.6', # https://github.com/sigmavirus24/requests-toolbelt/pull/28 'requests_toolbelt>=0.3.0', 'atomicwrites' diff --git a/tests/utils/test_vobject.py b/tests/utils/test_vobject.py index 9388fcc..a9e493c 100644 --- a/tests/utils/test_vobject.py +++ b/tests/utils/test_vobject.py @@ -2,8 +2,6 @@ from textwrap import dedent -import icalendar - import pytest import vdirsyncer.utils.vobject as vobject @@ -30,9 +28,8 @@ def test_split_collection_simple(benchmark): assert [normalize_item(item) for item in given] == \ [normalize_item(item) for item in _simple_split] - if vobject.ICALENDAR_ORIGINAL_ORDER_SUPPORT: - assert [x.splitlines() for x in given] == \ - [x.splitlines() for x in _simple_split] + assert [x.splitlines() for x in given] == \ + [x.splitlines() for x in _simple_split] def test_split_collection_multiple_wrappers(benchmark): @@ -47,9 +44,8 @@ def test_split_collection_multiple_wrappers(benchmark): assert [normalize_item(item) for item in given] == \ [normalize_item(item) for item in _simple_split] - if vobject.ICALENDAR_ORIGINAL_ORDER_SUPPORT: - assert [x.splitlines() for x in given] == \ - [x.splitlines() for x in _simple_split] + assert [x.splitlines() for x in given] == \ + [x.splitlines() for x in _simple_split] def test_split_collection_different_wrappers(): @@ -70,8 +66,7 @@ def test_split_collection_different_wrappers(): def test_join_collection_simple(benchmark): given = benchmark(lambda: vobject.join_collection(_simple_split)) assert normalize_item(given) == normalize_item(_simple_joined) - if vobject.ICALENDAR_ORIGINAL_ORDER_SUPPORT: - assert given.splitlines() == _simple_joined.splitlines() + assert given.splitlines() == _simple_joined.splitlines() def test_join_collection_vevents(benchmark): @@ -201,46 +196,3 @@ def test_multiline_uid_complex(): assert vobject.Item(a).uid == (u'040000008200E00074C5B7101A82E008000000005' u'0AAABEEF50DCF001000000062548482FA830A46B9' u'EA62114AC9F0EF') - - -@pytest.mark.xfail(icalendar.parser.NAME.findall('FOO.BAR') != ['FOO.BAR'], - reason=('version of icalendar doesn\'t support dots in ' - 'property names')) -def test_vcard_property_groups(): - vcard = dedent(u''' - BEGIN:VCARD - VERSION:3.0 - MYLABEL123.ADR:;;This is the Address 08; Some City;;12345;Germany - MYLABEL123.X-ABLABEL: - FN:Some Name - N:Name;Some;;;Nickname - UID:67c15e43-34d2-4f55-a6c6-4adb7aa7e3b2 - END:VCARD - ''').strip() - - book = u'BEGIN:VADDRESSBOOK\n' + vcard + u'\nEND:VADDRESSBOOK' - splitted = list(vobject.split_collection(book)) - assert len(splitted) == 1 - - assert vobject.Item(vcard).hash == vobject.Item(splitted[0]).hash - assert 'is the Address' in vobject.Item(vcard).parsed['MYLABEL123.ADR'] - - -def test_vcard_semicolons_in_values(): - # If this test fails because proper vCard support was added to icalendar, - # we can remove some ugly postprocessing code in to_unicode_lines. - - vcard = dedent(u''' - BEGIN:VCARD - VERSION:3.0 - ADR:;;Address 08;City;;12345;Germany - END:VCARD - ''').strip() - - # Assert that icalendar breaks vcard properties with semicolons in values - assert b'ADR:\\;\\;Address 08\\;City\\;\\;12345\\;Germany' in \ - vobject.Item(vcard).parsed.to_ical().splitlines() - - # Assert that vdirsyncer fixes these properties - assert u'ADR:;;Address 08;City;;12345;Germany' in \ - list(vobject.to_unicode_lines(vobject.Item(vcard).parsed)) diff --git a/vdirsyncer/cli/tasks.py b/vdirsyncer/cli/tasks.py index 8ac7189..766ef5c 100644 --- a/vdirsyncer/cli/tasks.py +++ b/vdirsyncer/cli/tasks.py @@ -9,7 +9,7 @@ from .utils import CliError, JobFailed, cli_logger, collections_for_pair, \ storage_class_from_config, storage_instance_from_config from ..sync import sync -from ..utils.vobject import Item, to_unicode_lines +from ..utils.vobject import Item def sync_pair(wq, pair_name, collections_to_sync, general, all_pairs, @@ -145,7 +145,7 @@ def _repair_collection(storage): else: stack.extend(component.subcomponents) - new_item = Item(u'\n'.join(to_unicode_lines(parsed))) + new_item = Item(u'\r\n'.join(parsed.dump_lines())) assert new_item.uid seen_uids.add(new_item.uid) if changed: diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index 097c4df..78756f5 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -3,10 +3,6 @@ import hashlib from itertools import chain, tee -import icalendar.cal -import icalendar.caselessdict -import icalendar.parser - from . import cached_property, split_sequence, uniq from .compat import text_type @@ -34,20 +30,6 @@ IGNORE_PROPS = _process_properties( del _process_properties -# Whether the installed icalendar version has -# https://github.com/collective/icalendar/pull/136 -# (support for keeping the order of properties and parameters) -# -# This basically checks whether the superclass of all icalendar classes has a -# method from OrderedDict. -try: - reversed(icalendar.caselessdict.CaselessDict()) -except TypeError: - ICALENDAR_ORIGINAL_ORDER_SUPPORT = False -else: - ICALENDAR_ORIGINAL_ORDER_SUPPORT = True - - class Item(object): '''Immutable wrapper class for VCALENDAR (VEVENT, VTODO) and @@ -107,7 +89,7 @@ class Item(object): @cached_property def parsed(self): try: - return icalendar.cal.Component.from_ical(self.raw) + return _Component.parse(self.raw) except Exception: return None @@ -130,7 +112,7 @@ def split_collection(text, inline=(u'VTIMEZONE',), wrap_items_with=(u'VCALENDAR',)): '''Emits items in the order they occur in the text.''' assert isinstance(text, text_type) - collections = icalendar.cal.Component.from_ical(text, multiple=True) + collections = _Component.parse(text, multiple=True) collection_name = None for collection in collections: @@ -150,32 +132,14 @@ def split_collection(text, inline=(u'VTIMEZONE',), collection.subcomponents, lambda item: item.name in inline ) - inlined_lines = list(chain(*(to_unicode_lines(inlined_item) + inlined_lines = list(chain(*(inlined_item.dump_lines() for inlined_item in inlined_items))) for item in normal_items: - lines = chain(start, inlined_lines, to_unicode_lines(item), end) + lines = chain(start, inlined_lines, item.dump_lines(), end) yield u''.join(line + u'\r\n' for line in lines if line) -def to_unicode_lines(item): - '''icalendar doesn't provide an efficient way of getting the ical data as - unicode. So let's do it ourselves.''' - - if ICALENDAR_ORIGINAL_ORDER_SUPPORT: - content_lines = item.content_lines(sorted=False) - else: - content_lines = item.content_lines() - - for content_line in content_lines: - if content_line: - # https://github.com/untitaker/vdirsyncer/issues/70 - # XXX: icalendar escapes semicolons which are not supposed to get - # escaped, because it is not aware of vcard - content_line = content_line.replace(u'\\;', u';') - yield icalendar.parser.foldline(content_line) - - _default_join_wrappers = { u'VCALENDAR': u'VCALENDAR', u'VEVENT': u'VCALENDAR', @@ -191,7 +155,7 @@ def join_collection(items, wrappers=_default_join_wrappers): } ''' - items1, items2 = tee((icalendar.cal.Component.from_ical(x) + items1, items2 = tee((_Component.parse(x) for x in items), 2) item_type, wrapper_type = _get_item_type(items1, wrappers) @@ -199,7 +163,7 @@ def join_collection(items, wrappers=_default_join_wrappers): return x.name == wrapper_type and x.subcomponents or [x] components = chain(*(_get_item_components(x) for x in items2)) - lines = chain(*uniq(tuple(to_unicode_lines(x)) for x in components)) + lines = chain(*uniq(tuple(x.dump_lines()) for x in components)) if wrapper_type is not None: start = [u'BEGIN:{}'.format(wrapper_type)] @@ -218,3 +182,68 @@ def _get_item_type(components, wrappers): else: return item_type, wrapper_type return None, None + + +class _Component(object): + ''' + Raw outline of the components. + + Barely parsing ``BEGIN`` and ``END`` lines, but not any other properties. + This gives us better performance and more tolerance towards slightly broken + items. + + Original version from https://github.com/collective/icalendar/, but apart + from the similar API, very few parts have been reused. + ''' + + def __init__(self, name, lines, subcomponents): + ''' + :param name: The component name. + :param lines: The component's own properties, as list of lines + (strings). + :param subcomponents: List of components. + ''' + self.name = name + self.lines = lines + self.subcomponents = subcomponents + + @classmethod + def parse(cls, lines, multiple=False): + if isinstance(lines, bytes): + lines = lines.decode('utf-8') + if isinstance(lines, text_type): + lines = lines.splitlines() + + stack = [] + rv = [] + for line in lines: + if line.startswith(u'BEGIN:'): + c_name = line[len(u'BEGIN:'):].strip().upper() + stack.append(cls(c_name, [], [])) + elif line.startswith(u'END:'): + component = stack.pop() + if stack: + stack[-1].subcomponents.append(component) + else: + rv.append(component) + else: + line = line.strip() + if line: + stack[-1].lines.append(line) + + if multiple: + return rv + elif len(rv) != 1: + raise ValueError('Found {} components, expected one.' + .format(len(rv))) + else: + return rv[0] + + def dump_lines(self): + yield u'BEGIN:{}'.format(self.name) + for line in self.lines: + yield line + for c in self.subcomponents: + for line in c.dump_lines(): + yield line + yield u'END:{}'.format(self.name)