Merge pull request #830 from pimutils/next

Keep moving forward
This commit is contained in:
Hugo Barrera 2020-06-10 19:48:15 +00:00 committed by GitHub
commit 68ff37e677
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 286 additions and 303 deletions

View file

@ -14,6 +14,10 @@ repos:
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-import-order, flake8-bugbear] additional_dependencies: [flake8-import-order, flake8-bugbear]
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.3.0
hooks:
- id: reorder-python-imports
- repo: local - repo: local
hooks: hooks:
- id: update-travis - id: update-travis

View file

@ -19,74 +19,22 @@
"include": [ "include": [
{ {
"env": "BUILD=style", "env": "BUILD=style",
"python": "3.6" "python": "3.7"
}, },
{ {
"env": "BUILD=test REQUIREMENTS=release", "env": "BUILD=test REQUIREMENTS=release",
"python": "3.5" "python": "3.7"
},
{
"dist": "trusty",
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=release ",
"python": "3.5"
},
{
"dist": "trusty",
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=release ",
"python": "3.5"
},
{
"env": "BUILD=test REQUIREMENTS=minimal",
"python": "3.5"
},
{
"dist": "trusty",
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=minimal ",
"python": "3.5"
},
{
"dist": "trusty",
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=minimal ",
"python": "3.5"
},
{
"env": "BUILD=test REQUIREMENTS=release",
"python": "3.6"
}, },
{ {
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=release ", "env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=release ",
"python": "3.6" "python": "3.7"
}, },
{ {
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=release ", "env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=release ",
"python": "3.6" "python": "3.7"
}, },
{ {
"env": "BUILD=test-storage DAV_SERVER=fastmail REQUIREMENTS=release ", "env": "BUILD=test-storage DAV_SERVER=fastmail REQUIREMENTS=release ",
"python": "3.6"
},
{
"env": "BUILD=test REQUIREMENTS=minimal",
"python": "3.6"
},
{
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=minimal ",
"python": "3.6"
},
{
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=minimal ",
"python": "3.6"
},
{
"env": "BUILD=test REQUIREMENTS=release",
"python": "3.7"
},
{
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=release ",
"python": "3.7"
},
{
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=release ",
"python": "3.7" "python": "3.7"
}, },
{ {
@ -127,7 +75,7 @@
}, },
{ {
"env": "BUILD=test ETESYNC_TESTS=true REQUIREMENTS=latest", "env": "BUILD=test ETESYNC_TESTS=true REQUIREMENTS=latest",
"python": "3.6" "python": "3.7"
} }
] ]
}, },

View file

@ -30,11 +30,6 @@ PYTEST_ARGS =
TEST_EXTRA_PACKAGES = TEST_EXTRA_PACKAGES =
ifeq ($(COVERAGE), true)
TEST_EXTRA_PACKAGES += pytest-cov
PYTEST_ARGS += --cov-config .coveragerc --cov vdirsyncer
endif
ifeq ($(ETESYNC_TESTS), true) ifeq ($(ETESYNC_TESTS), true)
TEST_EXTRA_PACKAGES += git+https://github.com/etesync/journal-manager@v0.5.2 TEST_EXTRA_PACKAGES += git+https://github.com/etesync/journal-manager@v0.5.2
TEST_EXTRA_PACKAGES += django djangorestframework==3.8.2 wsgi_intercept drf-nested-routers TEST_EXTRA_PACKAGES += django djangorestframework==3.8.2 wsgi_intercept drf-nested-routers
@ -106,9 +101,6 @@ docs:
linkcheck: linkcheck:
sphinx-build -W -b linkcheck ./docs/ ./docs/_build/linkcheck/ sphinx-build -W -b linkcheck ./docs/ ./docs/_build/linkcheck/
release:
python setup.py sdist bdist_wheel upload
release-deb: release-deb:
sh scripts/release-deb.sh debian jessie sh scripts/release-deb.sh debian jessie
sh scripts/release-deb.sh debian stretch sh scripts/release-deb.sh debian stretch

View file

@ -65,7 +65,7 @@ def github_issue_role(name, rawtext, text, lineno, inliner,
if issue_num <= 0: if issue_num <= 0:
raise ValueError() raise ValueError()
except ValueError: except ValueError:
msg = inliner.reporter.error('Invalid GitHub issue: {}'.format(text), msg = inliner.reporter.error(f'Invalid GitHub issue: {text}',
line=lineno) line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg) prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg] return [prb], [msg]

View file

@ -41,7 +41,7 @@ If your distribution doesn't provide a package for vdirsyncer, you still can
use Python's package manager "pip". First, you'll have to check that the use Python's package manager "pip". First, you'll have to check that the
following things are installed: following things are installed:
- Python 3.5+ and pip. - Python 3.7+ and pip.
- ``libxml`` and ``libxslt`` - ``libxml`` and ``libxslt``
- ``zlib`` - ``zlib``
- Linux or OS X. **Windows is not supported**, see :gh:`535`. - Linux or OS X. **Windows is not supported**, see :gh:`535`.

View file

@ -1,10 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
import itertools import itertools
import json import json
python_versions = ("3.5", "3.6", "3.7", "3.8") python_versions = ["3.7", "3.8"]
latest_python = "3.6"
cfg = {} cfg = {}
@ -34,7 +32,7 @@ matrix = []
cfg['matrix'] = {'include': matrix, 'fast_finish': True} cfg['matrix'] = {'include': matrix, 'fast_finish': True}
matrix.append({ matrix.append({
'python': latest_python, 'python': python_versions[0],
'env': 'BUILD=style' 'env': 'BUILD=style'
}) })
@ -51,7 +49,7 @@ for python, requirements in itertools.product(
'env': f"BUILD=test REQUIREMENTS={requirements}", 'env': f"BUILD=test REQUIREMENTS={requirements}",
}) })
if python == latest_python and requirements == "release": if python == python_versions[0] and requirements == "release":
dav_servers += ("fastmail",) dav_servers += ("fastmail",)
for dav_server in dav_servers: for dav_server in dav_servers:
@ -61,8 +59,6 @@ for python, requirements in itertools.product(
f"DAV_SERVER={dav_server} " f"DAV_SERVER={dav_server} "
f"REQUIREMENTS={requirements} ") f"REQUIREMENTS={requirements} ")
} }
if python == '3.5':
job['dist'] = 'trusty'
if dav_server in ("davical", "icloud"): if dav_server in ("davical", "icloud"):
job['if'] = 'NOT (type IN (pull_request))' job['if'] = 'NOT (type IN (pull_request))'
@ -70,7 +66,7 @@ for python, requirements in itertools.product(
matrix.append(job) matrix.append(job)
matrix.append({ matrix.append({
'python': latest_python, 'python': python_versions[0],
'env': ("BUILD=test " 'env': ("BUILD=test "
"ETESYNC_TESTS=true " "ETESYNC_TESTS=true "
"REQUIREMENTS=latest") "REQUIREMENTS=latest")

View file

@ -3,7 +3,12 @@ universal = 1
[tool:pytest] [tool:pytest]
norecursedirs = tests/storage/servers/* norecursedirs = tests/storage/servers/*
addopts = --tb=short addopts =
--tb=short
--cov-config .coveragerc
--cov=vdirsyncer
--cov-report=term-missing
--no-cov-on-fail
[flake8] [flake8]
# E731: Use a def instead of lambda expr # E731: Use a def instead of lambda expr

View file

@ -4,14 +4,14 @@ Vdirsyncer synchronizes calendars and contacts.
Please refer to https://vdirsyncer.pimutils.org/en/stable/packaging.html for Please refer to https://vdirsyncer.pimutils.org/en/stable/packaging.html for
how to package vdirsyncer. how to package vdirsyncer.
''' '''
from setuptools import Command
from setuptools import find_packages
from setuptools import Command, find_packages, setup from setuptools import setup
requirements = [ requirements = [
# https://github.com/mitsuhiko/click/issues/200 # https://github.com/mitsuhiko/click/issues/200
'click>=5.0,<6.0', 'click>=5.0',
'click-log>=0.3.0, <0.4.0', 'click-log>=0.3.0, <0.4.0',
# https://github.com/pimutils/vdirsyncer/issues/478 # https://github.com/pimutils/vdirsyncer/issues/478
@ -87,8 +87,6 @@ setup(
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: POSIX', 'Operating System :: POSIX',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Topic :: Internet', 'Topic :: Internet',

View file

@ -1,4 +1,5 @@
hypothesis>=5.0.0 hypothesis>=5.0.0
pytest pytest
pytest-cov
pytest-localserver pytest-localserver
pytest-subtesthack pytest-subtesthack

View file

@ -1,14 +1,11 @@
''' '''
Test suite for vdirsyncer. Test suite for vdirsyncer.
''' '''
import hypothesis.strategies as st import hypothesis.strategies as st
import urllib3.exceptions
from vdirsyncer.vobject import normalize_item from vdirsyncer.vobject import normalize_item
import urllib3
import urllib3.exceptions
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

View file

@ -5,10 +5,10 @@ import logging
import os import os
import click_log import click_log
from hypothesis import HealthCheck, Verbosity, settings
import pytest import pytest
from hypothesis import HealthCheck
from hypothesis import settings
from hypothesis import Verbosity
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)

View file

@ -1,25 +1,27 @@
import random import random
import uuid
import textwrap import textwrap
from urllib.parse import quote as urlquote, unquote as urlunquote import uuid
from urllib.parse import quote as urlquote
from urllib.parse import unquote as urlunquote
import hypothesis.strategies as st import hypothesis.strategies as st
import pytest
from hypothesis import given from hypothesis import given
import pytest from .. import assert_item_equals
from .. import EVENT_TEMPLATE
from .. import normalize_item
from .. import printable_characters_strategy
from .. import TASK_TEMPLATE
from .. import VCARD_TEMPLATE
from vdirsyncer import exceptions from vdirsyncer import exceptions
from vdirsyncer.storage.base import normalize_meta_value from vdirsyncer.storage.base import normalize_meta_value
from vdirsyncer.vobject import Item from vdirsyncer.vobject import Item
from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \
assert_item_equals, normalize_item, printable_characters_strategy
def get_server_mixin(server_name): def get_server_mixin(server_name):
from . import __name__ as base from . import __name__ as base
x = __import__('{}.servers.{}'.format(base, server_name), fromlist=['']) x = __import__(f'{base}.servers.{server_name}', fromlist=[''])
return x.ServerMixin return x.ServerMixin
@ -183,7 +185,7 @@ class StorageTests:
def test_discover(self, requires_collections, get_storage_args, get_item): def test_discover(self, requires_collections, get_storage_args, get_item):
collections = set() collections = set()
for i in range(1, 5): for i in range(1, 5):
collection = 'test{}'.format(i) collection = f'test{i}'
s = self.storage_class(**get_storage_args(collection=collection)) s = self.storage_class(**get_storage_args(collection=collection))
assert not list(s.list()) assert not list(s.list())
s.upload(get_item()) s.upload(get_item())

View file

@ -1,7 +1,7 @@
import pytest
import uuid import uuid
import pytest
@pytest.fixture @pytest.fixture
def slow_create_collection(request): def slow_create_collection(request):

View file

@ -1,19 +1,15 @@
import os
import uuid import uuid
import os
import pytest import pytest
import requests
import requests.exceptions import requests.exceptions
from .. import get_server_mixin
from .. import StorageTests
from tests import assert_item_equals from tests import assert_item_equals
from vdirsyncer import exceptions from vdirsyncer import exceptions
from vdirsyncer.vobject import Item from vdirsyncer.vobject import Item
from .. import StorageTests, get_server_mixin
dav_server = os.environ.get('DAV_SERVER', 'skip') dav_server = os.environ.get('DAV_SERVER', 'skip')
ServerMixin = get_server_mixin(dav_server) ServerMixin = get_server_mixin(dav_server)

View file

@ -2,18 +2,17 @@ import datetime
from textwrap import dedent from textwrap import dedent
import pytest import pytest
import requests
import requests.exceptions import requests.exceptions
from tests import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE from . import dav_server
from . import DAVStorageTests
from .. import format_item
from tests import EVENT_TEMPLATE
from tests import TASK_TEMPLATE
from tests import VCARD_TEMPLATE
from vdirsyncer import exceptions from vdirsyncer import exceptions
from vdirsyncer.storage.dav import CalDAVStorage from vdirsyncer.storage.dav import CalDAVStorage
from . import DAVStorageTests, dav_server
from .. import format_item
class TestCalDAVStorage(DAVStorageTests): class TestCalDAVStorage(DAVStorageTests):
storage_class = CalDAVStorage storage_class = CalDAVStorage

View file

@ -1,8 +1,7 @@
import pytest import pytest
from vdirsyncer.storage.dav import CardDAVStorage
from . import DAVStorageTests from . import DAVStorageTests
from vdirsyncer.storage.dav import CardDAVStorage
class TestCardDAVStorage(DAVStorageTests): class TestCardDAVStorage(DAVStorageTests):

View file

@ -1,6 +1,8 @@
import pytest import pytest
from vdirsyncer.storage.dav import _BAD_XML_CHARS, _merge_xml, _parse_xml from vdirsyncer.storage.dav import _BAD_XML_CHARS
from vdirsyncer.storage.dav import _merge_xml
from vdirsyncer.storage.dav import _parse_xml
def test_xml_utilities(): def test_xml_utilities():

View file

@ -9,7 +9,6 @@ https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/ https://docs.djangoproject.com/en/1.10/ref/settings/
""" """
import os import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)

View file

@ -13,11 +13,10 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include 1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
""" """
from django.conf.urls import include, url from django.conf.urls import include
from django.conf.urls import url
from rest_framework_nested import routers
from journal import views from journal import views
from rest_framework_nested import routers
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'journals', views.JournalViewSet) router.register(r'journals', views.JournalViewSet)

View file

@ -6,7 +6,6 @@ It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
""" """
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application

View file

@ -1,14 +1,13 @@
import shutil
import os import os
import shutil
import sys import sys
import pytest import pytest
import requests import requests
from vdirsyncer.storage.etesync import EtesyncContacts, EtesyncCalendars
from .. import StorageTests from .. import StorageTests
from vdirsyncer.storage.etesync import EtesyncCalendars
from vdirsyncer.storage.etesync import EtesyncContacts
pytestmark = pytest.mark.skipif(os.getenv('ETESYNC_TESTS', '') != 'true', pytestmark = pytest.mark.skipif(os.getenv('ETESYNC_TESTS', '') != 'true',

View file

@ -1,7 +1,8 @@
import os import os
import pytest
import uuid import uuid
import pytest
try: try:
caldav_args = { caldav_args = {
# Those credentials are configured through the Travis UI # Those credentials are configured through the Travis UI

View file

@ -1,10 +1,9 @@
import os import os
import shutil
import subprocess import subprocess
import time import time
import shutil
import pytest import pytest
import requests import requests
testserver_repo = os.path.dirname(__file__) testserver_repo = os.path.dirname(__file__)

View file

@ -2,11 +2,10 @@ import subprocess
import pytest import pytest
from . import StorageTests
from vdirsyncer.storage.filesystem import FilesystemStorage from vdirsyncer.storage.filesystem import FilesystemStorage
from vdirsyncer.vobject import Item from vdirsyncer.vobject import Item
from . import StorageTests
class TestFilesystemStorage(StorageTests): class TestFilesystemStorage(StorageTests):
storage_class = FilesystemStorage storage_class = FilesystemStorage

View file

@ -1,11 +1,10 @@
import pytest import pytest
from requests import Response from requests import Response
from tests import normalize_item from tests import normalize_item
from vdirsyncer.exceptions import UserError from vdirsyncer.exceptions import UserError
from vdirsyncer.storage.http import HttpStorage, prepare_auth from vdirsyncer.storage.http import HttpStorage
from vdirsyncer.storage.http import prepare_auth
def test_list(monkeypatch): def test_list(monkeypatch):

View file

@ -1,13 +1,11 @@
import pytest import pytest
from requests import Response from requests import Response
import vdirsyncer.storage.http import vdirsyncer.storage.http
from . import StorageTests
from vdirsyncer.storage.base import Storage from vdirsyncer.storage.base import Storage
from vdirsyncer.storage.singlefile import SingleFileStorage from vdirsyncer.storage.singlefile import SingleFileStorage
from . import StorageTests
class CombinedStorage(Storage): class CombinedStorage(Storage):
'''A subclass of HttpStorage to make testing easier. It supports writes via '''A subclass of HttpStorage to make testing easier. It supports writes via

View file

@ -1,8 +1,7 @@
import pytest import pytest
from vdirsyncer.storage.memory import MemoryStorage
from . import StorageTests from . import StorageTests
from vdirsyncer.storage.memory import MemoryStorage
class TestMemoryStorage(StorageTests): class TestMemoryStorage(StorageTests):

View file

@ -1,8 +1,7 @@
import pytest import pytest
from vdirsyncer.storage.singlefile import SingleFileStorage
from . import StorageTests from . import StorageTests
from vdirsyncer.storage.singlefile import SingleFileStorage
class TestSingleFileStorage(StorageTests): class TestSingleFileStorage(StorageTests):

View file

@ -1,8 +1,7 @@
from textwrap import dedent from textwrap import dedent
from click.testing import CliRunner
import pytest import pytest
from click.testing import CliRunner
import vdirsyncer.cli as cli import vdirsyncer.cli as cli

View file

@ -3,7 +3,8 @@ from textwrap import dedent
import pytest import pytest
from vdirsyncer import cli, exceptions from vdirsyncer import cli
from vdirsyncer import exceptions
from vdirsyncer.cli.config import Config from vdirsyncer.cli.config import Config

View file

@ -19,7 +19,7 @@ def storage(tmpdir, runner):
def test_basic(storage, runner, collection): def test_basic(storage, runner, collection):
if collection is not None: if collection is not None:
storage = storage.mkdir(collection) storage = storage.mkdir(collection)
collection_arg = 'foo/{}'.format(collection) collection_arg = f'foo/{collection}'
else: else:
collection_arg = 'foo' collection_arg = 'foo'

View file

@ -3,9 +3,9 @@ import sys
from textwrap import dedent from textwrap import dedent
import hypothesis.strategies as st import hypothesis.strategies as st
from hypothesis import example, given
import pytest import pytest
from hypothesis import example
from hypothesis import given
def test_simple_run(tmpdir, runner): def test_simple_run(tmpdir, runner):
@ -123,7 +123,10 @@ def test_verbosity(tmpdir, runner):
runner.write_with_general('') runner.write_with_general('')
result = runner.invoke(['--verbosity=HAHA', 'sync']) result = runner.invoke(['--verbosity=HAHA', 'sync'])
assert result.exception assert result.exception
assert 'invalid value for "--verbosity"' in result.output.lower() assert (
'invalid value for "--verbosity"' in result.output.lower()
or "invalid value for '--verbosity'" in result.output.lower()
)
def test_collections_cache_invalidation(tmpdir, runner): def test_collections_cache_invalidation(tmpdir, runner):
@ -461,7 +464,7 @@ def test_partial_sync(tmpdir, runner, partial_sync):
fileext = ".txt" fileext = ".txt"
path = "{base}/bar" path = "{base}/bar"
'''.format( '''.format(
partial_sync=('partial_sync = "{}"\n'.format(partial_sync) partial_sync=(f'partial_sync = "{partial_sync}"\n'
if partial_sync else ''), if partial_sync else ''),
base=str(tmpdir) base=str(tmpdir)
))) )))

View file

@ -1,6 +1,7 @@
from vdirsyncer import exceptions from vdirsyncer import exceptions
from vdirsyncer.cli.utils import handle_cli_error, \ from vdirsyncer.cli.utils import handle_cli_error
storage_instance_from_config, storage_names from vdirsyncer.cli.utils import storage_instance_from_config
from vdirsyncer.cli.utils import storage_names
def test_handle_cli_error(capsys): def test_handle_cli_error(capsys):

View file

@ -1,11 +1,12 @@
import sys
import logging import logging
import sys
import click_log import click_log
import pytest import pytest
import requests import requests
from vdirsyncer import http, utils from vdirsyncer import http
from vdirsyncer import utils
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)

View file

@ -1,10 +1,10 @@
import hypothesis.strategies as st import hypothesis.strategies as st
import pytest
from hypothesis import given from hypothesis import given
import pytest
from vdirsyncer import exceptions from vdirsyncer import exceptions
from vdirsyncer.cli.fetchparams import STRATEGIES, expand_fetch_params from vdirsyncer.cli.fetchparams import expand_fetch_params
from vdirsyncer.cli.fetchparams import STRATEGIES
@pytest.fixture @pytest.fixture
@ -47,7 +47,7 @@ def test_key_conflict(monkeypatch, mystrategy):
@given(s=st.text(), t=st.text(min_size=1)) @given(s=st.text(), t=st.text(min_size=1))
def test_fuzzing(s, t, mystrategy): def test_fuzzing(s, t, mystrategy):
config = expand_fetch_params({ config = expand_fetch_params({
'{}.fetch'.format(s): ['mystrategy', t] f'{s}.fetch': ['mystrategy', t]
}) })
assert config[s] == t assert config[s] == t

View file

@ -1,7 +1,7 @@
import pytest
from hypothesis import assume, given
import hypothesis.strategies as st import hypothesis.strategies as st
import pytest
from hypothesis import assume
from hypothesis import given
from vdirsyncer.sync.status import SqliteStatus from vdirsyncer.sync.status import SqliteStatus

View file

@ -1,17 +1,22 @@
from copy import deepcopy from copy import deepcopy
import hypothesis.strategies as st import hypothesis.strategies as st
from hypothesis import assume
from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule
import pytest import pytest
from hypothesis import assume
from hypothesis.stateful import Bundle
from hypothesis.stateful import rule
from hypothesis.stateful import RuleBasedStateMachine
from tests import blow_up, uid_strategy from tests import blow_up
from tests import uid_strategy
from vdirsyncer.storage.memory import MemoryStorage, _random_string from vdirsyncer.storage.memory import _random_string
from vdirsyncer.storage.memory import MemoryStorage
from vdirsyncer.sync import sync as _sync from vdirsyncer.sync import sync as _sync
from vdirsyncer.sync.exceptions import BothReadOnly, IdentConflict, \ from vdirsyncer.sync.exceptions import BothReadOnly
PartialSync, StorageEmpty, SyncConflict from vdirsyncer.sync.exceptions import IdentConflict
from vdirsyncer.sync.exceptions import PartialSync
from vdirsyncer.sync.exceptions import StorageEmpty
from vdirsyncer.sync.exceptions import SyncConflict
from vdirsyncer.sync.status import SqliteStatus from vdirsyncer.sync.status import SqliteStatus
from vdirsyncer.vobject import Item from vdirsyncer.vobject import Item
@ -253,7 +258,7 @@ def test_conflict_resolution_both_etags_new(winning_storage):
b.update(href_b, item_b, etag_b) b.update(href_b, item_b, etag_b)
with pytest.raises(SyncConflict): with pytest.raises(SyncConflict):
sync(a, b, status) sync(a, b, status)
sync(a, b, status, conflict_resolution='{} wins'.format(winning_storage)) sync(a, b, status, conflict_resolution=f'{winning_storage} wins')
assert items(a) == items(b) == { assert items(a) == items(b) == {
item_a.raw if winning_storage == 'a' else item_b.raw item_a.raw if winning_storage == 'a' else item_b.raw
} }
@ -563,7 +568,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('UID:{}'.format(uid)) item = Item(f'UID:{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,12 +1,13 @@
import hypothesis.strategies as st import hypothesis.strategies as st
from hypothesis import example, given
import pytest import pytest
from hypothesis import example
from hypothesis import given
from tests import blow_up from tests import blow_up
from vdirsyncer.exceptions import UserError from vdirsyncer.exceptions import UserError
from vdirsyncer.metasync import MetaSyncConflict, logger, metasync from vdirsyncer.metasync import logger
from vdirsyncer.metasync import metasync
from vdirsyncer.metasync import MetaSyncConflict
from vdirsyncer.storage.base import normalize_meta_value from vdirsyncer.storage.base import normalize_meta_value
from vdirsyncer.storage.memory import MemoryStorage from vdirsyncer.storage.memory import MemoryStorage

View file

@ -1,10 +1,12 @@
from hypothesis import HealthCheck, given, settings
import pytest import pytest
from hypothesis import given
from hypothesis import HealthCheck
from hypothesis import settings
from tests import uid_strategy from tests import uid_strategy
from vdirsyncer.repair import IrreparableItem
from vdirsyncer.repair import IrreparableItem, repair_item, repair_storage from vdirsyncer.repair import repair_item
from vdirsyncer.repair import repair_storage
from vdirsyncer.storage.memory import MemoryStorage from vdirsyncer.storage.memory import MemoryStorage
from vdirsyncer.utils import href_safe from vdirsyncer.utils import href_safe
from vdirsyncer.vobject import Item from vdirsyncer.vobject import Item
@ -18,11 +20,11 @@ def test_repair_uids(uid):
s.items = { s.items = {
'one': ( 'one': (
'asdf', 'asdf',
Item('BEGIN:VCARD\nFN:Hans\nUID:{}\nEND:VCARD'.format(uid)) Item(f'BEGIN:VCARD\nFN:Hans\nUID:{uid}\nEND:VCARD')
), ),
'two': ( 'two': (
'asdf', 'asdf',
Item('BEGIN:VCARD\nFN:Peppi\nUID:{}\nEND:VCARD'.format(uid)) Item(f'BEGIN:VCARD\nFN:Peppi\nUID:{uid}\nEND:VCARD')
) )
} }
@ -40,7 +42,7 @@ def test_repair_uids(uid):
@settings(suppress_health_check=HealthCheck.all()) @settings(suppress_health_check=HealthCheck.all())
def test_repair_unsafe_uids(uid): def test_repair_unsafe_uids(uid):
s = MemoryStorage() s = MemoryStorage()
item = Item('BEGIN:VCARD\nUID:{}\nEND:VCARD'.format(uid)) item = Item(f'BEGIN:VCARD\nUID:{uid}\nEND:VCARD')
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)
@ -58,7 +60,7 @@ def test_repair_unsafe_uids(uid):
('perfectly-fine', 'b@dh0mbr3') ('perfectly-fine', 'b@dh0mbr3')
]) ])
def test_repair_unsafe_href(uid, href): def test_repair_unsafe_href(uid, href):
item = Item('BEGIN:VCARD\nUID:{}\nEND:VCARD'.format(uid)) item = Item(f'BEGIN:VCARD\nUID:{uid}\nEND:VCARD')
new_item = repair_item(href, item, set(), True) new_item = repair_item(href, item, set(), True)
assert new_item.raw != item.raw assert new_item.raw != item.raw
assert new_item.uid != item.uid assert new_item.uid != item.uid

View file

@ -1,16 +1,20 @@
from textwrap import dedent from textwrap import dedent
import hypothesis.strategies as st import hypothesis.strategies as st
from hypothesis import assume, given
from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule
import pytest import pytest
from hypothesis import assume
from tests import BARE_EVENT_TEMPLATE, EVENT_TEMPLATE, \ from hypothesis import given
EVENT_WITH_TIMEZONE_TEMPLATE, VCARD_TEMPLATE, normalize_item, \ from hypothesis.stateful import Bundle
uid_strategy from hypothesis.stateful import rule
from hypothesis.stateful import RuleBasedStateMachine
import vdirsyncer.vobject as vobject import vdirsyncer.vobject as vobject
from tests import BARE_EVENT_TEMPLATE
from tests import EVENT_TEMPLATE
from tests import EVENT_WITH_TIMEZONE_TEMPLATE
from tests import normalize_item
from tests import uid_strategy
from tests import VCARD_TEMPLATE
_simple_split = [ _simple_split = [
@ -221,7 +225,7 @@ def test_replace_uid(template, uid):
item = vobject.Item(template.format(r=123, uid=123)).with_uid(uid) item = vobject.Item(template.format(r=123, uid=123)).with_uid(uid)
assert item.uid == uid assert item.uid == uid
if uid: if uid:
assert item.raw.count('\nUID:{}'.format(uid)) == 1 assert item.raw.count(f'\nUID:{uid}') == 1
else: else:
assert '\nUID:' not in item.raw assert '\nUID:' not in item.raw
@ -317,7 +321,7 @@ class VobjectMachine(RuleBasedStateMachine):
params=st.lists(st.tuples(value_strategy, value_strategy))) params=st.lists(st.tuples(value_strategy, value_strategy)))
def add_prop_raw(self, c, key, value, params): def add_prop_raw(self, c, key, value, params):
params_str = ','.join(k + '=' + v for k, v in params) params_str = ','.join(k + '=' + v for k, v in params)
c.props.insert(0, '{};{}:{}'.format(key, params_str, value)) c.props.insert(0, f'{key};{params_str}:{value}')
assert c[key] == value assert c[key] == value
assert key in c assert key in c
assert c.get(key) == value assert c.get(key) == value

View file

@ -19,8 +19,8 @@ except ImportError: # pragma: no cover
def _check_python_version(): # pragma: no cover def _check_python_version(): # pragma: no cover
import sys import sys
if sys.version_info < (3, 4, 0): if sys.version_info < (3, 7, 0):
print('vdirsyncer requires at least Python 3.5.') print('vdirsyncer requires at least Python 3.7.')
sys.exit(1) sys.exit(1)

View file

@ -3,10 +3,10 @@ import logging
import sys import sys
import click import click
import click_log import click_log
from .. import BUGTRACKER_HOME, __version__ from .. import __version__
from .. import BUGTRACKER_HOME
cli_logger = logging.getLogger(__name__) cli_logger = logging.getLogger(__name__)
@ -65,7 +65,7 @@ def max_workers_callback(ctx, param, value):
if value == 0 and logging.getLogger('vdirsyncer').level == logging.DEBUG: if value == 0 and logging.getLogger('vdirsyncer').level == logging.DEBUG:
value = 1 value = 1
cli_logger.debug('Using {} maximal workers.'.format(value)) cli_logger.debug(f'Using {value} maximal workers.')
return value return value
@ -75,7 +75,7 @@ def max_workers_option(default=0):
help += 'The default is 0, which means "as many as necessary". ' \ help += 'The default is 0, which means "as many as necessary". ' \
'With -vdebug enabled, the default is 1.' 'With -vdebug enabled, the default is 1.'
else: else:
help += 'The default is {}.'.format(default) help += f'The default is {default}.'
return click.option( return click.option(
'--max-workers', default=default, type=click.IntRange(min=0, max=None), '--max-workers', default=default, type=click.IntRange(min=0, max=None),

View file

@ -6,10 +6,12 @@ from itertools import chain
from click_threading import get_ui_worker from click_threading import get_ui_worker
from .. import exceptions
from .. import PROJECT_HOME
from ..utils import cached_property
from ..utils import expand_path
from .fetchparams import expand_fetch_params from .fetchparams import expand_fetch_params
from .utils import storage_class_from_config from .utils import storage_class_from_config
from .. import PROJECT_HOME, exceptions
from ..utils import cached_property, expand_path
GENERAL_ALL = frozenset(['status_path']) GENERAL_ALL = frozenset(['status_path'])
@ -101,7 +103,7 @@ class _ConfigReader:
def _parse_section(self, section_type, name, options): def _parse_section(self, section_type, name, options):
validate_section_name(name, section_type) validate_section_name(name, section_type)
if name in self._seen_names: if name in self._seen_names:
raise ValueError('Name "{}" already used.'.format(name)) raise ValueError(f'Name "{name}" already used.')
self._seen_names.add(name) self._seen_names.add(name)
if section_type == 'general': if section_type == 'general':
@ -163,7 +165,7 @@ class Config:
try: try:
self.pairs[name] = PairConfig(self, name, options) self.pairs[name] = PairConfig(self, name, options)
except ValueError as e: except ValueError as e:
raise exceptions.UserError('Pair {}: {}'.format(name, e)) raise exceptions.UserError(f'Pair {name}: {e}')
@classmethod @classmethod
def from_fileobject(cls, f): def from_fileobject(cls, f):

View file

@ -3,12 +3,14 @@ import json
import logging import logging
import sys import sys
from .utils import handle_collection_not_found, handle_storage_init_error, \
load_status, save_status, storage_class_from_config, \
storage_instance_from_config
from .. import exceptions from .. import exceptions
from ..utils import cached_property from ..utils import cached_property
from .utils import handle_collection_not_found
from .utils import handle_storage_init_error
from .utils import load_status
from .utils import save_status
from .utils import storage_class_from_config
from .utils import storage_instance_from_config
# Increase whenever upgrade potentially breaks discovery cache and collections # Increase whenever upgrade potentially breaks discovery cache and collections
@ -211,7 +213,7 @@ def _print_collections(instance_name, get_discovered):
logger.warning('Failed to discover collections for {}, use `-vdebug` ' logger.warning('Failed to discover collections for {}, use `-vdebug` '
'to see the full traceback.'.format(instance_name)) 'to see the full traceback.'.format(instance_name))
return return
logger.info('{}:'.format(instance_name)) logger.info(f'{instance_name}:')
for args in discovered.values(): for args in discovered.values():
collection = args['collection'] collection = args['collection']
if collection is None: if collection is None:
@ -226,7 +228,7 @@ def _print_collections(instance_name, get_discovered):
logger.info(' - {}{}'.format( logger.info(' - {}{}'.format(
json.dumps(collection), json.dumps(collection),
' ("{}")'.format(displayname) f' ("{displayname}")'
if displayname and displayname != collection if displayname and displayname != collection
else '' else ''
)) ))

View file

@ -4,7 +4,8 @@ import click
from . import AppContext from . import AppContext
from .. import exceptions from .. import exceptions
from ..utils import expand_path, synchronized from ..utils import expand_path
from ..utils import synchronized
SUFFIX = '.fetch' SUFFIX = '.fetch'
@ -19,7 +20,7 @@ def expand_fetch_params(config):
newkey = key[:-len(SUFFIX)] newkey = key[:-len(SUFFIX)]
if newkey in config: if newkey in config:
raise ValueError('Can\'t set {} and {}.'.format(key, newkey)) raise ValueError(f'Can\'t set {key} and {newkey}.')
config[newkey] = _fetch_value(config[key], key) config[newkey] = _fetch_value(config[key], key)
del config[key] del config[key]
@ -45,7 +46,7 @@ def _fetch_value(opts, key):
cache_key = tuple(opts) cache_key = tuple(opts)
if cache_key in password_cache: if cache_key in password_cache:
rv = password_cache[cache_key] rv = password_cache[cache_key]
logger.debug('Found cached value for {!r}.'.format(opts)) logger.debug(f'Found cached value for {opts!r}.')
if isinstance(rv, BaseException): if isinstance(rv, BaseException):
raise rv raise rv
return rv return rv
@ -54,7 +55,7 @@ def _fetch_value(opts, key):
try: try:
strategy_fn = STRATEGIES[strategy] strategy_fn = STRATEGIES[strategy]
except KeyError: except KeyError:
raise exceptions.UserError('Unknown strategy: {}'.format(strategy)) raise exceptions.UserError(f'Unknown strategy: {strategy}')
logger.debug('Fetching value for {} with {} strategy.' logger.debug('Fetching value for {} with {} strategy.'
.format(key, strategy)) .format(key, strategy))

View file

@ -1,13 +1,19 @@
import functools import functools
import json import json
from .. import exceptions
from .. import sync
from .config import CollectionConfig from .config import CollectionConfig
from .discover import collections_for_pair, storage_class_from_config, \ from .discover import collections_for_pair
storage_instance_from_config from .discover import storage_class_from_config
from .utils import JobFailed, cli_logger, get_status_name, \ from .discover import storage_instance_from_config
handle_cli_error, load_status, manage_sync_status, save_status from .utils import cli_logger
from .utils import get_status_name
from .. import exceptions, sync from .utils import handle_cli_error
from .utils import JobFailed
from .utils import load_status
from .utils import manage_sync_status
from .utils import save_status
def prepare_pair(wq, pair_name, collections, config, callback, **kwargs): def prepare_pair(wq, pair_name, collections, config, callback, **kwargs):
@ -45,7 +51,7 @@ def sync_collection(wq, collection, general, force_delete):
status_name = get_status_name(pair.name, collection.name) status_name = get_status_name(pair.name, collection.name)
try: try:
cli_logger.info('Syncing {}'.format(status_name)) cli_logger.info(f'Syncing {status_name}')
a = storage_instance_from_config(collection.config_a) a = storage_instance_from_config(collection.config_a)
b = storage_instance_from_config(collection.config_b) b = storage_instance_from_config(collection.config_b)
@ -110,7 +116,7 @@ def repair_collection(config, collection, repair_unsafe_uid):
config['type'] = storage_type config['type'] = storage_type
storage = storage_instance_from_config(config) storage = storage_instance_from_config(config)
cli_logger.info('Repairing {}/{}'.format(storage_name, collection)) cli_logger.info(f'Repairing {storage_name}/{collection}')
cli_logger.warning('Make sure no other program is talking to the server.') cli_logger.warning('Make sure no other program is talking to the server.')
repair_storage(storage, repair_unsafe_uid=repair_unsafe_uid) repair_storage(storage, repair_unsafe_uid=repair_unsafe_uid)
@ -121,7 +127,7 @@ def metasync_collection(wq, collection, general):
status_name = get_status_name(pair.name, collection.name) status_name = get_status_name(pair.name, collection.name)
try: try:
cli_logger.info('Metasyncing {}'.format(status_name)) cli_logger.info(f'Metasyncing {status_name}')
status = load_status(general['status_path'], pair.name, status = load_status(general['status_path'], pair.name,
collection.name, data_type='metadata') or {} collection.name, data_type='metadata') or {}

View file

@ -7,18 +7,21 @@ import os
import queue import queue
import sys import sys
import click
import click_threading
from atomicwrites import atomic_write from atomicwrites import atomic_write
import click
import click_threading
from . import cli_logger from . import cli_logger
from .. import BUGTRACKER_HOME, DOCS_HOME, exceptions from .. import BUGTRACKER_HOME
from ..sync.exceptions import IdentConflict, PartialSync, StorageEmpty, \ from .. import DOCS_HOME
SyncConflict from .. import exceptions
from ..sync.exceptions import IdentConflict
from ..sync.exceptions import PartialSync
from ..sync.exceptions import StorageEmpty
from ..sync.exceptions import SyncConflict
from ..sync.status import SqliteStatus from ..sync.status import SqliteStatus
from ..utils import expand_path, get_storage_init_args from ..utils import expand_path
from ..utils import get_storage_init_args
STATUS_PERMISSIONS = 0o600 STATUS_PERMISSIONS = 0o600
@ -144,11 +147,11 @@ def handle_cli_error(status_name=None, e=None):
import traceback import traceback
tb = traceback.format_tb(tb) tb = traceback.format_tb(tb)
if status_name: if status_name:
msg = 'Unknown error occurred for {}'.format(status_name) msg = f'Unknown error occurred for {status_name}'
else: else:
msg = 'Unknown error occurred' msg = 'Unknown error occurred'
msg += ': {}\nUse `-vdebug` to see the full traceback.'.format(e) msg += f': {e}\nUse `-vdebug` to see the full traceback.'
cli_logger.error(msg) cli_logger.error(msg)
cli_logger.debug(''.join(tb)) cli_logger.debug(''.join(tb))
@ -210,8 +213,7 @@ def manage_sync_status(base_path, pair_name, collection_name):
with open(path, 'rb') as f: with open(path, 'rb') as f:
if f.read(1) == b'{': if f.read(1) == b'{':
f.seek(0) f.seek(0)
# json.load doesn't work on binary files for Python 3.5 legacy_status = dict(json.load(f))
legacy_status = dict(json.loads(f.read().decode('utf-8')))
except (OSError, ValueError): except (OSError, ValueError):
pass pass
@ -247,7 +249,7 @@ def storage_class_from_config(config):
cls = storage_names[storage_name] cls = storage_names[storage_name]
except KeyError: except KeyError:
raise exceptions.UserError( raise exceptions.UserError(
'Unknown storage type: {}'.format(storage_name)) f'Unknown storage type: {storage_name}')
return cls, config return cls, config
@ -399,7 +401,7 @@ def handle_collection_not_found(config, collection, e=None):
storage_name = config.get('instance_name', None) storage_name = config.get('instance_name', None)
cli_logger.warning('{}No collection {} found for storage {}.' cli_logger.warning('{}No collection {} found for storage {}.'
.format('{}\n'.format(e) if e else '', .format(f'{e}\n' if e else '',
json.dumps(collection), storage_name)) json.dumps(collection), storage_name))
if click.confirm('Should vdirsyncer attempt to create it?'): if click.confirm('Should vdirsyncer attempt to create it?'):

View file

@ -10,7 +10,7 @@ class Error(Exception):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
for key, value in kwargs.items(): for key, value in kwargs.items():
if getattr(self, key, object()) is not None: # pragma: no cover if getattr(self, key, object()) is not None: # pragma: no cover
raise TypeError('Invalid argument: {}'.format(key)) raise TypeError(f'Invalid argument: {key}')
setattr(self, key, value) setattr(self, key, value)
super().__init__(*args) super().__init__(*args)
@ -25,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 += '\n - {}'.format(problem) msg += f'\n - {problem}'
return msg return msg

View file

@ -2,12 +2,14 @@ import logging
import requests import requests
from . import __version__
from . import DOCS_HOME
from . import exceptions
from .utils import expand_path from .utils import expand_path
from . import DOCS_HOME, exceptions, __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
USERAGENT = 'vdirsyncer/{}'.format(__version__) USERAGENT = f'vdirsyncer/{__version__}'
def _detect_faulty_requests(): # pragma: no cover def _detect_faulty_requests(): # pragma: no cover
@ -133,7 +135,7 @@ def request(method, url, session=None, latin1_fallback=True,
func = session.request func = session.request
logger.debug('{} {}'.format(method, url)) logger.debug(f'{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('Copying {} to {}'.format(key, storage_b)) logger.info(f'Copying {key} to {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('Copying {} to {}'.format(key, storage_a)) logger.info(f'Copying {key} to {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('Key: {}'.format(key)) logger.debug(f'Key: {key}')
logger.debug('A: {}'.format(a)) logger.debug(f'A: {a}')
logger.debug('B: {}'.format(b)) logger.debug(f'B: {b}')
logger.debug('S: {}'.format(s)) logger.debug(f'S: {s}')
if a != s and b != s: if a != s and b != s:
_resolve_conflict() _resolve_conflict()

View file

@ -1,7 +1,8 @@
import logging import logging
from os.path import basename from os.path import basename
from .utils import generate_href, href_safe from .utils import generate_href
from .utils import href_safe
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,7 +26,7 @@ def repair_storage(storage, repair_unsafe_uid):
'The PRODID property may indicate which software ' 'The PRODID property may indicate which software '
'created this item.' 'created this item.'
.format(href)) .format(href))
logger.error('Item content: {!r}'.format(item.raw)) logger.error(f'Item content: {item.raw!r}')
continue continue
seen_uids.add(new_item.uid) seen_uids.add(new_item.uid)

View file

@ -71,7 +71,7 @@ class Storage(metaclass=StorageMeta):
self.read_only = bool(read_only) self.read_only = bool(read_only)
if collection and instance_name: if collection and instance_name:
instance_name = '{}/{}'.format(instance_name, collection) instance_name = f'{instance_name}/{collection}'
self.instance_name = instance_name self.instance_name = instance_name
self.collection = collection self.collection = collection

View file

@ -2,17 +2,21 @@ import datetime
import logging import logging
import urllib.parse as urlparse import urllib.parse as urlparse
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from inspect import getfullargspec from inspect import getfullargspec
import requests import requests
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from .base import Storage, normalize_meta_value from .. import exceptions
from .. import exceptions, http, utils from .. import http
from ..http import USERAGENT, prepare_auth, \ from .. import utils
prepare_client_cert, prepare_verify from ..http import prepare_auth
from ..http import prepare_client_cert
from ..http import prepare_verify
from ..http import USERAGENT
from ..vobject import Item from ..vobject import Item
from .base import normalize_meta_value
from .base import Storage
dav_logger = logging.getLogger(__name__) dav_logger = logging.getLogger(__name__)
@ -34,7 +38,7 @@ del _generate_path_reserved_chars
def _contains_quoted_reserved_chars(x): def _contains_quoted_reserved_chars(x):
for y in _path_reserved_chars: for y in _path_reserved_chars:
if y in x: if y in x:
dav_logger.debug('Unsafe character: {!r}'.format(y)) dav_logger.debug(f'Unsafe character: {y!r}')
return True return True
return False return False
@ -52,7 +56,7 @@ def _assert_multistatus_success(r):
except (ValueError, IndexError): except (ValueError, IndexError):
continue continue
if st < 200 or st >= 400: if st < 200 or st >= 400:
raise HTTPError('Server error: {}'.format(st)) raise HTTPError(f'Server error: {st}')
def _normalize_href(base, href): def _normalize_href(base, href):
@ -78,7 +82,7 @@ def _normalize_href(base, href):
x = urlparse.quote(x, '/@%:') x = urlparse.quote(x, '/@%:')
if orig_href == x: if orig_href == x:
dav_logger.debug('Already normalized: {!r}'.format(x)) dav_logger.debug(f'Already normalized: {x!r}')
else: else:
dav_logger.debug('Normalized URL from {!r} to {!r}' dav_logger.debug('Normalized URL from {!r} to {!r}'
.format(orig_href, x)) .format(orig_href, x))
@ -120,7 +124,7 @@ def _merge_xml(items):
return None return None
rv = items[0] rv = items[0]
for item in items[1:]: for item in items[1:]:
rv.extend(item.getiterator()) rv.extend(item.iter())
return rv return rv
@ -459,7 +463,7 @@ class DAVStorage(Storage):
for href in hrefs: for href in hrefs:
if href != self._normalize_href(href): if href != self._normalize_href(href):
raise exceptions.NotFoundError(href) raise exceptions.NotFoundError(href)
href_xml.append('<D:href>{}</D:href>'.format(href)) href_xml.append(f'<D:href>{href}</D:href>')
if not href_xml: if not href_xml:
return () return ()
@ -591,7 +595,7 @@ class DAVStorage(Storage):
props = _merge_xml(props) props = _merge_xml(props)
if props.find('{DAV:}resourcetype/{DAV:}collection') is not None: if props.find('{DAV:}resourcetype/{DAV:}collection') is not None:
dav_logger.debug('Skipping {!r}, is collection.'.format(href)) dav_logger.debug(f'Skipping {href!r}, is collection.')
continue continue
etag = getattr(props.find('{DAV:}getetag'), 'text', '') etag = getattr(props.find('{DAV:}getetag'), 'text', '')
@ -641,7 +645,7 @@ class DAVStorage(Storage):
except KeyError: except KeyError:
raise exceptions.UnsupportedMetadataError() raise exceptions.UnsupportedMetadataError()
xpath = '{{{}}}{}'.format(namespace, tagname) xpath = f'{{{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>
@ -674,7 +678,7 @@ class DAVStorage(Storage):
except KeyError: except KeyError:
raise exceptions.UnsupportedMetadataError() raise exceptions.UnsupportedMetadataError()
lxml_selector = '{{{}}}{}'.format(namespace, tagname) lxml_selector = f'{{{namespace}}}{tagname}'
element = etree.Element(lxml_selector) element = etree.Element(lxml_selector)
element.text = normalize_meta_value(value) element.text = normalize_meta_value(value)

View file

@ -1,8 +1,8 @@
import binascii
import contextlib import contextlib
import functools import functools
import logging import logging
import os import os
import binascii
import atomicwrites import atomicwrites
import click import click
@ -66,7 +66,7 @@ class _Session:
key = self._get_key() key = self._get_key()
if not key: if not key:
password = click.prompt('Enter key password', hide_input=True) password = click.prompt('Enter key password', hide_input=True)
click.echo('Deriving key for {}'.format(self.email)) click.echo(f'Deriving key for {self.email}')
self.etesync.derive_key(password) self.etesync.derive_key(password)
self._set_key(self.etesync.cipher_key) self._set_key(self.etesync.cipher_key)
else: else:
@ -134,7 +134,7 @@ class EtesyncStorage(Storage):
**kwargs **kwargs
) )
else: else:
logger.debug('Skipping collection: {!r}'.format(entry)) logger.debug(f'Skipping collection: {entry!r}')
@classmethod @classmethod
def create_collection(cls, collection, email, secrets_dir, server_url=None, def create_collection(cls, collection, email, secrets_dir, server_url=None,

View file

@ -5,10 +5,14 @@ import subprocess
from atomicwrites import atomic_write from atomicwrites import atomic_write
from .base import Storage, normalize_meta_value
from .. import exceptions from .. import exceptions
from ..utils import checkdir, expand_path, generate_href, get_etag_from_file from ..utils import checkdir
from ..utils import expand_path
from ..utils import generate_href
from ..utils import get_etag_from_file
from ..vobject import Item from ..vobject import Item
from .base import normalize_meta_value
from .base import Storage
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -3,15 +3,16 @@ import logging
import os import os
import urllib.parse as urlparse import urllib.parse as urlparse
from atomicwrites import atomic_write
import click import click
from atomicwrites import atomic_write
from click_threading import get_ui_worker from click_threading import get_ui_worker
from . import base, dav from . import base
from . import dav
from .. import exceptions from .. import exceptions
from ..utils import checkdir, expand_path, open_graphical_browser from ..utils import checkdir
from ..utils import expand_path
from ..utils import open_graphical_browser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -80,7 +81,7 @@ class GoogleSession(dav.DAVSession):
# access_type and approval_prompt are Google specific # access_type and approval_prompt are Google specific
# extra parameters. # extra parameters.
access_type='offline', approval_prompt='force') access_type='offline', approval_prompt='force')
click.echo('Opening {} ...'.format(authorization_url)) click.echo(f'Opening {authorization_url} ...')
try: try:
open_graphical_browser(authorization_url) open_graphical_browser(authorization_url)
except Exception as e: except Exception as e:

View file

@ -1,10 +1,14 @@
import urllib.parse as urlparse import urllib.parse as urlparse
from .base import Storage
from .. import exceptions from .. import exceptions
from ..http import USERAGENT, prepare_auth, \ from ..http import prepare_auth
prepare_client_cert, prepare_verify, request from ..http import prepare_client_cert
from ..vobject import Item, split_collection from ..http import prepare_verify
from ..http import request
from ..http import USERAGENT
from ..vobject import Item
from ..vobject import split_collection
from .base import Storage
class HttpStorage(Storage): class HttpStorage(Storage):

View file

@ -1,12 +1,12 @@
import random import random
from .base import Storage, normalize_meta_value
from .. import exceptions from .. import exceptions
from .base import normalize_meta_value
from .base import Storage
def _random_string(): def _random_string():
return '{:.9f}'.format(random.random()) return f'{random.random():.9f}'
class MemoryStorage(Storage): class MemoryStorage(Storage):

View file

@ -7,10 +7,14 @@ import os
from atomicwrites import atomic_write from atomicwrites import atomic_write
from .base import Storage
from .. import exceptions from .. import exceptions
from ..utils import checkfile, expand_path, get_etag_from_file from ..utils import checkfile
from ..vobject import Item, join_collection, split_collection from ..utils import expand_path
from ..utils import get_etag_from_file
from ..vobject import Item
from ..vobject import join_collection
from ..vobject import split_collection
from .base import Storage
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -15,10 +15,13 @@ import logging
from ..exceptions import UserError from ..exceptions import UserError
from ..utils import uniq from ..utils import uniq
from .exceptions import BothReadOnly
from .status import SubStatus, ItemMetadata from .exceptions import IdentAlreadyExists
from .exceptions import BothReadOnly, IdentAlreadyExists, PartialSync, \ from .exceptions import PartialSync
StorageEmpty, SyncConflict from .exceptions import StorageEmpty
from .exceptions import SyncConflict
from .status import ItemMetadata
from .status import SubStatus
sync_logger = logging.getLogger(__name__) sync_logger = logging.getLogger(__name__)

View file

@ -1,7 +1,7 @@
import abc import abc
import contextlib import contextlib
import sys
import sqlite3 import sqlite3
import sys
from .exceptions import IdentAlreadyExists from .exceptions import IdentAlreadyExists

View file

@ -2,7 +2,6 @@ import functools
import os import os
import sys import sys
import uuid import uuid
from inspect import getfullargspec from inspect import getfullargspec
from . import exceptions from . import exceptions
@ -73,7 +72,7 @@ def get_etag_from_file(f):
mtime = getattr(stat, 'st_mtime_ns', None) mtime = getattr(stat, 'st_mtime_ns', None)
if mtime is None: if mtime is None:
mtime = stat.st_mtime mtime = stat.st_mtime
return '{:.9f};{}'.format(mtime, stat.st_ino) return f'{mtime:.9f};{stat.st_ino}'
def get_storage_init_specs(cls, stop_at=object): def get_storage_init_specs(cls, stop_at=object):
@ -125,7 +124,7 @@ def checkdir(path, create=False, mode=0o750):
if not os.path.isdir(path): if not os.path.isdir(path):
if os.path.exists(path): if os.path.exists(path):
raise OSError('{} is not a directory.'.format(path)) raise OSError(f'{path} is not a directory.')
if create: if create:
os.makedirs(path, mode) os.makedirs(path, mode)
else: else:
@ -143,7 +142,7 @@ def checkfile(path, create=False):
checkdir(os.path.dirname(path), create=create) checkdir(os.path.dirname(path), create=create)
if not os.path.isfile(path): if not os.path.isfile(path):
if os.path.exists(path): if os.path.exists(path):
raise OSError('{} is not a file.'.format(path)) raise OSError(f'{path} is not a file.')
if create: if create:
with open(path, 'wb'): with open(path, 'wb'):
pass pass

View file

@ -1,7 +1,9 @@
import hashlib import hashlib
from itertools import chain, tee from itertools import chain
from itertools import tee
from .utils import cached_property, uniq from .utils import cached_property
from .utils import uniq
IGNORE_PROPS = ( IGNORE_PROPS = (
@ -205,14 +207,14 @@ def join_collection(items, wrappers=_default_join_wrappers):
if wrapper_type is not None: if wrapper_type is not None:
lines = chain(*( lines = chain(*(
['BEGIN:{}'.format(wrapper_type)], [f'BEGIN:{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,
['END:{}'.format(wrapper_type)] [f'END:{wrapper_type}']
)) ))
return ''.join(line + '\r\n' for line in lines) return ''.join(line + '\r\n' for line in lines)
@ -299,14 +301,14 @@ class _Component:
return rv[0] return rv[0]
def dump_lines(self): def dump_lines(self):
yield 'BEGIN:{}'.format(self.name) yield f'BEGIN:{self.name}'
yield from self.props yield from self.props
for c in self.subcomponents: for c in self.subcomponents:
yield from c.dump_lines() yield from c.dump_lines()
yield 'END:{}'.format(self.name) yield f'END:{self.name}'
def __delitem__(self, key): def __delitem__(self, key):
prefix = ('{}:'.format(key), '{};'.format(key)) prefix = (f'{key}:', f'{key};')
new_lines = [] new_lines = []
lineiter = iter(self.props) lineiter = iter(self.props)
while True: while True:
@ -329,7 +331,7 @@ class _Component:
assert isinstance(val, str) assert isinstance(val, str)
assert '\n' not in val assert '\n' not in val
del self[key] del self[key]
line = '{}:{}'.format(key, val) line = f'{key}:{val}'
self.props.append(line) self.props.append(line)
def __contains__(self, obj): def __contains__(self, obj):
@ -342,8 +344,8 @@ class _Component:
raise ValueError(obj) raise ValueError(obj)
def __getitem__(self, key): def __getitem__(self, key):
prefix_without_params = '{}:'.format(key) prefix_without_params = f'{key}:'
prefix_with_params = '{};'.format(key) prefix_with_params = f'{key};'
iterlines = iter(self.props) iterlines = iter(self.props)
for line in iterlines: for line in iterlines:
if line.startswith(prefix_without_params): if line.startswith(prefix_without_params):