diff --git a/.travis.yml b/.travis.yml index 666437d..3231fc3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: python -python: "2.7" +python: + - "2.7" + - "3.3" env: global: - IS_TRAVIS=true diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 74e7b4d..2d10f7c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -3,8 +3,8 @@ * Make sure you have the latest version by executing ``pip install --user --upgrade vdirsyncer``. - * Include your configuration, the commands you're executing, and their - output. + * Include the Python version, your configuration, the commands you're + executing, and their output. * Use ``--verbosity=DEBUG`` when including output from vdirsyncer. diff --git a/README.rst b/README.rst index 1548bbf..48e8898 100644 --- a/README.rst +++ b/README.rst @@ -34,9 +34,11 @@ informations on problems with ownCloud. How to use ========== +vdirsyncer requires Python >= 2.7 or Python >= 3.3. + As all Python packages, vdirsyncer can be installed with ``pip``:: - pip install --user vdirsyncer # use the pip for Python 2 + pip install --user vdirsyncer Then copy ``example.cfg`` to ``~/.vdirsyncer/config`` and edit it. You can use the `VDIRSYNCER_CONFIG` environment variable to change the path vdirsyncer will diff --git a/tests/storage/__init__.py b/tests/storage/__init__.py index a774529..b19ba85 100644 --- a/tests/storage/__init__.py +++ b/tests/storage/__init__.py @@ -9,6 +9,7 @@ from vdirsyncer.storage.base import Item import vdirsyncer.exceptions as exceptions +from vdirsyncer.utils import text_type from .. import assert_item_equals import random import pytest @@ -33,7 +34,7 @@ class StorageTests(object): def test_generic(self): items = map(self._create_bogus_item, range(1, 10)) for i, item in enumerate(items): - assert item.uid == unicode(i + 1), item.raw + assert item.uid == text_type(i + 1), item.raw s = self._get_storage() hrefs = [] for item in items: diff --git a/tests/storage/dav/servers/radicale/__init__.py b/tests/storage/dav/servers/radicale/__init__.py index 1faf706..3b1c33f 100644 --- a/tests/storage/dav/servers/radicale/__init__.py +++ b/tests/storage/dav/servers/radicale/__init__.py @@ -2,8 +2,8 @@ import sys import os -import urlparse import pytest +from vdirsyncer.utils import urlparse from werkzeug.test import Client from werkzeug.wrappers import BaseResponse as WerkzeugResponse diff --git a/tests/storage/test_filesystem.py b/tests/storage/test_filesystem.py index b2faf5f..401d391 100644 --- a/tests/storage/test_filesystem.py +++ b/tests/storage/test_filesystem.py @@ -46,8 +46,8 @@ class TestFilesystemStorage(StorageTests): s = self.storage_class(str(tmpdir), '.txt') class BrokenItem(object): - raw = b'Ц, Ш, Л, ж, Д, З, Ю' + raw = u'Ц, Ш, Л, ж, Д, З, Ю'.encode('utf-8') uid = 'jeezus' - with pytest.raises(UnicodeError): + with pytest.raises(TypeError): s.upload(BrokenItem) assert not tmpdir.listdir() diff --git a/tests/storage/test_http.py b/tests/storage/test_http.py index d874503..95c806f 100644 --- a/tests/storage/test_http.py +++ b/tests/storage/test_http.py @@ -18,29 +18,25 @@ class TestHttpStorage(object): collection_url = 'http://127.0.0.1/calendar/collection.ics' items = [ - dedent(b''' - BEGIN:VEVENT - SUMMARY:Eine Kurzinfo - DESCRIPTION:Beschreibung des Termines - END:VEVENT - ''').strip(), - dedent(b''' - BEGIN:VEVENT - SUMMARY:Eine zweite Kurzinfo - DESCRIPTION:Beschreibung des anderen Termines - BEGIN:VALARM - ACTION:AUDIO - TRIGGER:19980403T120000 - ATTACH;FMTTYPE=audio/basic:http://host.com/pub/ssbanner.aud - REPEAT:4 - DURATION:PT1H - END:VALARM - END:VEVENT - ''').strip() + (u'BEGIN:VEVENT\n' + u'SUMMARY:Eine Kurzinfo\n' + u'DESCRIPTION:Beschreibung des Termines\n' + u'END:VEVENT'), + (u'BEGIN:VEVENT\n' + u'SUMMARY:Eine zweite Kurzinfo\n' + u'DESCRIPTION:Beschreibung des anderen Termines\n' + u'BEGIN:VALARM\n' + u'ACTION:AUDIO\n' + u'TRIGGER:19980403T120000\n' + u'ATTACH;FMTTYPE=audio/basic:http://host.com/pub/ssbanner.aud\n' + u'REPEAT:4\n' + u'DURATION:PT1H\n' + u'END:VALARM\n' + u'END:VEVENT') ] responses = [ - '\n'.join([b'BEGIN:VCALENDAR'] + items + [b'END:VCALENDAR']) + u'\n'.join([u'BEGIN:VCALENDAR'] + items + [u'END:VCALENDAR']) ] * 2 def get(method, url, *a, **kw): @@ -49,7 +45,8 @@ class TestHttpStorage(object): r = Response() r.status_code = 200 assert responses - r._content = responses.pop() + r._content = responses.pop().encode('utf-8') + r.encoding = 'utf-8' return r monkeypatch.setattr('requests.request', get) diff --git a/vdirsyncer/cli.py b/vdirsyncer/cli.py index 48748bd..29b61b5 100644 --- a/vdirsyncer/cli.py +++ b/vdirsyncer/cli.py @@ -10,7 +10,6 @@ import os import sys import json -import ConfigParser from vdirsyncer.sync import sync from vdirsyncer.utils import expand_path, split_dict, parse_options from vdirsyncer.storage import storage_names @@ -18,11 +17,17 @@ import vdirsyncer.log as log import argvard +try: + from ConfigParser import RawConfigParser +except ImportError: + from configparser import RawConfigParser + + cli_logger = log.get('cli') def load_config(fname, pair_options=('collections', 'conflict_resolution')): - c = ConfigParser.RawConfigParser() + c = RawConfigParser() c.read(fname) get_options = lambda s: dict(parse_options(c.items(s))) diff --git a/vdirsyncer/storage/base.py b/vdirsyncer/storage/base.py index 32701ab..028a3e4 100644 --- a/vdirsyncer/storage/base.py +++ b/vdirsyncer/storage/base.py @@ -8,6 +8,7 @@ ''' from .. import exceptions +from .. import utils class Item(object): @@ -15,7 +16,7 @@ class Item(object): '''should-be-immutable wrapper class for VCALENDAR and VCARD''' def __init__(self, raw): - assert type(raw) is unicode + assert isinstance(raw, utils.text_type) raw = raw.splitlines() self.uid = None diff --git a/vdirsyncer/storage/dav.py b/vdirsyncer/storage/dav.py index 4e1c9da..e00b61e 100644 --- a/vdirsyncer/storage/dav.py +++ b/vdirsyncer/storage/dav.py @@ -11,10 +11,9 @@ from .base import Storage, Item from .http import prepare_auth, prepare_verify, USERAGENT from .. import exceptions from .. import log -from ..utils import request, get_password +from ..utils import request, get_password, urlparse import requests import datetime -import urlparse from lxml import etree @@ -166,7 +165,7 @@ class DavStorage(Storage): hrefs_left = set(hrefs) for element in root.iter('{DAV:}response'): href = self._normalize_href( - element.find('{DAV:}href').text.decode(response.encoding)) + element.find('{DAV:}href').text) raw = element \ .find('{DAV:}propstat') \ .find('{DAV:}prop') \ diff --git a/vdirsyncer/storage/filesystem.py b/vdirsyncer/storage/filesystem.py index b98efce..066f32b 100644 --- a/vdirsyncer/storage/filesystem.py +++ b/vdirsyncer/storage/filesystem.py @@ -10,7 +10,7 @@ import os from vdirsyncer.storage.base import Storage, Item import vdirsyncer.exceptions as exceptions -from vdirsyncer.utils import expand_path +from vdirsyncer.utils import expand_path, text_type import vdirsyncer.log as log logger = log.get('storage.filesystem') @@ -76,7 +76,7 @@ class FilesystemStorage(Storage): if os.path.exists(path): raise IOError('{} is not a directory.') if create: - os.makedirs(path, 0750) + os.makedirs(path, 0o750) else: raise IOError('Directory {} does not exist. Use create = ' 'True in your configuration to automatically ' @@ -125,6 +125,10 @@ class FilesystemStorage(Storage): fpath = self._get_filepath(href) if os.path.exists(fpath): raise exceptions.AlreadyExistingError(item.uid) + + if not isinstance(item.raw, text_type): + raise TypeError('item.raw must be a unicode string.') + with safe_write(fpath, 'wb+') as f: f.write(item.raw.encode(self.encoding)) return href, f.get_etag() @@ -140,6 +144,9 @@ class FilesystemStorage(Storage): if etag != actual_etag: raise exceptions.WrongEtagError(etag, actual_etag) + if not isinstance(item.raw, text_type): + raise TypeError('item.raw must be a unicode string.') + with safe_write(fpath, 'wb') as f: f.write(item.raw.encode(self.encoding)) return f.get_etag() diff --git a/vdirsyncer/storage/http.py b/vdirsyncer/storage/http.py index 847e6f3..edb158f 100644 --- a/vdirsyncer/storage/http.py +++ b/vdirsyncer/storage/http.py @@ -1,4 +1,3 @@ - # -*- coding: utf-8 -*- ''' vdirsyncer.storage.http @@ -8,10 +7,10 @@ :license: MIT, see LICENSE for more details. ''' -import urlparse import hashlib from .base import Storage, Item -from vdirsyncer.utils import expand_path, get_password, request +from vdirsyncer.utils import expand_path, get_password, request, urlparse, \ + text_type USERAGENT = 'vdirsyncer' @@ -59,7 +58,7 @@ def prepare_auth(auth, username, password): def prepare_verify(verify): - if isinstance(verify, (str, unicode)): + if isinstance(verify, (text_type, bytes)): return expand_path(verify) return verify @@ -110,8 +109,8 @@ class HttpStorage(Storage): self._items[uid] = item for uid, item in self._items.items(): - yield uid, hashlib.sha256(item.raw).hexdigest() + yield uid, hashlib.sha256(item.raw.encode('utf-8')).hexdigest() def get(self, href): x = self._items[href] - return x, hashlib.sha256(x.raw).hexdigest() + return x, hashlib.sha256(x.raw.encode('utf-8')).hexdigest() diff --git a/vdirsyncer/sync.py b/vdirsyncer/sync.py index 96a0fe9..ae836fe 100644 --- a/vdirsyncer/sync.py +++ b/vdirsyncer/sync.py @@ -19,6 +19,7 @@ import itertools import vdirsyncer.exceptions as exceptions import vdirsyncer.log +from .utils import iteritems, itervalues sync_logger = vdirsyncer.log.get('sync') @@ -58,18 +59,18 @@ def sync(storage_a, storage_b, status, conflict_resolution=None): ''' a_href_to_status = dict( (href_a, (uid, etag_a)) - for uid, (href_a, etag_a, href_b, etag_b) in status.iteritems() + for uid, (href_a, etag_a, href_b, etag_b) in iteritems(status) ) b_href_to_status = dict( (href_b, (uid, etag_b)) - for uid, (href_a, etag_a, href_b, etag_b) in status.iteritems() + for uid, (href_a, etag_a, href_b, etag_b) in iteritems(status) ) # href => {'etag': etag, 'item': optional item, 'uid': uid} list_a = dict(prepare_list(storage_a, a_href_to_status)) list_b = dict(prepare_list(storage_b, b_href_to_status)) - a_uid_to_href = dict((x['uid'], href) for href, x in list_a.iteritems()) - b_uid_to_href = dict((x['uid'], href) for href, x in list_b.iteritems()) + a_uid_to_href = dict((x['uid'], href) for href, x in iteritems(list_a)) + b_uid_to_href = dict((x['uid'], href) for href, x in iteritems(list_b)) del a_href_to_status, b_href_to_status storages = { @@ -175,8 +176,8 @@ def get_actions(storages, status): storage_a, list_a, a_uid_to_href = storages['a'] storage_b, list_b, b_uid_to_href = storages['b'] - uids_a = (x['uid'] for x in list_a.itervalues()) - uids_b = (x['uid'] for x in list_b.itervalues()) + uids_a = (x['uid'] for x in itervalues(list_a)) + uids_b = (x['uid'] for x in itervalues(list_b)) handled = set() for uid in itertools.chain(uids_a, uids_b, status): if uid in handled: diff --git a/vdirsyncer/utils.py b/vdirsyncer/utils.py index 4aafd4a..7365135 100644 --- a/vdirsyncer/utils.py +++ b/vdirsyncer/utils.py @@ -8,13 +8,24 @@ ''' import os +import sys import vdirsyncer.log +import requests -try: # pragma: no cover - import urllib.parse as urlparse -except ImportError: # pragma: no cover +PY2 = sys.version_info[0] == 2 + + +if PY2: import urlparse + text_type = unicode + iteritems = lambda x: x.iteritems() + itervalues = lambda x: x.itervalues() +else: + import urllib.parse as urlparse + text_type = str + iteritems = lambda x: x.items() + itervalues = lambda x: x.values() try: @@ -23,9 +34,6 @@ except ImportError: keyring = None -import requests - - password_key_prefix = 'vdirsyncer:'