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
python: "2.7"
python:
- "2.7"
- "3.3"
env:
global:
- IS_TRAVIS=true

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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