Merge branch 'next'

This commit is contained in:
Hugo Osvaldo Barrera 2020-06-08 18:59:37 +02:00
commit 60e2e9669e
73 changed files with 354 additions and 425 deletions

15
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,15 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-toml
- id: check-added-large-files
- id: debug-statements
- repo: https://gitlab.com/pycqa/flake8
rev: "master" # pick a git hash / tag to point to
hooks:
- id: flake8
additional_dependencies: [flake8-import-order, flake8-bugbear]

View file

@ -2,6 +2,7 @@
"branches": { "branches": {
"only": [ "only": [
"auto", "auto",
"next",
"master", "master",
"/^.*-maintenance$/" "/^.*-maintenance$/"
] ]
@ -13,9 +14,6 @@
}, },
"install": [ "install": [
". scripts/travis-install.sh", ". scripts/travis-install.sh",
"pip install -U pip setuptools",
"pip install wheel",
"make -e install-dev",
"make -e install-$BUILD" "make -e install-$BUILD"
], ],
"language": "python", "language": "python",

View file

@ -71,7 +71,7 @@ install-servers:
(cd $(TESTSERVER_BASE)$$server && sh install.sh); \ (cd $(TESTSERVER_BASE)$$server && sh install.sh); \
done done
install-test: install-servers install-test: install-servers install-dev
pip install -Ur test-requirements.txt pip install -Ur test-requirements.txt
set -xe && if [ "$$REQUIREMENTS" = "devel" ]; then \ set -xe && if [ "$$REQUIREMENTS" = "devel" ]; then \
pip install -U --force-reinstall \ pip install -U --force-reinstall \
@ -81,8 +81,8 @@ install-test: install-servers
fi fi
[ -z "$(TEST_EXTRA_PACKAGES)" ] || pip install $(TEST_EXTRA_PACKAGES) [ -z "$(TEST_EXTRA_PACKAGES)" ] || pip install $(TEST_EXTRA_PACKAGES)
install-style: install-docs install-style: install-docs install-dev
pip install -U flake8==3.5.0 flake8-import-order 'flake8-bugbear>=17.3.0' autopep8 pip install -U flake8 flake8-import-order flake8-bugbear autopep8
style: style:
flake8 flake8
@ -114,6 +114,7 @@ release-deb:
sh scripts/release-deb.sh ubuntu zesty sh scripts/release-deb.sh ubuntu zesty
install-dev: install-dev:
pip install -U pip setuptools wheel
pip install -e . pip install -e .
[ "$(ETESYNC_TESTS)" = "false" ] || pip install -Ue .[etesync] [ "$(ETESYNC_TESTS)" = "false" ] || pip install -Ue .[etesync]
set -xe && if [ "$(REQUIREMENTS)" = "devel" ]; then \ set -xe && if [ "$(REQUIREMENTS)" = "devel" ]; then \

View file

@ -2,6 +2,26 @@
vdirsyncer vdirsyncer
========== ==========
.. image:: https://travis-ci.org/pimutils/vdirsyncer.svg?branch=master
:target: https://travis-ci.org/pimutils/vdirsyncer
:alt: CI status
.. image:: https://codecov.io/github/pimutils/vdirsyncer/coverage.svg?branch=master
:target: https://codecov.io/github/pimutils/vdirsyncer?branch=master
:alt: Codecov coverage report
.. image:: https://readthedocs.org/projects/vdirsyncer/badge/
:target: https://vdirsyncer.rtfd.org/
:alt: documentation
.. image:: https://img.shields.io/pypi/v/vdirsyncer.svg
:target: https://pypi.python.org/pypi/vdirsyncer
:alt: version on pypi
.. image:: https://img.shields.io/pypi/l/vdirsyncer.svg
:target: https://github.com/pimutils/vdirsyncer/blob/master/LICENCE
:alt: licence: BSD
- `Documentation <https://vdirsyncer.pimutils.org/en/stable/>`_ - `Documentation <https://vdirsyncer.pimutils.org/en/stable/>`_
- `Source code <https://github.com/pimutils/vdirsyncer>`_ - `Source code <https://github.com/pimutils/vdirsyncer>`_
@ -20,15 +40,6 @@ It aims to be for calendars and contacts what `OfflineIMAP
.. _programs: https://vdirsyncer.pimutils.org/en/latest/tutorials/ .. _programs: https://vdirsyncer.pimutils.org/en/latest/tutorials/
.. image:: https://travis-ci.org/pimutils/vdirsyncer.svg?branch=master
:target: https://travis-ci.org/pimutils/vdirsyncer
.. image:: https://codecov.io/github/pimutils/vdirsyncer/coverage.svg?branch=master
:target: https://codecov.io/github/pimutils/vdirsyncer?branch=master
.. image:: https://badge.waffle.io/pimutils/vdirsyncer.svg?label=ready&title=Ready
:target: https://waffle.io/pimutils/vdirsyncer
Links of interest Links of interest
================= =================

View file

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
import datetime import datetime
import os import os
import setuptools_scm from pkg_resources import get_distribution
extensions = ['sphinx.ext.autodoc'] extensions = ['sphinx.ext.autodoc']
@ -12,11 +10,11 @@ templates_path = ['_templates']
source_suffix = '.rst' source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
project = u'vdirsyncer' project = 'vdirsyncer'
copyright = (u'2014-{}, Markus Unterwaditzer & contributors' copyright = ('2014-{}, Markus Unterwaditzer & contributors'
.format(datetime.date.today().strftime('%Y'))) .format(datetime.date.today().strftime('%Y')))
release = setuptools_scm.get_version(root='..', relative_to=__file__) release = get_distribution('vdirsyncer').version
version = '.'.join(release.split('.')[:2]) # The short X.Y version. version = '.'.join(release.split('.')[:2]) # The short X.Y version.
rst_epilog = '.. |vdirsyncer_version| replace:: %s' % release rst_epilog = '.. |vdirsyncer_version| replace:: %s' % release
@ -44,18 +42,18 @@ htmlhelp_basename = 'vdirsyncerdoc'
latex_elements = {} latex_elements = {}
latex_documents = [ latex_documents = [
('index', 'vdirsyncer.tex', u'vdirsyncer Documentation', ('index', 'vdirsyncer.tex', 'vdirsyncer Documentation',
u'Markus Unterwaditzer', 'manual'), 'Markus Unterwaditzer', 'manual'),
] ]
man_pages = [ man_pages = [
('index', 'vdirsyncer', u'vdirsyncer Documentation', ('index', 'vdirsyncer', 'vdirsyncer Documentation',
[u'Markus Unterwaditzer'], 1) ['Markus Unterwaditzer'], 1)
] ]
texinfo_documents = [ texinfo_documents = [
('index', 'vdirsyncer', u'vdirsyncer Documentation', ('index', 'vdirsyncer', 'vdirsyncer Documentation',
u'Markus Unterwaditzer', 'vdirsyncer', 'Markus Unterwaditzer', 'vdirsyncer',
'Synchronize calendars and contacts.', 'Miscellaneous'), 'Synchronize calendars and contacts.', 'Miscellaneous'),
] ]

View file

@ -7,4 +7,4 @@ Vdirsyncer is continuously tested against the latest version of Baikal_.
- Baikal up to ``0.2.7`` also uses an old version of SabreDAV, with the same - Baikal up to ``0.2.7`` also uses an old version of SabreDAV, with the same
issue as ownCloud, see :gh:`160`. This issue is fixed in later versions. issue as ownCloud, see :gh:`160`. This issue is fixed in later versions.
.. _Baikal: http://baikal-server.com/ .. _Baikal: http://sabre.io/baikal/

View file

@ -69,7 +69,7 @@ Now we discover and sync our contacts::
Claws Mail Claws Mail
---------- ----------
Open Claws-Mail. Got to **Tools** => **Addressbook**. Open Claws-Mail. Go to **Tools** => **Addressbook**.
Click on **Addressbook** => **New vCard**. Choose a name for the book. Click on **Addressbook** => **New vCard**. Choose a name for the book.

View file

@ -10,13 +10,13 @@ the settings to use::
[storage cal] [storage cal]
type = "caldav" type = "caldav"
url = "https://caldav.messagingengine.com/" url = "https://caldav.fastmail.com/"
username = ... username = ...
password = ... password = ...
[storage card] [storage card]
type = "carddav" type = "carddav"
url = "https://carddav.messagingengine.com/" url = "https://carddav.fastmail.com/"
username = ... username = ...
password = ... password = ...

View file

@ -17,14 +17,11 @@ cfg['git'] = {
} }
cfg['branches'] = { cfg['branches'] = {
'only': ['auto', 'master', '/^.*-maintenance$/'] 'only': ['auto', 'next', 'master', '/^.*-maintenance$/']
} }
cfg['install'] = """ cfg['install'] = """
. scripts/travis-install.sh . scripts/travis-install.sh
pip install -U pip setuptools
pip install wheel
make -e install-dev
make -e install-$BUILD make -e install-$BUILD
""".strip().splitlines() """.strip().splitlines()

View file

@ -9,6 +9,8 @@ addopts = --tb=short
# E731: Use a def instead of lambda expr # E731: Use a def instead of lambda expr
# E743: Ambiguous function definition # E743: Ambiguous function definition
ignore = E731, E743 ignore = E731, E743
# E503: Line break occurred before a binary operator
extend-ignore = W503
select = C,E,F,W,B,B9 select = C,E,F,W,B,B9
exclude = .eggs, tests/storage/servers/owncloud/, tests/storage/servers/nextcloud/, tests/storage/servers/baikal/, build/ exclude = .eggs, tests/storage/servers/owncloud/, tests/storage/servers/nextcloud/, tests/storage/servers/baikal/, build/
application-package-names = tests,vdirsyncer application-package-names = tests,vdirsyncer

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
''' '''
Vdirsyncer synchronizes calendars and contacts. Vdirsyncer synchronizes calendars and contacts.
@ -18,14 +17,7 @@ requirements = [
# https://github.com/pimutils/vdirsyncer/issues/478 # https://github.com/pimutils/vdirsyncer/issues/478
'click-threading>=0.2', 'click-threading>=0.2',
# !=2.9.0: https://github.com/kennethreitz/requests/issues/2930 'requests >=2.20.0',
#
# >=2.4.1: https://github.com/shazow/urllib3/pull/444
# Without the above pull request, `verify=False` also disables fingerprint
# validation. This is *not* what we want, and it's not possible to
# replicate vdirsyncer's current behavior (verifying fingerprints without
# verifying against CAs) with older versions of urllib3.
'requests >=2.4.1, !=2.9.0',
# https://github.com/sigmavirus24/requests-toolbelt/pull/28 # https://github.com/sigmavirus24/requests-toolbelt/pull/28
# And https://github.com/sigmavirus24/requests-toolbelt/issues/54 # And https://github.com/sigmavirus24/requests-toolbelt/issues/54

View file

@ -1,4 +1,4 @@
hypothesis>=3.1,<4.0 hypothesis>=5.0.0
pytest pytest
pytest-localserver pytest-localserver
pytest-subtesthack pytest-subtesthack

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
''' '''
Test suite for vdirsyncer. Test suite for vdirsyncer.
''' '''
@ -21,7 +20,7 @@ def assert_item_equals(a, b):
assert normalize_item(a) == normalize_item(b) assert normalize_item(a) == normalize_item(b)
VCARD_TEMPLATE = u'''BEGIN:VCARD VCARD_TEMPLATE = '''BEGIN:VCARD
VERSION:3.0 VERSION:3.0
FN:Cyrus Daboo FN:Cyrus Daboo
N:Daboo;Cyrus;;; N:Daboo;Cyrus;;;
@ -37,7 +36,7 @@ X-SOMETHING:{r}
UID:{uid} UID:{uid}
END:VCARD''' END:VCARD'''
TASK_TEMPLATE = u'''BEGIN:VCALENDAR TASK_TEMPLATE = '''BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//dmfs.org//mimedir.icalendar//EN PRODID:-//dmfs.org//mimedir.icalendar//EN
BEGIN:VTODO BEGIN:VTODO
@ -52,7 +51,7 @@ END:VTODO
END:VCALENDAR''' END:VCALENDAR'''
BARE_EVENT_TEMPLATE = u'''BEGIN:VEVENT BARE_EVENT_TEMPLATE = '''BEGIN:VEVENT
DTSTART:19970714T170000Z DTSTART:19970714T170000Z
DTEND:19970715T035959Z DTEND:19970715T035959Z
SUMMARY:Bastille Day Party SUMMARY:Bastille Day Party
@ -61,10 +60,10 @@ UID:{uid}
END:VEVENT''' END:VEVENT'''
EVENT_TEMPLATE = u'''BEGIN:VCALENDAR EVENT_TEMPLATE = '''BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN PRODID:-//hacksw/handcal//NONSGML v1.0//EN
''' + BARE_EVENT_TEMPLATE + u''' ''' + BARE_EVENT_TEMPLATE + '''
END:VCALENDAR''' END:VCALENDAR'''
EVENT_WITH_TIMEZONE_TEMPLATE = '''BEGIN:VCALENDAR EVENT_WITH_TIMEZONE_TEMPLATE = '''BEGIN:VCALENDAR
@ -90,7 +89,7 @@ END:VTIMEZONE
END:VCALENDAR''' END:VCALENDAR'''
SIMPLE_TEMPLATE = u'''BEGIN:FOO SIMPLE_TEMPLATE = '''BEGIN:FOO
UID:{uid} UID:{uid}
X-SOMETHING:{r} X-SOMETHING:{r}
HAHA:YES HAHA:YES

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
''' '''
General-purpose fixtures for vdirsyncer's testsuite. General-purpose fixtures for vdirsyncer's testsuite.
''' '''
@ -34,8 +33,7 @@ settings.register_profile("ci", settings(
)) ))
settings.register_profile("deterministic", settings( settings.register_profile("deterministic", settings(
derandomize=True, derandomize=True,
perform_health_check=False, suppress_health_check=HealthCheck.all(),
suppress_health_check=[HealthCheck.too_slow],
)) ))
if os.environ.get('DETERMINISTIC_TESTS', 'false').lower() == 'true': if os.environ.get('DETERMINISTIC_TESTS', 'false').lower() == 'true':

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import random import random
import uuid import uuid
@ -31,7 +29,7 @@ def format_item(item_template, uid=None):
return Item(item_template.format(r=r, uid=uid or r)) return Item(item_template.format(r=r, uid=uid or r))
class StorageTests(object): class StorageTests:
storage_class = None storage_class = None
supports_collections = True supports_collections = True
supports_metadata = True supports_metadata = True
@ -173,10 +171,10 @@ class StorageTests(object):
_, etag = s.get(href) _, etag = s.get(href)
info[href] = etag info[href] = etag
assert dict( assert {
(href, etag) for href, item, etag href: etag for href, item, etag
in s.get_multi(href for href, etag in info.items()) in s.get_multi(href for href, etag in info.items())
) == info } == info
def test_repr(self, s, get_storage_args): def test_repr(self, s, get_storage_args):
assert self.storage_class.__name__ in repr(s) assert self.storage_class.__name__ in repr(s)
@ -191,10 +189,10 @@ class StorageTests(object):
s.upload(get_item()) s.upload(get_item())
collections.add(s.collection) collections.add(s.collection)
actual = set( actual = {
c['collection'] for c in c['collection'] for c in
self.storage_class.discover(**get_storage_args(collection=None)) self.storage_class.discover(**get_storage_args(collection=None))
) }
assert actual >= collections assert actual >= collections
@ -212,7 +210,7 @@ class StorageTests(object):
) )
href = s.upload(get_item())[0] href = s.upload(get_item())[0]
assert href in set(href for href, etag in s.list()) assert href in {href for href, etag in s.list()}
def test_discover_collection_arg(self, requires_collections, def test_discover_collection_arg(self, requires_collections,
get_storage_args): get_storage_args):
@ -255,7 +253,7 @@ class StorageTests(object):
monkeypatch.setattr('vdirsyncer.utils.generate_href', lambda x: x) monkeypatch.setattr('vdirsyncer.utils.generate_href', lambda x: x)
uid = u'test @ foo ät bar град сатану' uid = 'test @ foo ät bar град сатану'
collection = 'test @ foo ät bar' collection = 'test @ foo ät bar'
s = self.storage_class(**get_storage_args(collection=collection)) s = self.storage_class(**get_storage_args(collection=collection))
@ -286,12 +284,12 @@ class StorageTests(object):
try: try:
s.set_meta('color', None) s.set_meta('color', None)
assert not s.get_meta('color') assert not s.get_meta('color')
s.set_meta('color', u'#ff0000') s.set_meta('color', '#ff0000')
assert s.get_meta('color') == u'#ff0000' assert s.get_meta('color') == '#ff0000'
except exceptions.UnsupportedMetadataError: except exceptions.UnsupportedMetadataError:
pass pass
for x in (u'hello world', u'hello wörld'): for x in ('hello world', 'hello wörld'):
s.set_meta('displayname', x) s.set_meta('displayname', x)
rv = s.get_meta('displayname') rv = s.get_meta('displayname')
assert rv == x assert rv == x
@ -315,7 +313,7 @@ class StorageTests(object):
pytest.skip('This storage instance doesn\'t support iCalendar.') pytest.skip('This storage instance doesn\'t support iCalendar.')
uid = str(uuid.uuid4()) uid = str(uuid.uuid4())
item = Item(textwrap.dedent(u''' item = Item(textwrap.dedent('''
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest import pytest
import uuid import uuid

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import uuid import uuid
import os import os
@ -27,7 +25,7 @@ class DAVStorageTests(ServerMixin, StorageTests):
@pytest.mark.skipif(dav_server == 'radicale', @pytest.mark.skipif(dav_server == 'radicale',
reason='Radicale is very tolerant.') reason='Radicale is very tolerant.')
def test_dav_broken_item(self, s): def test_dav_broken_item(self, s):
item = Item(u'HAHA:YES') item = Item('HAHA:YES')
with pytest.raises((exceptions.Error, requests.exceptions.HTTPError)): with pytest.raises((exceptions.Error, requests.exceptions.HTTPError)):
s.upload(item) s.upload(item)
assert not list(s.list()) assert not list(s.list())
@ -50,7 +48,7 @@ class DAVStorageTests(ServerMixin, StorageTests):
monkeypatch.setattr(s, '_get_href', monkeypatch.setattr(s, '_get_href',
lambda item: item.ident + s.fileext) lambda item: item.ident + s.fileext)
item = get_item(uid=u'град сатану' + str(uuid.uuid4())) item = get_item(uid='град сатану' + str(uuid.uuid4()))
href, etag = s.upload(item) href, etag = s.upload(item)
item2, etag2 = s.get(href) item2, etag2 = s.get(href)
assert_item_equals(item, item2) assert_item_equals(item, item2)

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import datetime import datetime
from textwrap import dedent from textwrap import dedent
@ -64,7 +62,7 @@ class TestCalDAVStorage(DAVStorageTests):
s = self.storage_class(start_date=start_date, end_date=end_date, s = self.storage_class(start_date=start_date, end_date=end_date,
**get_storage_args()) **get_storage_args())
too_old_item = format_item(dedent(u''' too_old_item = format_item(dedent('''
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -78,7 +76,7 @@ class TestCalDAVStorage(DAVStorageTests):
END:VCALENDAR END:VCALENDAR
''').strip()) ''').strip())
too_new_item = format_item(dedent(u''' too_new_item = format_item(dedent('''
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -92,7 +90,7 @@ class TestCalDAVStorage(DAVStorageTests):
END:VCALENDAR END:VCALENDAR
''').strip()) ''').strip())
good_item = format_item(dedent(u''' good_item = format_item(dedent('''
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -140,13 +138,13 @@ class TestCalDAVStorage(DAVStorageTests):
task = s.upload(format_item(TASK_TEMPLATE))[0] task = s.upload(format_item(TASK_TEMPLATE))[0]
s.item_types = ('VTODO', 'VEVENT') s.item_types = ('VTODO', 'VEVENT')
def l(): def hrefs():
return set(href for href, etag in s.list()) return {href for href, etag in s.list()}
assert l() == {event, task} assert hrefs() == {event, task}
s.item_types = ('VTODO',) s.item_types = ('VTODO',)
assert l() == {task} assert hrefs() == {task}
s.item_types = ('VEVENT',) s.item_types = ('VEVENT',)
assert l() == {event} assert hrefs() == {event}
s.item_types = () s.item_types = ()
assert l() == {event, task} assert hrefs() == {event, task}

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest import pytest
from vdirsyncer.storage.dav import CardDAVStorage from vdirsyncer.storage.dav import CardDAVStorage

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import shutil import shutil
import os import os
import sys import sys

View file

@ -1 +0,0 @@
# -*- coding: utf-8 -*-

View file

@ -14,7 +14,7 @@ except KeyError as e:
@pytest.mark.flaky(reruns=5) @pytest.mark.flaky(reruns=5)
class ServerMixin(object): class ServerMixin:
@pytest.fixture @pytest.fixture
def davical_args(self): def davical_args(self):
if self.storage_class.fileext == '.ics': if self.storage_class.fileext == '.ics':

View file

@ -3,7 +3,7 @@ import os
import pytest import pytest
class ServerMixin(object): class ServerMixin:
@pytest.fixture @pytest.fixture
def get_storage_args(self, slow_create_collection): def get_storage_args(self, slow_create_collection):
@ -14,9 +14,9 @@ class ServerMixin(object):
} }
if self.storage_class.fileext == '.ics': if self.storage_class.fileext == '.ics':
args['url'] = 'https://caldav.messagingengine.com/' args['url'] = 'https://caldav.fastmail.com/'
elif self.storage_class.fileext == '.vcf': elif self.storage_class.fileext == '.vcf':
args['url'] = 'https://carddav.messagingengine.com/' args['url'] = 'https://carddav.fastmail.com/'
else: else:
raise RuntimeError() raise RuntimeError()

View file

@ -3,7 +3,7 @@ import os
import pytest import pytest
class ServerMixin(object): class ServerMixin:
@pytest.fixture @pytest.fixture
def get_storage_args(self, item_type, slow_create_collection): def get_storage_args(self, item_type, slow_create_collection):

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import os import os
import subprocess import subprocess
import time import time
@ -28,7 +26,7 @@ def wait():
return False return False
class ServerMixin(object): class ServerMixin:
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def setup_mysteryshack_server(self, xprocess): def setup_mysteryshack_server(self, xprocess):
def preparefunc(cwd): def preparefunc(cwd):

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import logging import logging
import pytest import pytest
@ -14,7 +12,7 @@ import wsgi_intercept.requests_intercept
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ServerMixin(object): class ServerMixin:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(self, request, tmpdir): def setup(self, request, tmpdir):

View file

@ -1,7 +1,7 @@
import pytest import pytest
class ServerMixin(object): class ServerMixin:
@pytest.fixture @pytest.fixture
def get_storage_args(self): def get_storage_args(self):

View file

@ -6,7 +6,7 @@ import wsgi_intercept
import wsgi_intercept.requests_intercept import wsgi_intercept.requests_intercept
class ServerMixin(object): class ServerMixin:
@pytest.fixture @pytest.fixture
def get_storage_args(self, request, tmpdir, slow_create_collection): def get_storage_args(self, request, tmpdir, slow_create_collection):
tmpdir.mkdir('xandikos') tmpdir.mkdir('xandikos')

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import subprocess import subprocess
import pytest import pytest
@ -32,8 +30,8 @@ class TestFilesystemStorage(StorageTests):
def test_broken_data(self, tmpdir): def test_broken_data(self, tmpdir):
s = self.storage_class(str(tmpdir), '.txt') s = self.storage_class(str(tmpdir), '.txt')
class BrokenItem(object): class BrokenItem:
raw = u'Ц, Ш, Л, ж, Д, З, Ю'.encode('utf-8') raw = 'Ц, Ш, Л, ж, Д, З, Ю'.encode()
uid = 'jeezus' uid = 'jeezus'
ident = uid ident = uid
with pytest.raises(TypeError): with pytest.raises(TypeError):
@ -42,13 +40,13 @@ class TestFilesystemStorage(StorageTests):
def test_ident_with_slash(self, tmpdir): def test_ident_with_slash(self, tmpdir):
s = self.storage_class(str(tmpdir), '.txt') s = self.storage_class(str(tmpdir), '.txt')
s.upload(Item(u'UID:a/b/c')) s.upload(Item('UID:a/b/c'))
item_file, = tmpdir.listdir() item_file, = tmpdir.listdir()
assert '/' not in item_file.basename and item_file.isfile() assert '/' not in item_file.basename and item_file.isfile()
def test_too_long_uid(self, tmpdir): def test_too_long_uid(self, tmpdir):
s = self.storage_class(str(tmpdir), '.txt') s = self.storage_class(str(tmpdir), '.txt')
item = Item(u'UID:' + u'hue' * 600) item = Item('UID:' + 'hue' * 600)
href, etag = s.upload(item) href, etag = s.upload(item)
assert item.uid not in href assert item.uid not in href
@ -60,27 +58,27 @@ class TestFilesystemStorage(StorageTests):
monkeypatch.setattr(subprocess, 'call', check_call_mock) monkeypatch.setattr(subprocess, 'call', check_call_mock)
s = self.storage_class(str(tmpdir), '.txt', post_hook=None) s = self.storage_class(str(tmpdir), '.txt', post_hook=None)
s.upload(Item(u'UID:a/b/c')) s.upload(Item('UID:a/b/c'))
def test_post_hook_active(self, tmpdir, monkeypatch): def test_post_hook_active(self, tmpdir, monkeypatch):
calls = [] calls = []
exe = 'foo' exe = 'foo'
def check_call_mock(l, *args, **kwargs): def check_call_mock(call, *args, **kwargs):
calls.append(True) calls.append(True)
assert len(l) == 2 assert len(call) == 2
assert l[0] == exe assert call[0] == exe
monkeypatch.setattr(subprocess, 'call', check_call_mock) monkeypatch.setattr(subprocess, 'call', check_call_mock)
s = self.storage_class(str(tmpdir), '.txt', post_hook=exe) s = self.storage_class(str(tmpdir), '.txt', post_hook=exe)
s.upload(Item(u'UID:a/b/c')) s.upload(Item('UID:a/b/c'))
assert calls assert calls
def test_ignore_git_dirs(self, tmpdir): def test_ignore_git_dirs(self, tmpdir):
tmpdir.mkdir('.git').mkdir('foo') tmpdir.mkdir('.git').mkdir('foo')
tmpdir.mkdir('a') tmpdir.mkdir('a')
tmpdir.mkdir('b') tmpdir.mkdir('b')
assert set(c['collection'] for c assert {c['collection'] for c
in self.storage_class.discover(str(tmpdir))) == {'a', 'b'} in self.storage_class.discover(str(tmpdir))} == {'a', 'b'}

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest import pytest
from requests import Response from requests import Response
@ -14,25 +12,25 @@ def test_list(monkeypatch):
collection_url = 'http://127.0.0.1/calendar/collection.ics' collection_url = 'http://127.0.0.1/calendar/collection.ics'
items = [ items = [
(u'BEGIN:VEVENT\n' ('BEGIN:VEVENT\n'
u'SUMMARY:Eine Kurzinfo\n' 'SUMMARY:Eine Kurzinfo\n'
u'DESCRIPTION:Beschreibung des Termines\n' 'DESCRIPTION:Beschreibung des Termines\n'
u'END:VEVENT'), 'END:VEVENT'),
(u'BEGIN:VEVENT\n' ('BEGIN:VEVENT\n'
u'SUMMARY:Eine zweite Küèrzinfo\n' 'SUMMARY:Eine zweite Küèrzinfo\n'
u'DESCRIPTION:Beschreibung des anderen Termines\n' 'DESCRIPTION:Beschreibung des anderen Termines\n'
u'BEGIN:VALARM\n' 'BEGIN:VALARM\n'
u'ACTION:AUDIO\n' 'ACTION:AUDIO\n'
u'TRIGGER:19980403T120000\n' 'TRIGGER:19980403T120000\n'
u'ATTACH;FMTTYPE=audio/basic:http://host.com/pub/ssbanner.aud\n' 'ATTACH;FMTTYPE=audio/basic:http://host.com/pub/ssbanner.aud\n'
u'REPEAT:4\n' 'REPEAT:4\n'
u'DURATION:PT1H\n' 'DURATION:PT1H\n'
u'END:VALARM\n' 'END:VALARM\n'
u'END:VEVENT') 'END:VEVENT')
] ]
responses = [ responses = [
u'\n'.join([u'BEGIN:VCALENDAR'] + items + [u'END:VCALENDAR']) '\n'.join(['BEGIN:VCALENDAR'] + items + ['END:VCALENDAR'])
] * 2 ] * 2
def get(self, method, url, *a, **kw): def get(self, method, url, *a, **kw):
@ -58,8 +56,8 @@ def test_list(monkeypatch):
assert etag2 == etag assert etag2 == etag
found_items[normalize_item(item)] = href found_items[normalize_item(item)] = href
expected = set(normalize_item(u'BEGIN:VCALENDAR\n' + x + '\nEND:VCALENDAR') expected = {normalize_item('BEGIN:VCALENDAR\n' + x + '\nEND:VCALENDAR')
for x in items) for x in items}
assert set(found_items) == expected assert set(found_items) == expected

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest import pytest
from requests import Response from requests import Response
@ -21,7 +19,7 @@ class CombinedStorage(Storage):
if kwargs.get('collection', None) is not None: if kwargs.get('collection', None) is not None:
raise ValueError() raise ValueError()
super(CombinedStorage, self).__init__(**kwargs) super().__init__(**kwargs)
self.url = url self.url = url
self.path = path self.path = path
self._reader = vdirsyncer.storage.http.HttpStorage(url=url) self._reader = vdirsyncer.storage.http.HttpStorage(url=url)

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest import pytest
from vdirsyncer.storage.memory import MemoryStorage from vdirsyncer.storage.memory import MemoryStorage

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest import pytest
from vdirsyncer.storage.singlefile import SingleFileStorage from vdirsyncer.storage.singlefile import SingleFileStorage

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from textwrap import dedent from textwrap import dedent
from click.testing import CliRunner from click.testing import CliRunner
@ -9,7 +7,7 @@ import pytest
import vdirsyncer.cli as cli import vdirsyncer.cli as cli
class _CustomRunner(object): class _CustomRunner:
def __init__(self, tmpdir): def __init__(self, tmpdir):
self.tmpdir = tmpdir self.tmpdir = tmpdir
self.cfg = tmpdir.join('config') self.cfg = tmpdir.join('config')

View file

@ -23,7 +23,7 @@ def read_config(tmpdir, monkeypatch):
def test_read_config(read_config): def test_read_config(read_config):
errors, c = read_config(u''' errors, c = read_config('''
[general] [general]
status_path = "/tmp/status/" status_path = "/tmp/status/"
@ -59,7 +59,7 @@ def test_read_config(read_config):
def test_missing_collections_param(read_config): def test_missing_collections_param(read_config):
with pytest.raises(exceptions.UserError) as excinfo: with pytest.raises(exceptions.UserError) as excinfo:
read_config(u''' read_config('''
[general] [general]
status_path = "/tmp/status/" status_path = "/tmp/status/"
@ -79,7 +79,7 @@ def test_missing_collections_param(read_config):
def test_invalid_section_type(read_config): def test_invalid_section_type(read_config):
with pytest.raises(exceptions.UserError) as excinfo: with pytest.raises(exceptions.UserError) as excinfo:
read_config(u''' read_config('''
[general] [general]
status_path = "/tmp/status/" status_path = "/tmp/status/"
@ -92,7 +92,7 @@ def test_invalid_section_type(read_config):
def test_missing_general_section(read_config): def test_missing_general_section(read_config):
with pytest.raises(exceptions.UserError) as excinfo: with pytest.raises(exceptions.UserError) as excinfo:
read_config(u''' read_config('''
[pair my_pair] [pair my_pair]
a = "my_a" a = "my_a"
b = "my_b" b = "my_b"
@ -114,7 +114,7 @@ def test_missing_general_section(read_config):
def test_wrong_general_section(read_config): def test_wrong_general_section(read_config):
with pytest.raises(exceptions.UserError) as excinfo: with pytest.raises(exceptions.UserError) as excinfo:
read_config(u''' read_config('''
[general] [general]
wrong = true wrong = true
''') ''')
@ -128,7 +128,7 @@ def test_wrong_general_section(read_config):
def test_invalid_storage_name(read_config): def test_invalid_storage_name(read_config):
with pytest.raises(exceptions.UserError) as excinfo: with pytest.raises(exceptions.UserError) as excinfo:
read_config(u''' read_config('''
[general] [general]
status_path = "{base}/status/" status_path = "{base}/status/"
@ -140,7 +140,7 @@ def test_invalid_storage_name(read_config):
def test_invalid_collections_arg(read_config): def test_invalid_collections_arg(read_config):
with pytest.raises(exceptions.UserError) as excinfo: with pytest.raises(exceptions.UserError) as excinfo:
read_config(u''' read_config('''
[general] [general]
status_path = "/tmp/status/" status_path = "/tmp/status/"
@ -165,7 +165,7 @@ def test_invalid_collections_arg(read_config):
def test_duplicate_sections(read_config): def test_duplicate_sections(read_config):
with pytest.raises(exceptions.UserError) as excinfo: with pytest.raises(exceptions.UserError) as excinfo:
read_config(u''' read_config('''
[general] [general]
status_path = "/tmp/status/" status_path = "/tmp/status/"

View file

@ -1,5 +1,3 @@
# encoding: utf-8
from textwrap import dedent from textwrap import dedent
import pytest import pytest

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import json import json
import sys import sys
from textwrap import dedent from textwrap import dedent
@ -257,17 +255,17 @@ def test_multiple_pairs(tmpdir, runner):
result = runner.invoke(['discover']) result = runner.invoke(['discover'])
assert not result.exception assert not result.exception
assert set(result.output.splitlines()) > set([ assert set(result.output.splitlines()) > {
'Discovering collections for pair bambaz', 'Discovering collections for pair bambaz',
'Discovering collections for pair foobar' 'Discovering collections for pair foobar'
]) }
result = runner.invoke(['sync']) result = runner.invoke(['sync'])
assert not result.exception assert not result.exception
assert set(result.output.splitlines()) == set([ assert set(result.output.splitlines()) == {
'Syncing bambaz', 'Syncing bambaz',
'Syncing foobar', 'Syncing foobar',
]) }
# XXX: https://github.com/pimutils/vdirsyncer/issues/617 # XXX: https://github.com/pimutils/vdirsyncer/issues/617
@ -277,17 +275,17 @@ def test_multiple_pairs(tmpdir, runner):
st.text( st.text(
st.characters( st.characters(
blacklist_characters=set( blacklist_characters=set(
u'./\x00' # Invalid chars on POSIX filesystems './\x00' # Invalid chars on POSIX filesystems
), ),
# Surrogates can't be encoded to utf-8 in Python # Surrogates can't be encoded to utf-8 in Python
blacklist_categories=set(['Cs']) blacklist_categories={'Cs'}
), ),
min_size=1, min_size=1,
max_size=50 max_size=50
), ),
min_size=1 min_size=1
)) ))
@example(collections=[u'persönlich']) @example(collections=['persönlich'])
@example(collections={'a', 'A'}) @example(collections={'a', 'A'})
@example(collections={'\ufffe'}) @example(collections={'\ufffe'})
def test_create_collections(subtest, collections): def test_create_collections(subtest, collections):
@ -322,8 +320,8 @@ def test_create_collections(subtest, collections):
) )
assert not result.exception, result.output assert not result.exception, result.output
assert set(x.basename for x in tmpdir.join('foo').listdir()) == \ assert {x.basename for x in tmpdir.join('foo').listdir()} == \
set(x.basename for x in tmpdir.join('bar').listdir()) {x.basename for x in tmpdir.join('bar').listdir()}
def test_ident_conflict(tmpdir, runner): def test_ident_conflict(tmpdir, runner):

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import sys import sys
import logging import logging
@ -20,7 +18,7 @@ def test_get_storage_init_args():
from vdirsyncer.storage.memory import MemoryStorage from vdirsyncer.storage.memory import MemoryStorage
all, required = utils.get_storage_init_args(MemoryStorage) all, required = utils.get_storage_init_args(MemoryStorage)
assert all == set(['fileext', 'collection', 'read_only', 'instance_name']) assert all == {'fileext', 'collection', 'read_only', 'instance_name'}
assert not required assert not required

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest import pytest
from vdirsyncer.cli.discover import expand_collections from vdirsyncer.cli.discover import expand_collections

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import hypothesis.strategies as st import hypothesis.strategies as st
from hypothesis import given from hypothesis import given
@ -23,7 +21,7 @@ def mystrategy(monkeypatch):
def value_cache(monkeypatch): def value_cache(monkeypatch):
_cache = {} _cache = {}
class FakeContext(object): class FakeContext:
fetched_params = _cache fetched_params = _cache
def find_object(self, _): def find_object(self, _):

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from copy import deepcopy from copy import deepcopy
import hypothesis.strategies as st import hypothesis.strategies as st
@ -32,7 +30,7 @@ def empty_storage(x):
def items(s): def items(s):
return set(x[1].raw for x in s.items.values()) return {x[1].raw for x in s.items.values()}
def test_irrelevant_status(): def test_irrelevant_status():
@ -49,7 +47,7 @@ def test_missing_status():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
item = Item(u'asdf') item = Item('asdf')
a.upload(item) a.upload(item)
b.upload(item) b.upload(item)
sync(a, b, status) sync(a, b, status)
@ -62,8 +60,8 @@ def test_missing_status_and_different_items():
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
item1 = Item(u'UID:1\nhaha') item1 = Item('UID:1\nhaha')
item2 = Item(u'UID:1\nhoho') item2 = Item('UID:1\nhoho')
a.upload(item1) a.upload(item1)
b.upload(item2) b.upload(item2)
with pytest.raises(SyncConflict): with pytest.raises(SyncConflict):
@ -79,8 +77,8 @@ def test_read_only_and_prefetch():
b.read_only = True b.read_only = True
status = {} status = {}
item1 = Item(u'UID:1\nhaha') item1 = Item('UID:1\nhaha')
item2 = Item(u'UID:2\nhoho') item2 = Item('UID:2\nhoho')
a.upload(item1) a.upload(item1)
a.upload(item2) a.upload(item2)
@ -152,22 +150,22 @@ def test_upload_and_update():
b = MemoryStorage(fileext='.b') b = MemoryStorage(fileext='.b')
status = {} status = {}
item = Item(u'UID:1') # new item 1 in a item = Item('UID:1') # new item 1 in a
a.upload(item) a.upload(item)
sync(a, b, status) sync(a, b, status)
assert items(b) == items(a) == {item.raw} assert items(b) == items(a) == {item.raw}
item = Item(u'UID:1\nASDF:YES') # update of item 1 in b item = Item('UID:1\nASDF:YES') # update of item 1 in b
b.update('1.b', item, b.get('1.b')[1]) b.update('1.b', item, b.get('1.b')[1])
sync(a, b, status) sync(a, b, status)
assert items(b) == items(a) == {item.raw} assert items(b) == items(a) == {item.raw}
item2 = Item(u'UID:2') # new item 2 in b item2 = Item('UID:2') # new item 2 in b
b.upload(item2) b.upload(item2)
sync(a, b, status) sync(a, b, status)
assert items(b) == items(a) == {item.raw, item2.raw} assert items(b) == items(a) == {item.raw, item2.raw}
item2 = Item(u'UID:2\nASDF:YES') # update of item 2 in a item2 = Item('UID:2\nASDF:YES') # update of item 2 in a
a.update('2.a', item2, a.get('2.a')[1]) a.update('2.a', item2, a.get('2.a')[1])
sync(a, b, status) sync(a, b, status)
assert items(b) == items(a) == {item.raw, item2.raw} assert items(b) == items(a) == {item.raw, item2.raw}
@ -178,9 +176,9 @@ def test_deletion():
b = MemoryStorage(fileext='.b') b = MemoryStorage(fileext='.b')
status = {} status = {}
item = Item(u'UID:1') item = Item('UID:1')
a.upload(item) a.upload(item)
item2 = Item(u'UID:2') item2 = Item('UID:2')
a.upload(item2) a.upload(item2)
sync(a, b, status) sync(a, b, status)
b.delete('1.b', b.get('1.b')[1]) b.delete('1.b', b.get('1.b')[1])
@ -215,7 +213,7 @@ def test_insert_hash():
def test_already_synced(): def test_already_synced():
a = MemoryStorage(fileext='.a') a = MemoryStorage(fileext='.a')
b = MemoryStorage(fileext='.b') b = MemoryStorage(fileext='.b')
item = Item(u'UID:1') item = Item('UID:1')
a.upload(item) a.upload(item)
b.upload(item) b.upload(item)
status = { status = {
@ -243,14 +241,14 @@ def test_already_synced():
def test_conflict_resolution_both_etags_new(winning_storage): def test_conflict_resolution_both_etags_new(winning_storage):
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
item = Item(u'UID:1') item = Item('UID:1')
href_a, etag_a = a.upload(item) href_a, etag_a = a.upload(item)
href_b, etag_b = b.upload(item) href_b, etag_b = b.upload(item)
status = {} status = {}
sync(a, b, status) sync(a, b, status)
assert status assert status
item_a = Item(u'UID:1\nitem a') item_a = Item('UID:1\nitem a')
item_b = Item(u'UID:1\nitem b') item_b = Item('UID:1\nitem b')
a.update(href_a, item_a, etag_a) a.update(href_a, item_a, etag_a)
b.update(href_b, item_b, etag_b) b.update(href_b, item_b, etag_b)
with pytest.raises(SyncConflict): with pytest.raises(SyncConflict):
@ -264,13 +262,13 @@ def test_conflict_resolution_both_etags_new(winning_storage):
def test_updated_and_deleted(): def test_updated_and_deleted():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
href_a, etag_a = a.upload(Item(u'UID:1')) href_a, etag_a = a.upload(Item('UID:1'))
status = {} status = {}
sync(a, b, status, force_delete=True) sync(a, b, status, force_delete=True)
(href_b, etag_b), = b.list() (href_b, etag_b), = b.list()
b.delete(href_b, etag_b) b.delete(href_b, etag_b)
updated = Item(u'UID:1\nupdated') updated = Item('UID:1\nupdated')
a.update(href_a, updated, etag_a) a.update(href_a, updated, etag_a)
sync(a, b, status, force_delete=True) sync(a, b, status, force_delete=True)
@ -280,8 +278,8 @@ def test_updated_and_deleted():
def test_conflict_resolution_invalid_mode(): def test_conflict_resolution_invalid_mode():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
item_a = Item(u'UID:1\nitem a') item_a = Item('UID:1\nitem a')
item_b = Item(u'UID:1\nitem b') item_b = Item('UID:1\nitem b')
a.upload(item_a) a.upload(item_a)
b.upload(item_b) b.upload(item_b)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -291,7 +289,7 @@ def test_conflict_resolution_invalid_mode():
def test_conflict_resolution_new_etags_without_changes(): def test_conflict_resolution_new_etags_without_changes():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
item = Item(u'UID:1') item = Item('UID:1')
href_a, etag_a = a.upload(item) href_a, etag_a = a.upload(item)
href_b, etag_b = b.upload(item) href_b, etag_b = b.upload(item)
status = {'1': (href_a, 'BOGUS_a', href_b, 'BOGUS_b')} status = {'1': (href_a, 'BOGUS_a', href_b, 'BOGUS_b')}
@ -326,7 +324,7 @@ def test_uses_get_multi(monkeypatch):
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
item = Item(u'UID:1') item = Item('UID:1')
expected_href, etag = a.upload(item) expected_href, etag = a.upload(item)
sync(a, b, {}) sync(a, b, {})
@ -336,8 +334,8 @@ def test_uses_get_multi(monkeypatch):
def test_empty_storage_dataloss(): def test_empty_storage_dataloss():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
a.upload(Item(u'UID:1')) a.upload(Item('UID:1'))
a.upload(Item(u'UID:2')) a.upload(Item('UID:2'))
status = {} status = {}
sync(a, b, status) sync(a, b, status)
with pytest.raises(StorageEmpty): with pytest.raises(StorageEmpty):
@ -350,22 +348,22 @@ def test_empty_storage_dataloss():
def test_no_uids(): def test_no_uids():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
a.upload(Item(u'ASDF')) a.upload(Item('ASDF'))
b.upload(Item(u'FOOBAR')) b.upload(Item('FOOBAR'))
status = {} status = {}
sync(a, b, status) sync(a, b, status)
assert items(a) == items(b) == {u'ASDF', u'FOOBAR'} assert items(a) == items(b) == {'ASDF', 'FOOBAR'}
def test_changed_uids(): def test_changed_uids():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
href_a, etag_a = a.upload(Item(u'UID:A-ONE')) href_a, etag_a = a.upload(Item('UID:A-ONE'))
href_b, etag_b = b.upload(Item(u'UID:B-ONE')) href_b, etag_b = b.upload(Item('UID:B-ONE'))
status = {} status = {}
sync(a, b, status) sync(a, b, status)
a.update(href_a, Item(u'UID:A-TWO'), etag_a) a.update(href_a, Item('UID:A-TWO'), etag_a)
sync(a, b, status) sync(a, b, status)
@ -383,8 +381,8 @@ def test_partial_sync_revert():
a = MemoryStorage(instance_name='a') a = MemoryStorage(instance_name='a')
b = MemoryStorage(instance_name='b') b = MemoryStorage(instance_name='b')
status = {} status = {}
a.upload(Item(u'UID:1')) a.upload(Item('UID:1'))
b.upload(Item(u'UID:2')) b.upload(Item('UID:2'))
b.read_only = True b.read_only = True
sync(a, b, status, partial_sync='revert') sync(a, b, status, partial_sync='revert')
@ -418,13 +416,13 @@ def test_ident_conflict(sync_inbetween):
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
href_a, etag_a = a.upload(Item(u'UID:aaa')) href_a, etag_a = a.upload(Item('UID:aaa'))
href_b, etag_b = a.upload(Item(u'UID:bbb')) href_b, etag_b = a.upload(Item('UID:bbb'))
if sync_inbetween: if sync_inbetween:
sync(a, b, status) sync(a, b, status)
a.update(href_a, Item(u'UID:xxx'), etag_a) a.update(href_a, Item('UID:xxx'), etag_a)
a.update(href_b, Item(u'UID:xxx'), etag_b) a.update(href_b, Item('UID:xxx'), etag_b)
with pytest.raises(IdentConflict): with pytest.raises(IdentConflict):
sync(a, b, status) sync(a, b, status)
@ -441,7 +439,7 @@ def test_moved_href():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
href, etag = a.upload(Item(u'UID:haha')) href, etag = a.upload(Item('UID:haha'))
sync(a, b, status) sync(a, b, status)
b.items['lol'] = b.items.pop('haha') b.items['lol'] = b.items.pop('haha')
@ -476,26 +474,26 @@ def test_bogus_etag_change():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
href_a, etag_a = a.upload(Item(u'UID:ASDASD')) href_a, etag_a = a.upload(Item('UID:ASDASD'))
sync(a, b, status) sync(a, b, status)
assert len(status) == len(list(a.list())) == len(list(b.list())) == 1 assert len(status) == len(list(a.list())) == len(list(b.list())) == 1
(href_b, etag_b), = b.list() (href_b, etag_b), = b.list()
a.update(href_a, Item(u'UID:ASDASD'), etag_a) a.update(href_a, Item('UID:ASDASD'), etag_a)
b.update(href_b, Item(u'UID:ASDASD\nACTUALCHANGE:YES'), etag_b) b.update(href_b, Item('UID:ASDASD\nACTUALCHANGE:YES'), etag_b)
b.delete = b.update = b.upload = blow_up b.delete = b.update = b.upload = blow_up
sync(a, b, status) sync(a, b, status)
assert len(status) == 1 assert len(status) == 1
assert items(a) == items(b) == {u'UID:ASDASD\nACTUALCHANGE:YES'} assert items(a) == items(b) == {'UID:ASDASD\nACTUALCHANGE:YES'}
def test_unicode_hrefs(): def test_unicode_hrefs():
a = MemoryStorage() a = MemoryStorage()
b = MemoryStorage() b = MemoryStorage()
status = {} status = {}
href, etag = a.upload(Item(u'UID:äää')) href, etag = a.upload(Item('UID:äää'))
sync(a, b, status) sync(a, b, status)
@ -565,7 +563,7 @@ class SyncMachine(RuleBasedStateMachine):
uid=uid_strategy, uid=uid_strategy,
etag=st.text()) etag=st.text())
def upload(self, storage, uid, etag): def upload(self, storage, uid, etag):
item = Item(u'UID:{}'.format(uid)) item = Item('UID:{}'.format(uid))
storage.items[uid] = (etag, item) storage.items[uid] = (etag, item)
@rule(storage=Storage, href=st.text()) @rule(storage=Storage, href=st.text())

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import hypothesis.strategies as st import hypothesis.strategies as st
from hypothesis import example, given from hypothesis import example, given
@ -130,7 +128,7 @@ metadata = st.dictionaries(keys, values)
status=metadata, keys=st.sets(keys), status=metadata, keys=st.sets(keys),
conflict_resolution=st.just('a wins') | st.just('b wins') conflict_resolution=st.just('a wins') | st.just('b wins')
) )
@example(a={u'0': u'0'}, b={}, status={u'0': u'0'}, keys={u'0'}, @example(a={'0': '0'}, b={}, status={'0': '0'}, keys={'0'},
conflict_resolution='a wins') conflict_resolution='a wins')
@example(a={'0': '0'}, b={'0': '1'}, status={'0': '0'}, keys={'0'}, @example(a={'0': '0'}, b={'0': '1'}, status={'0': '0'}, keys={'0'},
conflict_resolution='a wins') conflict_resolution='a wins')
@ -144,9 +142,9 @@ def test_fuzzing(a, b, status, keys, conflict_resolution):
b = _get_storage(b, 'B') b = _get_storage(b, 'B')
winning_storage = (a if conflict_resolution == 'a wins' else b) winning_storage = (a if conflict_resolution == 'a wins' else b)
expected_values = dict((key, winning_storage.get_meta(key)) expected_values = {key: winning_storage.get_meta(key)
for key in keys for key in keys
if key not in status) if key not in status}
metasync(a, b, status, metasync(a, b, status,
keys=keys, conflict_resolution=conflict_resolution) keys=keys, conflict_resolution=conflict_resolution)

View file

@ -1,4 +1,4 @@
from hypothesis import given, settings from hypothesis import HealthCheck, given, settings
import pytest import pytest
@ -11,17 +11,18 @@ from vdirsyncer.vobject import Item
@given(uid=uid_strategy) @given(uid=uid_strategy)
@settings(perform_health_check=False) # Using the random module for UIDs # Using the random module for UIDs:
@settings(suppress_health_check=HealthCheck.all())
def test_repair_uids(uid): def test_repair_uids(uid):
s = MemoryStorage() s = MemoryStorage()
s.items = { s.items = {
'one': ( 'one': (
'asdf', 'asdf',
Item(u'BEGIN:VCARD\nFN:Hans\nUID:{}\nEND:VCARD'.format(uid)) Item('BEGIN:VCARD\nFN:Hans\nUID:{}\nEND:VCARD'.format(uid))
), ),
'two': ( 'two': (
'asdf', 'asdf',
Item(u'BEGIN:VCARD\nFN:Peppi\nUID:{}\nEND:VCARD'.format(uid)) Item('BEGIN:VCARD\nFN:Peppi\nUID:{}\nEND:VCARD'.format(uid))
) )
} }
@ -35,10 +36,11 @@ def test_repair_uids(uid):
@given(uid=uid_strategy.filter(lambda x: not href_safe(x))) @given(uid=uid_strategy.filter(lambda x: not href_safe(x)))
@settings(perform_health_check=False) # Using the random module for UIDs # Using the random module for UIDs:
@settings(suppress_health_check=HealthCheck.all())
def test_repair_unsafe_uids(uid): def test_repair_unsafe_uids(uid):
s = MemoryStorage() s = MemoryStorage()
item = Item(u'BEGIN:VCARD\nUID:{}\nEND:VCARD'.format(uid)) item = Item('BEGIN:VCARD\nUID:{}\nEND:VCARD'.format(uid))
href, etag = s.upload(item) href, etag = s.upload(item)
assert s.get(href)[0].uid == uid assert s.get(href)[0].uid == uid
assert not href_safe(uid) assert not href_safe(uid)

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from textwrap import dedent from textwrap import dedent
import hypothesis.strategies as st import hypothesis.strategies as st
@ -21,10 +19,10 @@ _simple_split = [
VCARD_TEMPLATE.format(r=678, uid=678) VCARD_TEMPLATE.format(r=678, uid=678)
] ]
_simple_joined = u'\r\n'.join( _simple_joined = '\r\n'.join(
[u'BEGIN:VADDRESSBOOK'] + ['BEGIN:VADDRESSBOOK']
_simple_split + + _simple_split
[u'END:VADDRESSBOOK\r\n'] + ['END:VADDRESSBOOK\r\n']
) )
@ -39,10 +37,10 @@ def test_split_collection_simple(benchmark):
def test_split_collection_multiple_wrappers(benchmark): def test_split_collection_multiple_wrappers(benchmark):
joined = u'\r\n'.join( joined = '\r\n'.join(
u'BEGIN:VADDRESSBOOK\r\n' + 'BEGIN:VADDRESSBOOK\r\n'
x + + x
u'\r\nEND:VADDRESSBOOK\r\n' + '\r\nEND:VADDRESSBOOK\r\n'
for x in _simple_split for x in _simple_split
) )
given = benchmark(lambda: list(vobject.split_collection(joined))) given = benchmark(lambda: list(vobject.split_collection(joined)))
@ -105,32 +103,32 @@ def test_split_collection_timezones():
] ]
timezone = ( timezone = (
u'BEGIN:VTIMEZONE\r\n' 'BEGIN:VTIMEZONE\r\n'
u'TZID:/mozilla.org/20070129_1/Asia/Tokyo\r\n' 'TZID:/mozilla.org/20070129_1/Asia/Tokyo\r\n'
u'X-LIC-LOCATION:Asia/Tokyo\r\n' 'X-LIC-LOCATION:Asia/Tokyo\r\n'
u'BEGIN:STANDARD\r\n' 'BEGIN:STANDARD\r\n'
u'TZOFFSETFROM:+0900\r\n' 'TZOFFSETFROM:+0900\r\n'
u'TZOFFSETTO:+0900\r\n' 'TZOFFSETTO:+0900\r\n'
u'TZNAME:JST\r\n' 'TZNAME:JST\r\n'
u'DTSTART:19700101T000000\r\n' 'DTSTART:19700101T000000\r\n'
u'END:STANDARD\r\n' 'END:STANDARD\r\n'
u'END:VTIMEZONE' 'END:VTIMEZONE'
) )
full = u'\r\n'.join( full = '\r\n'.join(
[u'BEGIN:VCALENDAR'] + ['BEGIN:VCALENDAR']
items + + items
[timezone, u'END:VCALENDAR'] + [timezone, 'END:VCALENDAR']
) )
given = set(normalize_item(item) given = {normalize_item(item)
for item in vobject.split_collection(full)) for item in vobject.split_collection(full)}
expected = set( expected = {
normalize_item(u'\r\n'.join(( normalize_item('\r\n'.join((
u'BEGIN:VCALENDAR', item, timezone, u'END:VCALENDAR' 'BEGIN:VCALENDAR', item, timezone, 'END:VCALENDAR'
))) )))
for item in items for item in items
) }
assert given == expected assert given == expected
@ -148,20 +146,20 @@ def test_split_contacts():
def test_hash_item(): def test_hash_item():
a = EVENT_TEMPLATE.format(r=1, uid=1) a = EVENT_TEMPLATE.format(r=1, uid=1)
b = u'\n'.join(line for line in a.splitlines() b = '\n'.join(line for line in a.splitlines()
if u'PRODID' not in line) if 'PRODID' not in line)
assert vobject.hash_item(a) == vobject.hash_item(b) assert vobject.hash_item(a) == vobject.hash_item(b)
def test_multiline_uid(benchmark): def test_multiline_uid(benchmark):
a = (u'BEGIN:FOO\r\n' a = ('BEGIN:FOO\r\n'
u'UID:123456789abcd\r\n' 'UID:123456789abcd\r\n'
u' efgh\r\n' ' efgh\r\n'
u'END:FOO\r\n') 'END:FOO\r\n')
assert benchmark(lambda: vobject.Item(a).uid) == u'123456789abcdefgh' assert benchmark(lambda: vobject.Item(a).uid) == '123456789abcdefgh'
complex_uid_item = dedent(u''' complex_uid_item = dedent('''
BEGIN:VCALENDAR BEGIN:VCALENDAR
BEGIN:VTIMEZONE BEGIN:VTIMEZONE
TZID:Europe/Rome TZID:Europe/Rome
@ -202,9 +200,9 @@ complex_uid_item = dedent(u'''
def test_multiline_uid_complex(benchmark): def test_multiline_uid_complex(benchmark):
assert benchmark(lambda: vobject.Item(complex_uid_item).uid) == ( assert benchmark(lambda: vobject.Item(complex_uid_item).uid) == (
u'040000008200E00074C5B7101A82E008000000005' '040000008200E00074C5B7101A82E008000000005'
u'0AAABEEF50DCF001000000062548482FA830A46B9' '0AAABEEF50DCF001000000062548482FA830A46B9'
u'EA62114AC9F0EF' 'EA62114AC9F0EF'
) )

View file

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
''' '''
Vdirsyncer synchronizes calendars and contacts. Vdirsyncer synchronizes calendars and contacts.
''' '''
from __future__ import print_function
PROJECT_HOME = 'https://github.com/pimutils/vdirsyncer' PROJECT_HOME = 'https://github.com/pimutils/vdirsyncer'
BUGTRACKER_HOME = PROJECT_HOME + '/issues' BUGTRACKER_HOME = PROJECT_HOME + '/issues'

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import functools import functools
import logging import logging
import sys import sys
@ -15,7 +13,7 @@ cli_logger = logging.getLogger(__name__)
click_log.basic_config('vdirsyncer') click_log.basic_config('vdirsyncer')
class AppContext(object): class AppContext:
def __init__(self): def __init__(self):
self.config = None self.config = None
self.fetched_params = {} self.fetched_params = {}

View file

@ -33,17 +33,17 @@ def _validate_general_section(general_config):
problems = [] problems = []
if invalid: if invalid:
problems.append(u'general section doesn\'t take the parameters: {}' problems.append('general section doesn\'t take the parameters: {}'
.format(u', '.join(invalid))) .format(', '.join(invalid)))
if missing: if missing:
problems.append(u'general section is missing the parameters: {}' problems.append('general section is missing the parameters: {}'
.format(u', '.join(missing))) .format(', '.join(missing)))
if problems: if problems:
raise exceptions.UserError( raise exceptions.UserError(
u'Invalid general section. Copy the example ' 'Invalid general section. Copy the example '
u'config from the repository and edit it: {}' 'config from the repository and edit it: {}'
.format(PROJECT_HOME), problems=problems) .format(PROJECT_HOME), problems=problems)
@ -64,7 +64,7 @@ def _validate_collections_param(collections):
e = ValueError( e = ValueError(
'Expected list of format ' 'Expected list of format '
'["config_name", "storage_a_name", "storage_b_name"]' '["config_name", "storage_a_name", "storage_b_name"]'
.format(len(collection))) )
if len(collection) != 3: if len(collection) != 3:
raise e raise e
@ -151,7 +151,7 @@ def _parse_options(items, section=None):
.format(section, key, e)) .format(section, key, e))
class Config(object): class Config:
def __init__(self, general, pairs, storages): def __init__(self, general, pairs, storages):
self.general = general self.general = general
self.storages = storages self.storages = storages
@ -209,7 +209,7 @@ class Config(object):
raise exceptions.PairNotFound(e, pair_name=pair_name) raise exceptions.PairNotFound(e, pair_name=pair_name)
class PairConfig(object): class PairConfig:
def __init__(self, full_config, name, options): def __init__(self, full_config, name, options):
self._config = full_config self._config = full_config
self.name = name self.name = name
@ -299,7 +299,7 @@ class PairConfig(object):
return partial_sync return partial_sync
class CollectionConfig(object): class CollectionConfig:
def __init__(self, pair, name, config_a, config_b): def __init__(self, pair, name, config_a, config_b):
self.pair = pair self.pair = pair
self._config = pair._config self._config = pair._config

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import hashlib import hashlib
import json import json
import logging import logging
@ -224,7 +222,7 @@ def _print_collections(instance_name, get_discovered):
storage = storage_instance_from_config(args, create=False) storage = storage_instance_from_config(args, create=False)
displayname = storage.get_meta('displayname') displayname = storage.get_meta('displayname')
except Exception: except Exception:
displayname = u'' displayname = ''
logger.info(' - {}{}'.format( logger.info(' - {}{}'.format(
json.dumps(collection), json.dumps(collection),

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import logging import logging
import click import click

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import functools import functools
import json import json

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import contextlib import contextlib
import errno import errno
import importlib import importlib
@ -27,7 +25,7 @@ STATUS_PERMISSIONS = 0o600
STATUS_DIR_PERMISSIONS = 0o700 STATUS_DIR_PERMISSIONS = 0o700
class _StorageIndex(object): class _StorageIndex:
def __init__(self): def __init__(self):
self._storages = dict( self._storages = dict(
caldav='vdirsyncer.storage.dav.CalDAVStorage', caldav='vdirsyncer.storage.dav.CalDAVStorage',
@ -288,24 +286,24 @@ def handle_storage_init_error(cls, config):
if missing: if missing:
problems.append( problems.append(
u'{} storage requires the parameters: {}' '{} storage requires the parameters: {}'
.format(cls.storage_name, u', '.join(missing))) .format(cls.storage_name, ', '.join(missing)))
if invalid: if invalid:
problems.append( problems.append(
u'{} storage doesn\'t take the parameters: {}' '{} storage doesn\'t take the parameters: {}'
.format(cls.storage_name, u', '.join(invalid))) .format(cls.storage_name, ', '.join(invalid)))
if not problems: if not problems:
raise e raise e
raise exceptions.UserError( raise exceptions.UserError(
u'Failed to initialize {}'.format(config['instance_name']), 'Failed to initialize {}'.format(config['instance_name']),
problems=problems problems=problems
) )
class WorkerQueue(object): class WorkerQueue:
''' '''
A simple worker-queue setup. A simple worker-queue setup.

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
''' '''
Contains exception classes used by vdirsyncer. Not all exceptions are here, Contains exception classes used by vdirsyncer. Not all exceptions are here,
only the most commonly used ones. only the most commonly used ones.
@ -14,7 +13,7 @@ class Error(Exception):
raise TypeError('Invalid argument: {}'.format(key)) raise TypeError('Invalid argument: {}'.format(key))
setattr(self, key, value) setattr(self, key, value)
super(Error, self).__init__(*args) super().__init__(*args)
class UserError(Error, ValueError): class UserError(Error, ValueError):
@ -26,7 +25,7 @@ class UserError(Error, ValueError):
def __str__(self): def __str__(self):
msg = Error.__str__(self) msg = Error.__str__(self)
for problem in self.problems or (): for problem in self.problems or ():
msg += u'\n - {}'.format(problem) msg += '\n - {}'.format(problem)
return msg return msg

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import logging import logging
import requests import requests
@ -134,7 +133,7 @@ def request(method, url, session=None, latin1_fallback=True,
func = session.request func = session.request
logger.debug(u'{} {}'.format(method, url)) logger.debug('{} {}'.format(method, url))
logger.debug(kwargs.get('headers', {})) logger.debug(kwargs.get('headers', {}))
logger.debug(kwargs.get('data', None)) logger.debug(kwargs.get('data', None))
logger.debug('Sending request...') logger.debug('Sending request...')

View file

@ -16,12 +16,12 @@ class MetaSyncConflict(MetaSyncError):
def metasync(storage_a, storage_b, status, keys, conflict_resolution=None): def metasync(storage_a, storage_b, status, keys, conflict_resolution=None):
def _a_to_b(): def _a_to_b():
logger.info(u'Copying {} to {}'.format(key, storage_b)) logger.info('Copying {} to {}'.format(key, storage_b))
storage_b.set_meta(key, a) storage_b.set_meta(key, a)
status[key] = a status[key] = a
def _b_to_a(): def _b_to_a():
logger.info(u'Copying {} to {}'.format(key, storage_a)) logger.info('Copying {} to {}'.format(key, storage_a))
storage_a.set_meta(key, b) storage_a.set_meta(key, b)
status[key] = b status[key] = b
@ -45,10 +45,10 @@ def metasync(storage_a, storage_b, status, keys, conflict_resolution=None):
a = storage_a.get_meta(key) a = storage_a.get_meta(key)
b = storage_b.get_meta(key) b = storage_b.get_meta(key)
s = normalize_meta_value(status.get(key)) s = normalize_meta_value(status.get(key))
logger.debug(u'Key: {}'.format(key)) logger.debug('Key: {}'.format(key))
logger.debug(u'A: {}'.format(a)) logger.debug('A: {}'.format(a))
logger.debug(u'B: {}'.format(b)) logger.debug('B: {}'.format(b))
logger.debug(u'S: {}'.format(s)) logger.debug('S: {}'.format(s))
if a != s and b != s: if a != s and b != s:
_resolve_conflict() _resolve_conflict()

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import logging import logging
from os.path import basename from os.path import basename
@ -17,7 +15,7 @@ def repair_storage(storage, repair_unsafe_uid):
all_hrefs = list(storage.list()) all_hrefs = list(storage.list())
for i, (href, _) in enumerate(all_hrefs): for i, (href, _) in enumerate(all_hrefs):
item, etag = storage.get(href) item, etag = storage.get(href)
logger.info(u'[{}/{}] Processing {}' logger.info('[{}/{}] Processing {}'
.format(i, len(all_hrefs), href)) .format(i, len(all_hrefs), href))
try: try:

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
''' '''
There are storage classes which control the access to one vdir-collection and There are storage classes which control the access to one vdir-collection and
offer basic CRUD-ish methods for modifying those collections. The exact offer basic CRUD-ish methods for modifying those collections. The exact

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import contextlib import contextlib
import functools import functools
@ -20,7 +18,7 @@ class StorageMeta(type):
def __init__(cls, name, bases, d): def __init__(cls, name, bases, d):
for method in ('update', 'upload', 'delete'): for method in ('update', 'upload', 'delete'):
setattr(cls, method, mutating_storage_method(getattr(cls, method))) setattr(cls, method, mutating_storage_method(getattr(cls, method)))
return super(StorageMeta, cls).__init__(name, bases, d) return super().__init__(name, bases, d)
class Storage(metaclass=StorageMeta): class Storage(metaclass=StorageMeta):
@ -116,7 +114,7 @@ class Storage(metaclass=StorageMeta):
return '<{}(**{})>'.format( return '<{}(**{})>'.format(
self.__class__.__name__, self.__class__.__name__,
dict((x, getattr(self, x)) for x in self._repr_attributes) {x: getattr(self, x) for x in self._repr_attributes}
) )
def list(self): def list(self):

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import datetime import datetime
import logging import logging
import urllib.parse as urlparse import urllib.parse as urlparse
@ -140,7 +138,7 @@ def _fuzzy_matches_mimetype(strict, weak):
return False return False
class Discover(object): class Discover:
_namespace = None _namespace = None
_resourcetype = None _resourcetype = None
_homeset_xml = None _homeset_xml = None
@ -349,7 +347,7 @@ class CardDiscover(Discover):
_well_known_uri = '/.well-known/carddav' _well_known_uri = '/.well-known/carddav'
class DAVSession(object): class DAVSession:
''' '''
A helper class to connect to DAV servers. A helper class to connect to DAV servers.
''' '''
@ -423,7 +421,7 @@ class DAVStorage(Storage):
self.session, kwargs = \ self.session, kwargs = \
self.session_class.init_and_remaining_args(**kwargs) self.session_class.init_and_remaining_args(**kwargs)
super(DAVStorage, self).__init__(**kwargs) super().__init__(**kwargs)
import inspect import inspect
__init__.__signature__ = inspect.signature(session_class.__init__) __init__.__signature__ = inspect.signature(session_class.__init__)
@ -483,7 +481,7 @@ class DAVStorage(Storage):
.format(href)) .format(href))
continue continue
raw = raw.text or u'' raw = raw.text or ''
if isinstance(raw, bytes): if isinstance(raw, bytes):
raw = raw.decode(response.encoding) raw = raw.decode(response.encoding)
@ -617,7 +615,7 @@ class DAVStorage(Storage):
headers = self.session.get_default_headers() headers = self.session.get_default_headers()
headers['Depth'] = '1' headers['Depth'] = '1'
data = '''<?xml version="1.0" encoding="utf-8" ?> data = b'''<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:"> <D:propfind xmlns:D="DAV:">
<D:prop> <D:prop>
<D:resourcetype/> <D:resourcetype/>
@ -625,7 +623,7 @@ class DAVStorage(Storage):
<D:getetag/> <D:getetag/>
</D:prop> </D:prop>
</D:propfind> </D:propfind>
'''.encode('utf-8') '''
# We use a PROPFIND request instead of addressbook-query due to issues # We use a PROPFIND request instead of addressbook-query due to issues
# with Zimbra. See https://github.com/pimutils/vdirsyncer/issues/83 # with Zimbra. See https://github.com/pimutils/vdirsyncer/issues/83
@ -643,7 +641,7 @@ class DAVStorage(Storage):
except KeyError: except KeyError:
raise exceptions.UnsupportedMetadataError() raise exceptions.UnsupportedMetadataError()
xpath = '{%s}%s' % (namespace, tagname) xpath = '{{{}}}{}'.format(namespace, tagname)
data = '''<?xml version="1.0" encoding="utf-8" ?> data = '''<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:"> <D:propfind xmlns:D="DAV:">
<D:prop> <D:prop>
@ -668,7 +666,7 @@ class DAVStorage(Storage):
text = normalize_meta_value(getattr(prop, 'text', None)) text = normalize_meta_value(getattr(prop, 'text', None))
if text: if text:
return text return text
return u'' return ''
def set_meta(self, key, value): def set_meta(self, key, value):
try: try:
@ -676,7 +674,7 @@ class DAVStorage(Storage):
except KeyError: except KeyError:
raise exceptions.UnsupportedMetadataError() raise exceptions.UnsupportedMetadataError()
lxml_selector = '{%s}%s' % (namespace, tagname) lxml_selector = '{{{}}}{}'.format(namespace, tagname)
element = etree.Element(lxml_selector) element = etree.Element(lxml_selector)
element.text = normalize_meta_value(value) element.text = normalize_meta_value(value)
@ -730,7 +728,7 @@ class CalDAVStorage(DAVStorage):
def __init__(self, start_date=None, end_date=None, def __init__(self, start_date=None, end_date=None,
item_types=(), **kwargs): item_types=(), **kwargs):
super(CalDAVStorage, self).__init__(**kwargs) super().__init__(**kwargs)
if not isinstance(item_types, (list, tuple)): if not isinstance(item_types, (list, tuple)):
raise exceptions.UserError('item_types must be a list.') raise exceptions.UserError('item_types must be a list.')
@ -774,9 +772,8 @@ class CalDAVStorage(DAVStorage):
timefilter=timefilter) timefilter=timefilter)
else: else:
if start is not None and end is not None: if start is not None and end is not None:
for x in CalDAVStorage._get_list_filters(('VTODO', 'VEVENT'), yield from CalDAVStorage._get_list_filters(('VTODO', 'VEVENT'),
start, end): start, end)
yield x
def list(self): def list(self):
caldavfilters = list(self._get_list_filters( caldavfilters = list(self._get_list_filters(
@ -792,8 +789,7 @@ class CalDAVStorage(DAVStorage):
# instead? # instead?
# #
# See https://github.com/dmfs/tasks/issues/118 for backstory. # See https://github.com/dmfs/tasks/issues/118 for backstory.
for x in DAVStorage.list(self): yield from DAVStorage.list(self)
yield x
data = '''<?xml version="1.0" encoding="utf-8" ?> data = '''<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" <C:calendar-query xmlns:D="DAV:"

View file

@ -110,7 +110,7 @@ class EtesyncStorage(Storage):
raise ValueError('Collection argument required') raise ValueError('Collection argument required')
self._session = _Session(email, secrets_dir, server_url, db_path) self._session = _Session(email, secrets_dir, server_url, db_path)
super(EtesyncStorage, self).__init__(**kwargs) super().__init__(**kwargs)
self._journal = self._session.etesync.get(self.collection) self._journal = self._session.etesync.get(self.collection)
def _sync_journal(self): def _sync_journal(self):

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import errno import errno
import logging import logging
import os import os
@ -22,7 +20,7 @@ class FilesystemStorage(Storage):
def __init__(self, path, fileext, encoding='utf-8', post_hook=None, def __init__(self, path, fileext, encoding='utf-8', post_hook=None,
**kwargs): **kwargs):
super(FilesystemStorage, self).__init__(**kwargs) super().__init__(**kwargs)
path = expand_path(path) path = expand_path(path)
checkdir(path, create=False) checkdir(path, create=False)
self.path = path self.path = path
@ -174,7 +172,7 @@ class FilesystemStorage(Storage):
return normalize_meta_value(f.read().decode(self.encoding)) return normalize_meta_value(f.read().decode(self.encoding))
except OSError as e: except OSError as e:
if e.errno == errno.ENOENT: if e.errno == errno.ENOENT:
return u'' return ''
else: else:
raise raise

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import json import json
import logging import logging
import os import os
@ -123,7 +121,7 @@ class GoogleCalendarStorage(dav.CalDAVStorage):
if not kwargs.get('collection'): if not kwargs.get('collection'):
raise exceptions.CollectionRequired() raise exceptions.CollectionRequired()
super(GoogleCalendarStorage, self).__init__( super().__init__(
token_file=token_file, client_id=client_id, token_file=token_file, client_id=client_id,
client_secret=client_secret, start_date=start_date, client_secret=client_secret, start_date=start_date,
end_date=end_date, item_types=item_types, end_date=end_date, item_types=item_types,
@ -158,7 +156,7 @@ class GoogleContactsStorage(dav.CardDAVStorage):
if not kwargs.get('collection'): if not kwargs.get('collection'):
raise exceptions.CollectionRequired() raise exceptions.CollectionRequired()
super(GoogleContactsStorage, self).__init__( super().__init__(
token_file=token_file, client_id=client_id, token_file=token_file, client_id=client_id,
client_secret=client_secret, client_secret=client_secret,
**kwargs **kwargs

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import urllib.parse as urlparse import urllib.parse as urlparse
from .base import Storage from .base import Storage
@ -21,7 +19,7 @@ class HttpStorage(Storage):
def __init__(self, url, username='', password='', verify=True, auth=None, def __init__(self, url, username='', password='', verify=True, auth=None,
useragent=USERAGENT, verify_fingerprint=None, auth_cert=None, useragent=USERAGENT, verify_fingerprint=None, auth_cert=None,
**kwargs): **kwargs):
super(HttpStorage, self).__init__(**kwargs) super().__init__(**kwargs)
self._settings = { self._settings = {
'auth': prepare_auth(auth, username, password), 'auth': prepare_auth(auth, username, password),

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import random import random
from .base import Storage, normalize_meta_value from .base import Storage, normalize_meta_value
@ -26,7 +24,7 @@ class MemoryStorage(Storage):
self.items = {} # href => (etag, item) self.items = {} # href => (etag, item)
self.metadata = {} self.metadata = {}
self.fileext = fileext self.fileext = fileext
super(MemoryStorage, self).__init__(**kwargs) super().__init__(**kwargs)
def _get_href(self, item): def _get_href(self, item):
return item.ident + self.fileext return item.ident + self.fileext

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import collections import collections
import contextlib import contextlib
import functools import functools
@ -41,7 +39,7 @@ class SingleFileStorage(Storage):
_last_etag = None _last_etag = None
def __init__(self, path, encoding='utf-8', **kwargs): def __init__(self, path, encoding='utf-8', **kwargs):
super(SingleFileStorage, self).__init__(**kwargs) super().__init__(**kwargs)
path = os.path.abspath(expand_path(path)) path = os.path.abspath(expand_path(path))
checkfile(path, create=False) checkfile(path, create=False)
@ -70,9 +68,10 @@ class SingleFileStorage(Storage):
args['path'] = subpath args['path'] = subpath
collection_end = ( collection_end = (
placeholder_pos + placeholder_pos
2 + # length of '%s' + 2 # length of '%s'
len(subpath) - len(path) + len(subpath)
- len(path)
) )
collection = subpath[placeholder_pos:collection_end] collection = subpath[placeholder_pos:collection_end]
args['collection'] = collection args['collection'] = collection
@ -162,10 +161,11 @@ class SingleFileStorage(Storage):
def _write(self): def _write(self):
if self._last_etag is not None and \ if self._last_etag is not None and \
self._last_etag != get_etag_from_file(self.path): self._last_etag != get_etag_from_file(self.path):
raise exceptions.PreconditionFailed( raise exceptions.PreconditionFailed((
'Some other program modified the file {r!}. Re-run the ' 'Some other program modified the file {!r}. Re-run the '
'synchronization and make sure absolutely no other program is ' 'synchronization and make sure absolutely no other program is '
'writing into the same file.'.format(self.path)) 'writing into the same file.'
).format(self.path))
text = join_collection( text = join_collection(
item.raw for item, etag in self._items.values() item.raw for item, etag in self._items.values()
) )

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
''' '''
The `sync` function in `vdirsyncer.sync` can be called on two instances of The `sync` function in `vdirsyncer.sync` can be called on two instances of
`Storage` to synchronize them. Apart from the defined errors, this is the only `Storage` to synchronize them. Apart from the defined errors, this is the only
@ -24,7 +23,7 @@ from .exceptions import BothReadOnly, IdentAlreadyExists, PartialSync, \
sync_logger = logging.getLogger(__name__) sync_logger = logging.getLogger(__name__)
class _StorageInfo(object): class _StorageInfo:
'''A wrapper class that holds prefetched items, the status and other '''A wrapper class that holds prefetched items, the status and other
things.''' things.'''
def __init__(self, storage, status): def __init__(self, storage, status):
@ -74,9 +73,9 @@ class _StorageInfo(object):
new_meta = self.status.get_new(ident) new_meta = self.status.get_new(ident)
return ( return (
new_meta.etag != old_meta.etag and # etag changed new_meta.etag != old_meta.etag # etag changed
# item actually changed # item actually changed
(old_meta.hash is None or new_meta.hash != old_meta.hash) and (old_meta.hash is None or new_meta.hash != old_meta.hash)
) )
def set_item_cache(self, ident, item): def set_item_cache(self, ident, item):
@ -199,7 +198,7 @@ class Upload(Action):
if self.dest.storage.read_only: if self.dest.storage.read_only:
href = etag = None href = etag = None
else: else:
sync_logger.info(u'Copying (uploading) item {} to {}' sync_logger.info('Copying (uploading) item {} to {}'
.format(self.ident, self.dest.storage)) .format(self.ident, self.dest.storage))
href, etag = self.dest.storage.upload(self.item) href, etag = self.dest.storage.upload(self.item)
assert href is not None assert href is not None
@ -221,7 +220,7 @@ class Update(Action):
if self.dest.storage.read_only: if self.dest.storage.read_only:
meta = ItemMetadata(hash=self.item.hash) meta = ItemMetadata(hash=self.item.hash)
else: else:
sync_logger.info(u'Copying (updating) item {} to {}' sync_logger.info('Copying (updating) item {} to {}'
.format(self.ident, self.dest.storage)) .format(self.ident, self.dest.storage))
meta = self.dest.status.get_new(self.ident) meta = self.dest.status.get_new(self.ident)
meta.etag = \ meta.etag = \
@ -238,7 +237,7 @@ class Delete(Action):
def _run_impl(self, a, b): def _run_impl(self, a, b):
meta = self.dest.status.get_new(self.ident) meta = self.dest.status.get_new(self.ident)
if not self.dest.storage.read_only: if not self.dest.storage.read_only:
sync_logger.info(u'Deleting item {} from {}' sync_logger.info('Deleting item {} from {}'
.format(self.ident, self.dest.storage)) .format(self.ident, self.dest.storage))
self.dest.storage.delete(meta.href, meta.etag) self.dest.storage.delete(meta.href, meta.etag)
@ -251,14 +250,14 @@ class ResolveConflict(Action):
def run(self, a, b, conflict_resolution, partial_sync): def run(self, a, b, conflict_resolution, partial_sync):
with self.auto_rollback(a, b): with self.auto_rollback(a, b):
sync_logger.info(u'Doing conflict resolution for item {}...' sync_logger.info('Doing conflict resolution for item {}...'
.format(self.ident)) .format(self.ident))
meta_a = a.status.get_new(self.ident) meta_a = a.status.get_new(self.ident)
meta_b = b.status.get_new(self.ident) meta_b = b.status.get_new(self.ident)
if meta_a.hash == meta_b.hash: if meta_a.hash == meta_b.hash:
sync_logger.info(u'...same content on both sides.') sync_logger.info('...same content on both sides.')
elif conflict_resolution is None: elif conflict_resolution is None:
raise SyncConflict(ident=self.ident, href_a=meta_a.href, raise SyncConflict(ident=self.ident, href_a=meta_a.href,
href_b=meta_b.href) href_b=meta_b.href)

View file

@ -313,7 +313,7 @@ class SqliteStatus(_StatusBase):
return self._get_by_href_impl(*a, **kw) return self._get_by_href_impl(*a, **kw)
class SubStatus(object): class SubStatus:
def __init__(self, parent, side): def __init__(self, parent, side):
self.parent = parent self.parent = parent
assert side in 'ab' assert side in 'ab'

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import functools import functools
import os import os
import sys import sys
@ -154,7 +152,7 @@ def checkfile(path, create=False):
.format(path)) .format(path))
class cached_property(object): class cached_property:
'''A read-only @property that is only evaluated once. Only usable on class '''A read-only @property that is only evaluated once. Only usable on class
instances' methods. instances' methods.
''' '''
@ -212,8 +210,7 @@ def open_graphical_browser(url, new=0, autoraise=True):
emulator. emulator.
''' '''
import webbrowser import webbrowser
cli_names = set(['www-browser', 'links', 'links2', 'elinks', 'lynx', cli_names = {'www-browser', 'links', 'links2', 'elinks', 'lynx', 'w3m'}
'w3m'])
if webbrowser._tryorder is None: # Python 3.7 if webbrowser._tryorder is None: # Python 3.7
webbrowser.register_standard_browsers() webbrowser.register_standard_browsers()

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import hashlib import hashlib
from itertools import chain, tee from itertools import chain, tee
@ -34,7 +32,7 @@ IGNORE_PROPS = (
) )
class Item(object): class Item:
'''Immutable wrapper class for VCALENDAR (VEVENT, VTODO) and '''Immutable wrapper class for VCALENDAR (VEVENT, VTODO) and
VCARD''' VCARD'''
@ -117,7 +115,7 @@ def normalize_item(item, ignore_props=IGNORE_PROPS):
del x[prop] del x[prop]
x.props.sort() x.props.sort()
return u'\r\n'.join(filter(bool, (line.strip() for line in x.props))) return '\r\n'.join(filter(bool, (line.strip() for line in x.props)))
def _strip_timezones(item): def _strip_timezones(item):
@ -146,16 +144,16 @@ def split_collection(text):
for item in chain(items.values(), ungrouped_items): for item in chain(items.values(), ungrouped_items):
item.subcomponents.extend(inline) item.subcomponents.extend(inline)
yield u'\r\n'.join(item.dump_lines()) yield '\r\n'.join(item.dump_lines())
def _split_collection_impl(item, main, inline, items, ungrouped_items): def _split_collection_impl(item, main, inline, items, ungrouped_items):
if item.name == u'VTIMEZONE': if item.name == 'VTIMEZONE':
inline.append(item) inline.append(item)
elif item.name == u'VCARD': elif item.name == 'VCARD':
ungrouped_items.append(item) ungrouped_items.append(item)
elif item.name in (u'VTODO', u'VEVENT', u'VJOURNAL'): elif item.name in ('VTODO', 'VEVENT', 'VJOURNAL'):
uid = item.get(u'UID', u'') uid = item.get('UID', '')
wrapper = _Component(main.name, main.props[:], []) wrapper = _Component(main.name, main.props[:], [])
if uid.strip(): if uid.strip():
@ -164,7 +162,7 @@ def _split_collection_impl(item, main, inline, items, ungrouped_items):
ungrouped_items.append(wrapper) ungrouped_items.append(wrapper)
wrapper.subcomponents.append(item) wrapper.subcomponents.append(item)
elif item.name in (u'VCALENDAR', u'VADDRESSBOOK'): elif item.name in ('VCALENDAR', 'VADDRESSBOOK'):
if item.name == 'VCALENDAR': if item.name == 'VCALENDAR':
del item['METHOD'] del item['METHOD']
for subitem in item.subcomponents: for subitem in item.subcomponents:
@ -176,10 +174,10 @@ def _split_collection_impl(item, main, inline, items, ungrouped_items):
_default_join_wrappers = { _default_join_wrappers = {
u'VCALENDAR': u'VCALENDAR', 'VCALENDAR': 'VCALENDAR',
u'VEVENT': u'VCALENDAR', 'VEVENT': 'VCALENDAR',
u'VTODO': u'VCALENDAR', 'VTODO': 'VCALENDAR',
u'VCARD': u'VADDRESSBOOK' 'VCARD': 'VADDRESSBOOK'
} }
@ -207,16 +205,16 @@ def join_collection(items, wrappers=_default_join_wrappers):
if wrapper_type is not None: if wrapper_type is not None:
lines = chain(*( lines = chain(*(
[u'BEGIN:{}'.format(wrapper_type)], ['BEGIN:{}'.format(wrapper_type)],
# XXX: wrapper_props is a list of lines (with line-wrapping), so # XXX: wrapper_props is a list of lines (with line-wrapping), so
# filtering out duplicate lines will almost certainly break # filtering out duplicate lines will almost certainly break
# multiline-values. Since the only props we usually need to # multiline-values. Since the only props we usually need to
# support are PRODID and VERSION, I don't care. # support are PRODID and VERSION, I don't care.
uniq(wrapper_props), uniq(wrapper_props),
lines, lines,
[u'END:{}'.format(wrapper_type)] ['END:{}'.format(wrapper_type)]
)) ))
return u''.join(line + u'\r\n' for line in lines) return ''.join(line + '\r\n' for line in lines)
def _get_item_type(components, wrappers): def _get_item_type(components, wrappers):
@ -237,7 +235,7 @@ def _get_item_type(components, wrappers):
raise ValueError('Not sure how to join components.') raise ValueError('Not sure how to join components.')
class _Component(object): class _Component:
''' '''
Raw outline of the components. Raw outline of the components.
@ -277,10 +275,10 @@ class _Component(object):
rv = [] rv = []
try: try:
for _i, line in enumerate(lines): for _i, line in enumerate(lines):
if line.startswith(u'BEGIN:'): if line.startswith('BEGIN:'):
c_name = line[len(u'BEGIN:'):].strip().upper() c_name = line[len('BEGIN:'):].strip().upper()
stack.append(cls(c_name, [], [])) stack.append(cls(c_name, [], []))
elif line.startswith(u'END:'): elif line.startswith('END:'):
component = stack.pop() component = stack.pop()
if stack: if stack:
stack[-1].subcomponents.append(component) stack[-1].subcomponents.append(component)
@ -301,16 +299,14 @@ class _Component(object):
return rv[0] return rv[0]
def dump_lines(self): def dump_lines(self):
yield u'BEGIN:{}'.format(self.name) yield 'BEGIN:{}'.format(self.name)
for line in self.props: yield from self.props
yield line
for c in self.subcomponents: for c in self.subcomponents:
for line in c.dump_lines(): yield from c.dump_lines()
yield line yield 'END:{}'.format(self.name)
yield u'END:{}'.format(self.name)
def __delitem__(self, key): def __delitem__(self, key):
prefix = (u'{}:'.format(key), u'{};'.format(key)) prefix = ('{}:'.format(key), '{};'.format(key))
new_lines = [] new_lines = []
lineiter = iter(self.props) lineiter = iter(self.props)
while True: while True:
@ -323,7 +319,7 @@ class _Component(object):
break break
for line in lineiter: for line in lineiter:
if not line.startswith((u' ', u'\t')): if not line.startswith((' ', '\t')):
new_lines.append(line) new_lines.append(line)
break break
@ -331,9 +327,9 @@ class _Component(object):
def __setitem__(self, key, val): def __setitem__(self, key, val):
assert isinstance(val, str) assert isinstance(val, str)
assert u'\n' not in val assert '\n' not in val
del self[key] del self[key]
line = u'{}:{}'.format(key, val) line = '{}:{}'.format(key, val)
self.props.append(line) self.props.append(line)
def __contains__(self, obj): def __contains__(self, obj):
@ -360,7 +356,7 @@ class _Component(object):
raise KeyError() raise KeyError()
for line in iterlines: for line in iterlines:
if line.startswith((u' ', u'\t')): if line.startswith((' ', '\t')):
rv += line[1:] rv += line[1:]
else: else:
break break
@ -375,8 +371,8 @@ class _Component(object):
def __eq__(self, other): def __eq__(self, other):
return ( return (
isinstance(other, type(self)) and isinstance(other, type(self))
self.name == other.name and and self.name == other.name
self.props == other.props and and self.props == other.props
self.subcomponents == other.subcomponents and self.subcomponents == other.subcomponents
) )