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

View file

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

View file

@ -2,6 +2,26 @@
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/>`_
- `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/
.. 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
=================

View file

@ -1 +1 @@
.. include:: ../CHANGELOG.rst
.. include:: ../CHANGELOG.rst

View file

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
import datetime
import os
import setuptools_scm
from pkg_resources import get_distribution
extensions = ['sphinx.ext.autodoc']
@ -12,11 +10,11 @@ templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
project = u'vdirsyncer'
copyright = (u'2014-{}, Markus Unterwaditzer & contributors'
project = 'vdirsyncer'
copyright = ('2014-{}, Markus Unterwaditzer & contributors'
.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.
rst_epilog = '.. |vdirsyncer_version| replace:: %s' % release
@ -44,18 +42,18 @@ htmlhelp_basename = 'vdirsyncerdoc'
latex_elements = {}
latex_documents = [
('index', 'vdirsyncer.tex', u'vdirsyncer Documentation',
u'Markus Unterwaditzer', 'manual'),
('index', 'vdirsyncer.tex', 'vdirsyncer Documentation',
'Markus Unterwaditzer', 'manual'),
]
man_pages = [
('index', 'vdirsyncer', u'vdirsyncer Documentation',
[u'Markus Unterwaditzer'], 1)
('index', 'vdirsyncer', 'vdirsyncer Documentation',
['Markus Unterwaditzer'], 1)
]
texinfo_documents = [
('index', 'vdirsyncer', u'vdirsyncer Documentation',
u'Markus Unterwaditzer', 'vdirsyncer',
('index', 'vdirsyncer', 'vdirsyncer Documentation',
'Markus Unterwaditzer', 'vdirsyncer',
'Synchronize calendars and contacts.', 'Miscellaneous'),
]

View file

@ -286,7 +286,7 @@ Service to hardcode those into opensource software [googleterms]_:
3. In the sidebar, select "Credentials" and create a new "OAuth Client ID". The
application type is "Other".
You'll be prompted to create a OAuth consent screen first. Fill out that
form however you like.
@ -368,7 +368,7 @@ password. Neither are stored.
secrets_dir = ...
#server_path = ...
#db_path = ...
:param email: The email address of your account.
:param secrets_dir: A directory where vdirsyncer can store the encryption
key and authentication token.
@ -386,7 +386,7 @@ password. Neither are stored.
secrets_dir = ...
#server_path = ...
#db_path = ...
:param email: The email address of your account.
:param secrets_dir: A directory where vdirsyncer can store the encryption
key and authentication token.

View file

@ -60,7 +60,7 @@ passwords from the OS's password store. Installation::
Basic usage::
password.fetch = ["command", "keyring", "get", "example.com", "foouser"]
.. _keyring: https://github.com/jaraco/keyring/
Password Prompt

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
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
----------
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.
@ -77,7 +77,7 @@ Then search for the for the vCard in the folder **~/.contacts/**. Click
ok, and you we will see your contacts.
.. note::
Claws-Mail shows only contacts that have a mail address.
Crontab

View file

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

View file

@ -39,7 +39,7 @@ program chosen:
* Like with ``todo.txt``, Dropbox and friends are obviously agnostic/unaware of
the files' contents. If a file has changed on both sides, Dropbox just copies
both versions to both sides.
This is a good idea if the user is directly interfacing with the file system
and is able to resolve conflicts themselves. Here it might lead to
erroneous behavior with e.g. ``khal``, since there are now two events with

View file

@ -9,7 +9,7 @@ ARG distrover
RUN apt-get update
RUN apt-get install -y build-essential fakeroot debhelper git
RUN apt-get install -y python3-all python3-pip
RUN apt-get install -y ruby ruby-dev
RUN apt-get install -y ruby ruby-dev
RUN apt-get install -y python-all python-pip
RUN gem install fpm

View file

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

View file

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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
'''
Vdirsyncer synchronizes calendars and contacts.
@ -18,14 +17,7 @@ requirements = [
# https://github.com/pimutils/vdirsyncer/issues/478
'click-threading>=0.2',
# !=2.9.0: https://github.com/kennethreitz/requests/issues/2930
#
# >=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',
'requests >=2.20.0',
# https://github.com/sigmavirus24/requests-toolbelt/pull/28
# 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-localserver
pytest-subtesthack

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import uuid
import os
@ -27,7 +25,7 @@ class DAVStorageTests(ServerMixin, StorageTests):
@pytest.mark.skipif(dav_server == 'radicale',
reason='Radicale is very tolerant.')
def test_dav_broken_item(self, s):
item = Item(u'HAHA:YES')
item = Item('HAHA:YES')
with pytest.raises((exceptions.Error, requests.exceptions.HTTPError)):
s.upload(item)
assert not list(s.list())
@ -50,7 +48,7 @@ class DAVStorageTests(ServerMixin, StorageTests):
monkeypatch.setattr(s, '_get_href',
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)
item2, etag2 = s.get(href)
assert_item_equals(item, item2)

View file

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

View file

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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import shutil
import os
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)
class ServerMixin(object):
class ServerMixin:
@pytest.fixture
def davical_args(self):
if self.storage_class.fileext == '.ics':

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import sys
import logging
@ -20,7 +18,7 @@ def test_get_storage_init_args():
from vdirsyncer.storage.memory import 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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import hypothesis.strategies as st
from hypothesis import example, given
@ -130,7 +128,7 @@ metadata = st.dictionaries(keys, values)
status=metadata, keys=st.sets(keys),
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')
@example(a={'0': '0'}, b={'0': '1'}, status={'0': '0'}, keys={'0'},
conflict_resolution='a wins')
@ -144,9 +142,9 @@ def test_fuzzing(a, b, status, keys, conflict_resolution):
b = _get_storage(b, 'B')
winning_storage = (a if conflict_resolution == 'a wins' else b)
expected_values = dict((key, winning_storage.get_meta(key))
for key in keys
if key not in status)
expected_values = {key: winning_storage.get_meta(key)
for key in keys
if key not in status}
metasync(a, b, status,
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
@ -11,17 +11,18 @@ from vdirsyncer.vobject import Item
@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):
s = MemoryStorage()
s.items = {
'one': (
'asdf',
Item(u'BEGIN:VCARD\nFN:Hans\nUID:{}\nEND:VCARD'.format(uid))
Item('BEGIN:VCARD\nFN:Hans\nUID:{}\nEND:VCARD'.format(uid))
),
'two': (
'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)))
@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):
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)
assert s.get(href)[0].uid == uid
assert not href_safe(uid)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import contextlib
import errno
import importlib
@ -27,7 +25,7 @@ STATUS_PERMISSIONS = 0o600
STATUS_DIR_PERMISSIONS = 0o700
class _StorageIndex(object):
class _StorageIndex:
def __init__(self):
self._storages = dict(
caldav='vdirsyncer.storage.dav.CalDAVStorage',
@ -288,24 +286,24 @@ def handle_storage_init_error(cls, config):
if missing:
problems.append(
u'{} storage requires the parameters: {}'
.format(cls.storage_name, u', '.join(missing)))
'{} storage requires the parameters: {}'
.format(cls.storage_name, ', '.join(missing)))
if invalid:
problems.append(
u'{} storage doesn\'t take the parameters: {}'
.format(cls.storage_name, u', '.join(invalid)))
'{} storage doesn\'t take the parameters: {}'
.format(cls.storage_name, ', '.join(invalid)))
if not problems:
raise e
raise exceptions.UserError(
u'Failed to initialize {}'.format(config['instance_name']),
'Failed to initialize {}'.format(config['instance_name']),
problems=problems
)
class WorkerQueue(object):
class WorkerQueue:
'''
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,
only the most commonly used ones.
@ -14,7 +13,7 @@ class Error(Exception):
raise TypeError('Invalid argument: {}'.format(key))
setattr(self, key, value)
super(Error, self).__init__(*args)
super().__init__(*args)
class UserError(Error, ValueError):
@ -26,7 +25,7 @@ class UserError(Error, ValueError):
def __str__(self):
msg = Error.__str__(self)
for problem in self.problems or ():
msg += u'\n - {}'.format(problem)
msg += '\n - {}'.format(problem)
return msg

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import logging
import requests
@ -134,7 +133,7 @@ def request(method, url, session=None, latin1_fallback=True,
func = session.request
logger.debug(u'{} {}'.format(method, url))
logger.debug('{} {}'.format(method, url))
logger.debug(kwargs.get('headers', {}))
logger.debug(kwargs.get('data', None))
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 _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)
status[key] = 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)
status[key] = b
@ -45,10 +45,10 @@ def metasync(storage_a, storage_b, status, keys, conflict_resolution=None):
a = storage_a.get_meta(key)
b = storage_b.get_meta(key)
s = normalize_meta_value(status.get(key))
logger.debug(u'Key: {}'.format(key))
logger.debug(u'A: {}'.format(a))
logger.debug(u'B: {}'.format(b))
logger.debug(u'S: {}'.format(s))
logger.debug('Key: {}'.format(key))
logger.debug('A: {}'.format(a))
logger.debug('B: {}'.format(b))
logger.debug('S: {}'.format(s))
if a != s and b != s:
_resolve_conflict()

View file

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

View file

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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import contextlib
import functools
@ -20,7 +18,7 @@ class StorageMeta(type):
def __init__(cls, name, bases, d):
for method in ('update', 'upload', 'delete'):
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):
@ -116,7 +114,7 @@ class Storage(metaclass=StorageMeta):
return '<{}(**{})>'.format(
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):

View file

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

View file

@ -110,7 +110,7 @@ class EtesyncStorage(Storage):
raise ValueError('Collection argument required')
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)
def _sync_journal(self):

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import collections
import contextlib
import functools
@ -41,7 +39,7 @@ class SingleFileStorage(Storage):
_last_etag = None
def __init__(self, path, encoding='utf-8', **kwargs):
super(SingleFileStorage, self).__init__(**kwargs)
super().__init__(**kwargs)
path = os.path.abspath(expand_path(path))
checkfile(path, create=False)
@ -70,9 +68,10 @@ class SingleFileStorage(Storage):
args['path'] = subpath
collection_end = (
placeholder_pos +
2 + # length of '%s'
len(subpath) - len(path)
placeholder_pos
+ 2 # length of '%s'
+ len(subpath)
- len(path)
)
collection = subpath[placeholder_pos:collection_end]
args['collection'] = collection
@ -162,10 +161,11 @@ class SingleFileStorage(Storage):
def _write(self):
if self._last_etag is not None and \
self._last_etag != get_etag_from_file(self.path):
raise exceptions.PreconditionFailed(
'Some other program modified the file {r!}. Re-run the '
raise exceptions.PreconditionFailed((
'Some other program modified the file {!r}. Re-run the '
'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(
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
`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__)
class _StorageInfo(object):
class _StorageInfo:
'''A wrapper class that holds prefetched items, the status and other
things.'''
def __init__(self, storage, status):
@ -74,9 +73,9 @@ class _StorageInfo(object):
new_meta = self.status.get_new(ident)
return (
new_meta.etag != old_meta.etag and # etag changed
new_meta.etag != old_meta.etag # etag 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):
@ -199,7 +198,7 @@ class Upload(Action):
if self.dest.storage.read_only:
href = etag = None
else:
sync_logger.info(u'Copying (uploading) item {} to {}'
sync_logger.info('Copying (uploading) item {} to {}'
.format(self.ident, self.dest.storage))
href, etag = self.dest.storage.upload(self.item)
assert href is not None
@ -221,7 +220,7 @@ class Update(Action):
if self.dest.storage.read_only:
meta = ItemMetadata(hash=self.item.hash)
else:
sync_logger.info(u'Copying (updating) item {} to {}'
sync_logger.info('Copying (updating) item {} to {}'
.format(self.ident, self.dest.storage))
meta = self.dest.status.get_new(self.ident)
meta.etag = \
@ -238,7 +237,7 @@ class Delete(Action):
def _run_impl(self, a, b):
meta = self.dest.status.get_new(self.ident)
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))
self.dest.storage.delete(meta.href, meta.etag)
@ -251,14 +250,14 @@ class ResolveConflict(Action):
def run(self, a, b, conflict_resolution, partial_sync):
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))
meta_a = a.status.get_new(self.ident)
meta_b = b.status.get_new(self.ident)
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:
raise SyncConflict(ident=self.ident, href_a=meta_a.href,
href_b=meta_b.href)

View file

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

View file

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

View file

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