Remove icalendar

This commit is contained in:
Markus Unterwaditzer 2015-03-07 18:24:27 +01:00
parent 42662a97c3
commit 8d5fed48bc
6 changed files with 80 additions and 99 deletions

View file

@ -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

View file

@ -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
=============

View file

@ -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'

View file

@ -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))

View file

@ -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:

View file

@ -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)