Be Python 3 compatible

Not that anybody actually uses Python 3, but this helps very much with
finding obscure bugs.
This commit is contained in:
Markus Unterwaditzer 2014-04-16 15:28:01 +02:00
parent cef68b5d09
commit e66b43c839
14 changed files with 77 additions and 55 deletions

View file

@ -1,5 +1,7 @@
language: python language: python
python: "2.7" python:
- "2.7"
- "3.3"
env: env:
global: global:
- IS_TRAVIS=true - IS_TRAVIS=true

View file

@ -3,8 +3,8 @@
* Make sure you have the latest version by executing ``pip install --user * Make sure you have the latest version by executing ``pip install --user
--upgrade vdirsyncer``. --upgrade vdirsyncer``.
* Include your configuration, the commands you're executing, and their * Include the Python version, your configuration, the commands you're
output. executing, and their output.
* Use ``--verbosity=DEBUG`` when including output from vdirsyncer. * Use ``--verbosity=DEBUG`` when including output from vdirsyncer.

View file

@ -34,9 +34,11 @@ informations on problems with ownCloud.
How to use How to use
========== ==========
vdirsyncer requires Python >= 2.7 or Python >= 3.3.
As all Python packages, vdirsyncer can be installed with ``pip``:: 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 Then copy ``example.cfg`` to ``~/.vdirsyncer/config`` and edit it. You can use the
`VDIRSYNCER_CONFIG` environment variable to change the path vdirsyncer will `VDIRSYNCER_CONFIG` environment variable to change the path vdirsyncer will

View file

@ -9,6 +9,7 @@
from vdirsyncer.storage.base import Item from vdirsyncer.storage.base import Item
import vdirsyncer.exceptions as exceptions import vdirsyncer.exceptions as exceptions
from vdirsyncer.utils import text_type
from .. import assert_item_equals from .. import assert_item_equals
import random import random
import pytest import pytest
@ -33,7 +34,7 @@ class StorageTests(object):
def test_generic(self): def test_generic(self):
items = map(self._create_bogus_item, range(1, 10)) items = map(self._create_bogus_item, range(1, 10))
for i, item in enumerate(items): 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() s = self._get_storage()
hrefs = [] hrefs = []
for item in items: for item in items:

View file

@ -2,8 +2,8 @@
import sys import sys
import os import os
import urlparse
import pytest import pytest
from vdirsyncer.utils import urlparse
from werkzeug.test import Client from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse as WerkzeugResponse from werkzeug.wrappers import BaseResponse as WerkzeugResponse

View file

@ -46,8 +46,8 @@ class TestFilesystemStorage(StorageTests):
s = self.storage_class(str(tmpdir), '.txt') s = self.storage_class(str(tmpdir), '.txt')
class BrokenItem(object): class BrokenItem(object):
raw = b'Ц, Ш, Л, ж, Д, З, Ю' raw = u'Ц, Ш, Л, ж, Д, З, Ю'.encode('utf-8')
uid = 'jeezus' uid = 'jeezus'
with pytest.raises(UnicodeError): with pytest.raises(TypeError):
s.upload(BrokenItem) s.upload(BrokenItem)
assert not tmpdir.listdir() assert not tmpdir.listdir()

View file

@ -18,29 +18,25 @@ class TestHttpStorage(object):
collection_url = 'http://127.0.0.1/calendar/collection.ics' collection_url = 'http://127.0.0.1/calendar/collection.ics'
items = [ items = [
dedent(b''' (u'BEGIN:VEVENT\n'
BEGIN:VEVENT u'SUMMARY:Eine Kurzinfo\n'
SUMMARY:Eine Kurzinfo u'DESCRIPTION:Beschreibung des Termines\n'
DESCRIPTION:Beschreibung des Termines u'END:VEVENT'),
END:VEVENT (u'BEGIN:VEVENT\n'
''').strip(), u'SUMMARY:Eine zweite Kurzinfo\n'
dedent(b''' u'DESCRIPTION:Beschreibung des anderen Termines\n'
BEGIN:VEVENT u'BEGIN:VALARM\n'
SUMMARY:Eine zweite Kurzinfo u'ACTION:AUDIO\n'
DESCRIPTION:Beschreibung des anderen Termines u'TRIGGER:19980403T120000\n'
BEGIN:VALARM u'ATTACH;FMTTYPE=audio/basic:http://host.com/pub/ssbanner.aud\n'
ACTION:AUDIO u'REPEAT:4\n'
TRIGGER:19980403T120000 u'DURATION:PT1H\n'
ATTACH;FMTTYPE=audio/basic:http://host.com/pub/ssbanner.aud u'END:VALARM\n'
REPEAT:4 u'END:VEVENT')
DURATION:PT1H
END:VALARM
END:VEVENT
''').strip()
] ]
responses = [ responses = [
'\n'.join([b'BEGIN:VCALENDAR'] + items + [b'END:VCALENDAR']) u'\n'.join([u'BEGIN:VCALENDAR'] + items + [u'END:VCALENDAR'])
] * 2 ] * 2
def get(method, url, *a, **kw): def get(method, url, *a, **kw):
@ -49,7 +45,8 @@ class TestHttpStorage(object):
r = Response() r = Response()
r.status_code = 200 r.status_code = 200
assert responses assert responses
r._content = responses.pop() r._content = responses.pop().encode('utf-8')
r.encoding = 'utf-8'
return r return r
monkeypatch.setattr('requests.request', get) monkeypatch.setattr('requests.request', get)

View file

@ -10,7 +10,6 @@
import os import os
import sys import sys
import json import json
import ConfigParser
from vdirsyncer.sync import sync from vdirsyncer.sync import sync
from vdirsyncer.utils import expand_path, split_dict, parse_options from vdirsyncer.utils import expand_path, split_dict, parse_options
from vdirsyncer.storage import storage_names from vdirsyncer.storage import storage_names
@ -18,11 +17,17 @@ import vdirsyncer.log as log
import argvard import argvard
try:
from ConfigParser import RawConfigParser
except ImportError:
from configparser import RawConfigParser
cli_logger = log.get('cli') cli_logger = log.get('cli')
def load_config(fname, pair_options=('collections', 'conflict_resolution')): def load_config(fname, pair_options=('collections', 'conflict_resolution')):
c = ConfigParser.RawConfigParser() c = RawConfigParser()
c.read(fname) c.read(fname)
get_options = lambda s: dict(parse_options(c.items(s))) get_options = lambda s: dict(parse_options(c.items(s)))

View file

@ -8,6 +8,7 @@
''' '''
from .. import exceptions from .. import exceptions
from .. import utils
class Item(object): class Item(object):
@ -15,7 +16,7 @@ class Item(object):
'''should-be-immutable wrapper class for VCALENDAR and VCARD''' '''should-be-immutable wrapper class for VCALENDAR and VCARD'''
def __init__(self, raw): def __init__(self, raw):
assert type(raw) is unicode assert isinstance(raw, utils.text_type)
raw = raw.splitlines() raw = raw.splitlines()
self.uid = None self.uid = None

View file

@ -11,10 +11,9 @@ from .base import Storage, Item
from .http import prepare_auth, prepare_verify, USERAGENT from .http import prepare_auth, prepare_verify, USERAGENT
from .. import exceptions from .. import exceptions
from .. import log from .. import log
from ..utils import request, get_password from ..utils import request, get_password, urlparse
import requests import requests
import datetime import datetime
import urlparse
from lxml import etree from lxml import etree
@ -166,7 +165,7 @@ class DavStorage(Storage):
hrefs_left = set(hrefs) hrefs_left = set(hrefs)
for element in root.iter('{DAV:}response'): for element in root.iter('{DAV:}response'):
href = self._normalize_href( href = self._normalize_href(
element.find('{DAV:}href').text.decode(response.encoding)) element.find('{DAV:}href').text)
raw = element \ raw = element \
.find('{DAV:}propstat') \ .find('{DAV:}propstat') \
.find('{DAV:}prop') \ .find('{DAV:}prop') \

View file

@ -10,7 +10,7 @@
import os import os
from vdirsyncer.storage.base import Storage, Item from vdirsyncer.storage.base import Storage, Item
import vdirsyncer.exceptions as exceptions import vdirsyncer.exceptions as exceptions
from vdirsyncer.utils import expand_path from vdirsyncer.utils import expand_path, text_type
import vdirsyncer.log as log import vdirsyncer.log as log
logger = log.get('storage.filesystem') logger = log.get('storage.filesystem')
@ -76,7 +76,7 @@ class FilesystemStorage(Storage):
if os.path.exists(path): if os.path.exists(path):
raise IOError('{} is not a directory.') raise IOError('{} is not a directory.')
if create: if create:
os.makedirs(path, 0750) os.makedirs(path, 0o750)
else: else:
raise IOError('Directory {} does not exist. Use create = ' raise IOError('Directory {} does not exist. Use create = '
'True in your configuration to automatically ' 'True in your configuration to automatically '
@ -125,6 +125,10 @@ class FilesystemStorage(Storage):
fpath = self._get_filepath(href) fpath = self._get_filepath(href)
if os.path.exists(fpath): if os.path.exists(fpath):
raise exceptions.AlreadyExistingError(item.uid) 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: with safe_write(fpath, 'wb+') as f:
f.write(item.raw.encode(self.encoding)) f.write(item.raw.encode(self.encoding))
return href, f.get_etag() return href, f.get_etag()
@ -140,6 +144,9 @@ class FilesystemStorage(Storage):
if etag != actual_etag: if etag != actual_etag:
raise exceptions.WrongEtagError(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: with safe_write(fpath, 'wb') as f:
f.write(item.raw.encode(self.encoding)) f.write(item.raw.encode(self.encoding))
return f.get_etag() return f.get_etag()

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
vdirsyncer.storage.http vdirsyncer.storage.http
@ -8,10 +7,10 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
''' '''
import urlparse
import hashlib import hashlib
from .base import Storage, Item 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' USERAGENT = 'vdirsyncer'
@ -59,7 +58,7 @@ def prepare_auth(auth, username, password):
def prepare_verify(verify): def prepare_verify(verify):
if isinstance(verify, (str, unicode)): if isinstance(verify, (text_type, bytes)):
return expand_path(verify) return expand_path(verify)
return verify return verify
@ -110,8 +109,8 @@ class HttpStorage(Storage):
self._items[uid] = item self._items[uid] = item
for uid, item in self._items.items(): 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): def get(self, href):
x = self._items[href] x = self._items[href]
return x, hashlib.sha256(x.raw).hexdigest() return x, hashlib.sha256(x.raw.encode('utf-8')).hexdigest()

View file

@ -19,6 +19,7 @@ import itertools
import vdirsyncer.exceptions as exceptions import vdirsyncer.exceptions as exceptions
import vdirsyncer.log import vdirsyncer.log
from .utils import iteritems, itervalues
sync_logger = vdirsyncer.log.get('sync') 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( a_href_to_status = dict(
(href_a, (uid, etag_a)) (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( b_href_to_status = dict(
(href_b, (uid, etag_b)) (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} # href => {'etag': etag, 'item': optional item, 'uid': uid}
list_a = dict(prepare_list(storage_a, a_href_to_status)) list_a = dict(prepare_list(storage_a, a_href_to_status))
list_b = dict(prepare_list(storage_b, b_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()) 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 list_b.iteritems()) b_uid_to_href = dict((x['uid'], href) for href, x in iteritems(list_b))
del a_href_to_status, b_href_to_status del a_href_to_status, b_href_to_status
storages = { storages = {
@ -175,8 +176,8 @@ def get_actions(storages, status):
storage_a, list_a, a_uid_to_href = storages['a'] storage_a, list_a, a_uid_to_href = storages['a']
storage_b, list_b, b_uid_to_href = storages['b'] storage_b, list_b, b_uid_to_href = storages['b']
uids_a = (x['uid'] for x in list_a.itervalues()) uids_a = (x['uid'] for x in itervalues(list_a))
uids_b = (x['uid'] for x in list_b.itervalues()) uids_b = (x['uid'] for x in itervalues(list_b))
handled = set() handled = set()
for uid in itertools.chain(uids_a, uids_b, status): for uid in itertools.chain(uids_a, uids_b, status):
if uid in handled: if uid in handled:

View file

@ -8,13 +8,24 @@
''' '''
import os import os
import sys
import vdirsyncer.log import vdirsyncer.log
import requests
try: # pragma: no cover PY2 = sys.version_info[0] == 2
import urllib.parse as urlparse
except ImportError: # pragma: no cover
if PY2:
import urlparse 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: try:
@ -23,9 +34,6 @@ except ImportError:
keyring = None keyring = None
import requests
password_key_prefix = 'vdirsyncer:' password_key_prefix = 'vdirsyncer:'