mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Remove Python 2 support (#499)
* Discontinue Python 2. See #219 * Remove Python 2 config option * Remove coerce_native * Remove PY2 variable * s/text_type/str/g * Flake8 fixes * Remove str = str * s/to_native/to_unicode/g * Remove to_unicode = to_unicode * Remove iteritems * Remove itervalues * Remove str import, flake8 fixes * Remove urlparse compat code * Remove with_metaclass * Remove unused PY2 variable * Remove getargspec_ish * Remove to_bytes * Remove compat module * Remove Python 2 from Travis * fixup! Remove urlparse compat code * fixup! Remove urlparse compat code * fixup! Remove compat module
This commit is contained in:
parent
696e53dc1f
commit
18d8bb9fc2
27 changed files with 107 additions and 334 deletions
68
.travis.yml
68
.travis.yml
|
|
@ -14,70 +14,6 @@
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"matrix": {
|
"matrix": {
|
||||||
"include": [
|
"include": [
|
||||||
{
|
|
||||||
"env": "BUILD=style BUILD_PRS=true",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test REMOTESTORAGE_SERVER=mysteryshack REQUIREMENTS=devel BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test REMOTESTORAGE_SERVER=mysteryshack REQUIREMENTS=release BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test REMOTESTORAGE_SERVER=mysteryshack REQUIREMENTS=minimal BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=owncloud REQUIREMENTS=devel BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=owncloud REQUIREMENTS=release BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=owncloud REQUIREMENTS=minimal BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=nextcloud REQUIREMENTS=devel BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=nextcloud REQUIREMENTS=release BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=nextcloud REQUIREMENTS=minimal BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=baikal REQUIREMENTS=devel BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=baikal REQUIREMENTS=release BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=baikal REQUIREMENTS=minimal BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=davical REQUIREMENTS=devel BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=davical REQUIREMENTS=release BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"env": "BUILD=test DAV_SERVER=davical REQUIREMENTS=minimal BUILD_PRS=false",
|
|
||||||
"python": "2.7"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"env": "BUILD=style BUILD_PRS=true",
|
"env": "BUILD=style BUILD_PRS=true",
|
||||||
"python": "3.3"
|
"python": "3.3"
|
||||||
|
|
@ -186,10 +122,6 @@
|
||||||
"env": "BUILD=test DAV_SERVER=davical REQUIREMENTS=minimal BUILD_PRS=false",
|
"env": "BUILD=test DAV_SERVER=davical REQUIREMENTS=minimal BUILD_PRS=false",
|
||||||
"python": "3.5"
|
"python": "3.5"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"env": "BUILD=style BUILD_PRS=true",
|
|
||||||
"python": "pypy"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"env": "BUILD=test",
|
"env": "BUILD=test",
|
||||||
"language": "generic",
|
"language": "generic",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ Package maintainers and users who have to manually update their installation
|
||||||
may want to subscribe to `GitHub's tag feed
|
may want to subscribe to `GitHub's tag feed
|
||||||
<https://github.com/pimutils/vdirsyncer/tags.atom>`_.
|
<https://github.com/pimutils/vdirsyncer/tags.atom>`_.
|
||||||
|
|
||||||
|
Version 0.13.0
|
||||||
|
==============
|
||||||
|
|
||||||
|
- Python 2 is no longer supported at all. See :gh:`219`.
|
||||||
|
|
||||||
Version 0.12.1
|
Version 0.12.1
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,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:
|
||||||
|
|
||||||
- A compatible version of Python (2.7+ or 3.3+) and the corresponding pip package
|
- Python 3.3+ and pip.
|
||||||
- ``libxml`` and ``libxslt``
|
- ``libxml`` and ``libxslt``
|
||||||
- ``zlib``
|
- ``zlib``
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ cfg['script'] = [script("""
|
||||||
matrix = []
|
matrix = []
|
||||||
cfg['matrix'] = {'include': matrix}
|
cfg['matrix'] = {'include': matrix}
|
||||||
|
|
||||||
for python in ("2.7", "3.3", "3.4", "3.5", "pypy"):
|
for python in ("3.3", "3.4", "3.5"):
|
||||||
matrix.append({
|
matrix.append({
|
||||||
'python': python,
|
'python': python,
|
||||||
'env': 'BUILD=style BUILD_PRS=true'
|
'env': 'BUILD=style BUILD_PRS=true'
|
||||||
|
|
@ -48,12 +48,6 @@ for python in ("2.7", "3.3", "3.4", "3.5", "pypy"):
|
||||||
dav_servers = ("radicale", "owncloud", "nextcloud", "baikal",
|
dav_servers = ("radicale", "owncloud", "nextcloud", "baikal",
|
||||||
"davical")
|
"davical")
|
||||||
rs_servers = ("mysteryshack",)
|
rs_servers = ("mysteryshack",)
|
||||||
elif python == "2.7":
|
|
||||||
dav_servers = ("owncloud", "nextcloud", "baikal", "davical")
|
|
||||||
rs_servers = ("mysteryshack",)
|
|
||||||
elif python == "pypy":
|
|
||||||
dav_servers = ()
|
|
||||||
rs_servers = ()
|
|
||||||
else:
|
else:
|
||||||
dav_servers = ("radicale",)
|
dav_servers = ("radicale",)
|
||||||
rs_servers = ()
|
rs_servers = ()
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import pytest
|
||||||
|
|
||||||
from vdirsyncer import exceptions
|
from vdirsyncer import exceptions
|
||||||
from vdirsyncer.cli.fetchparams import STRATEGIES, expand_fetch_params
|
from vdirsyncer.cli.fetchparams import STRATEGIES, expand_fetch_params
|
||||||
from vdirsyncer.utils.compat import PY2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -90,7 +89,6 @@ def test_key_conflict(monkeypatch, mystrategy):
|
||||||
assert 'Can\'t set foo.fetch and foo.' in str(excinfo.value)
|
assert 'Can\'t set foo.fetch and foo.' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(PY2, reason='Don\'t care about Python 2')
|
|
||||||
@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({
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ from hypothesis import example, given
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vdirsyncer.utils.compat import PY2, to_native, to_unicode
|
|
||||||
|
|
||||||
|
|
||||||
def test_simple_run(tmpdir, runner):
|
def test_simple_run(tmpdir, runner):
|
||||||
runner.write_with_general(dedent('''
|
runner.write_with_general(dedent('''
|
||||||
|
|
@ -277,7 +275,6 @@ def test_multiple_pairs(tmpdir, runner):
|
||||||
st.characters(
|
st.characters(
|
||||||
blacklist_characters=set(
|
blacklist_characters=set(
|
||||||
u'./\x00' # Invalid chars on POSIX filesystems
|
u'./\x00' # Invalid chars on POSIX filesystems
|
||||||
+ (u';' if PY2 else u'') # https://bugs.python.org/issue16374
|
|
||||||
),
|
),
|
||||||
# Surrogates can't be encoded to utf-8 in Python
|
# Surrogates can't be encoded to utf-8 in Python
|
||||||
blacklist_categories=set(['Cs'])
|
blacklist_categories=set(['Cs'])
|
||||||
|
|
@ -289,7 +286,6 @@ def test_multiple_pairs(tmpdir, runner):
|
||||||
))
|
))
|
||||||
@example(collections=[u'persönlich'])
|
@example(collections=[u'persönlich'])
|
||||||
def test_create_collections(subtest, collections):
|
def test_create_collections(subtest, collections):
|
||||||
collections = set(to_native(x, 'utf-8') for x in collections)
|
|
||||||
|
|
||||||
@subtest
|
@subtest
|
||||||
def test_inner(tmpdir, runner):
|
def test_inner(tmpdir, runner):
|
||||||
|
|
@ -325,7 +321,7 @@ def test_create_collections(subtest, collections):
|
||||||
# Quoted from
|
# Quoted from
|
||||||
# https://stackoverflow.com/questions/18137554/how-to-convert-path-to-mac-os-x-path-the-almost-nfd-normal-form # noqa
|
# https://stackoverflow.com/questions/18137554/how-to-convert-path-to-mac-os-x-path-the-almost-nfd-normal-form # noqa
|
||||||
u = lambda xs: set(
|
u = lambda xs: set(
|
||||||
unicodedata.normalize('NFKD', to_unicode(x, 'utf-8'))
|
unicodedata.normalize('NFKD', x)
|
||||||
for x in xs
|
for x in xs
|
||||||
)
|
)
|
||||||
assert u(x.basename for x in tmpdir.join('foo').listdir()) == \
|
assert u(x.basename for x in tmpdir.join('foo').listdir()) == \
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,5 @@
|
||||||
from hypothesis import given
|
|
||||||
from hypothesis.strategies import (
|
|
||||||
binary,
|
|
||||||
booleans,
|
|
||||||
complex_numbers,
|
|
||||||
floats,
|
|
||||||
integers,
|
|
||||||
none,
|
|
||||||
one_of,
|
|
||||||
text
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
from vdirsyncer import exceptions
|
from vdirsyncer import exceptions
|
||||||
from vdirsyncer.cli.utils import coerce_native, handle_cli_error
|
from vdirsyncer.cli.utils import handle_cli_error
|
||||||
|
|
||||||
|
|
||||||
@given(one_of(
|
|
||||||
binary(),
|
|
||||||
booleans(),
|
|
||||||
complex_numbers(),
|
|
||||||
floats(),
|
|
||||||
integers(),
|
|
||||||
none(),
|
|
||||||
text()
|
|
||||||
))
|
|
||||||
def test_coerce_native_fuzzing(s):
|
|
||||||
coerce_native(s)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_cli_error(capsys):
|
def test_handle_cli_error(capsys):
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,6 @@ def setup_logging():
|
||||||
click_log.basic_config('vdirsyncer').setLevel(logging.DEBUG)
|
click_log.basic_config('vdirsyncer').setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
# XXX: Py2
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def suppress_py2_warning(monkeypatch):
|
|
||||||
monkeypatch.setattr('vdirsyncer.cli._check_python2', lambda _: None)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pytest_benchmark
|
import pytest_benchmark
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from urllib.parse import quote as urlquote, unquote as urlunquote
|
||||||
|
|
||||||
import hypothesis.strategies as st
|
import hypothesis.strategies as st
|
||||||
from hypothesis import given
|
from hypothesis import given
|
||||||
|
|
@ -11,7 +12,6 @@ import pytest
|
||||||
|
|
||||||
import vdirsyncer.exceptions as exceptions
|
import vdirsyncer.exceptions as exceptions
|
||||||
from vdirsyncer.storage.base import Item, normalize_meta_value
|
from vdirsyncer.storage.base import Item, normalize_meta_value
|
||||||
from vdirsyncer.utils.compat import iteritems, text_type, urlquote, urlunquote
|
|
||||||
|
|
||||||
from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \
|
from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \
|
||||||
assert_item_equals, normalize_item, printable_characters_strategy
|
assert_item_equals, normalize_item, printable_characters_strategy
|
||||||
|
|
@ -80,8 +80,8 @@ class StorageTests(object):
|
||||||
hrefs.sort()
|
hrefs.sort()
|
||||||
assert hrefs == sorted(s.list())
|
assert hrefs == sorted(s.list())
|
||||||
for href, etag in hrefs:
|
for href, etag in hrefs:
|
||||||
assert isinstance(href, (text_type, bytes))
|
assert isinstance(href, (str, bytes))
|
||||||
assert isinstance(etag, (text_type, bytes))
|
assert isinstance(etag, (str, bytes))
|
||||||
assert s.has(href)
|
assert s.has(href)
|
||||||
item, etag2 = s.get(href)
|
item, etag2 = s.get(href)
|
||||||
assert etag == etag2
|
assert etag == etag2
|
||||||
|
|
@ -114,7 +114,7 @@ class StorageTests(object):
|
||||||
new_item = get_item(uid=item.uid)
|
new_item = get_item(uid=item.uid)
|
||||||
new_etag = s.update(href, new_item, etag)
|
new_etag = s.update(href, new_item, etag)
|
||||||
# See https://github.com/pimutils/vdirsyncer/issues/48
|
# See https://github.com/pimutils/vdirsyncer/issues/48
|
||||||
assert isinstance(new_etag, (bytes, text_type))
|
assert isinstance(new_etag, (bytes, str))
|
||||||
assert_item_equals(s.get(href)[0], new_item)
|
assert_item_equals(s.get(href)[0], new_item)
|
||||||
|
|
||||||
def test_update_nonexisting(self, s, get_item):
|
def test_update_nonexisting(self, s, get_item):
|
||||||
|
|
@ -162,7 +162,7 @@ class StorageTests(object):
|
||||||
|
|
||||||
assert dict(
|
assert dict(
|
||||||
(href, etag) for href, item, etag
|
(href, etag) for href, item, etag
|
||||||
in s.get_multi(href for href, etag in iteritems(info))
|
in s.get_multi(href for href, etag in info.items())
|
||||||
) == info
|
) == info
|
||||||
|
|
||||||
def test_repr(self, s, get_storage_args):
|
def test_repr(self, s, get_storage_args):
|
||||||
|
|
@ -277,7 +277,7 @@ class StorageTests(object):
|
||||||
s.set_meta('displayname', x)
|
s.set_meta('displayname', x)
|
||||||
rv = s.get_meta('displayname')
|
rv = s.get_meta('displayname')
|
||||||
assert rv == x
|
assert rv == x
|
||||||
assert isinstance(rv, text_type)
|
assert isinstance(rv, str)
|
||||||
|
|
||||||
@given(value=st.one_of(
|
@given(value=st.one_of(
|
||||||
st.none(),
|
st.none(),
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
from urllib.parse import quote as urlquote
|
||||||
|
|
||||||
from vdirsyncer.utils.compat import urlquote
|
import pytest
|
||||||
|
|
||||||
import wsgi_intercept
|
import wsgi_intercept
|
||||||
import wsgi_intercept.requests_intercept
|
import wsgi_intercept.requests_intercept
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import platform
|
|
||||||
|
|
||||||
import click_log
|
import click_log
|
||||||
|
|
||||||
|
|
@ -13,7 +12,6 @@ from vdirsyncer import utils
|
||||||
|
|
||||||
# These modules might be uninitialized and unavailable if not explicitly
|
# These modules might be uninitialized and unavailable if not explicitly
|
||||||
# imported
|
# imported
|
||||||
import vdirsyncer.utils.compat # noqa
|
|
||||||
import vdirsyncer.utils.http # noqa
|
import vdirsyncer.utils.http # noqa
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -43,11 +41,8 @@ def test_request_ssl(httpsserver):
|
||||||
|
|
||||||
def _fingerprints_broken():
|
def _fingerprints_broken():
|
||||||
from pkg_resources import parse_version as ver
|
from pkg_resources import parse_version as ver
|
||||||
tolerant_python = (
|
|
||||||
utils.compat.PY2 and platform.python_implementation() != 'PyPy'
|
|
||||||
)
|
|
||||||
broken_urllib3 = ver(requests.__version__) <= ver('2.5.1')
|
broken_urllib3 = ver(requests.__version__) <= ver('2.5.1')
|
||||||
return broken_urllib3 and not tolerant_python
|
return broken_urllib3
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(_fingerprints_broken(),
|
@pytest.mark.skipif(_fingerprints_broken(),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,16 @@ except ImportError: # pragma: no cover
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_python_version(): # pragma: no cover
|
||||||
|
import sys
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
print('vdirsyncer requires Python 3.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
_check_python_version()
|
||||||
|
del _check_python_version
|
||||||
|
|
||||||
|
|
||||||
def _detect_faulty_requests(): # pragma: no cover
|
def _detect_faulty_requests(): # pragma: no cover
|
||||||
import requests
|
import requests
|
||||||
if 'dist-packages' not in requests.__file__:
|
if 'dist-packages' not in requests.__file__:
|
||||||
|
|
@ -41,3 +51,4 @@ def _detect_faulty_requests(): # pragma: no cover
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
_detect_faulty_requests()
|
_detect_faulty_requests()
|
||||||
|
del _detect_faulty_requests
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ import click
|
||||||
|
|
||||||
import click_log
|
import click_log
|
||||||
|
|
||||||
from .. import PROJECT_HOME, __version__, exceptions
|
from .. import __version__
|
||||||
from ..utils.compat import PY2
|
|
||||||
|
|
||||||
|
|
||||||
cli_logger = logging.getLogger(__name__)
|
cli_logger = logging.getLogger(__name__)
|
||||||
|
|
@ -38,29 +37,6 @@ def catch_errors(f):
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
def _check_python2(config):
|
|
||||||
# XXX: Py2
|
|
||||||
if not PY2:
|
|
||||||
return
|
|
||||||
|
|
||||||
msg = (
|
|
||||||
'Python 2 support will be dropped. Please switch '
|
|
||||||
'to at least Python 3.3 as soon as possible. See '
|
|
||||||
'{home}/issues/219 for more information.'
|
|
||||||
.format(home=PROJECT_HOME)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not config.general.get('python2', False):
|
|
||||||
raise exceptions.UserError(
|
|
||||||
msg + (
|
|
||||||
'\nSet python2 = true in the [general] section to get rid of '
|
|
||||||
'this error for now.'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cli_logger.warning(msg)
|
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@click_log.init('vdirsyncer')
|
@click_log.init('vdirsyncer')
|
||||||
@click_log.simple_verbosity_option()
|
@click_log.simple_verbosity_option()
|
||||||
|
|
@ -76,7 +52,6 @@ def app(ctx, config):
|
||||||
|
|
||||||
if not ctx.config:
|
if not ctx.config:
|
||||||
ctx.config = load_config(config)
|
ctx.config = load_config(config)
|
||||||
_check_python2(ctx.config)
|
|
||||||
|
|
||||||
main = app
|
main = app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,13 @@ from . import cli_logger
|
||||||
from .fetchparams import expand_fetch_params
|
from .fetchparams import expand_fetch_params
|
||||||
from .. import PROJECT_HOME, exceptions
|
from .. import PROJECT_HOME, exceptions
|
||||||
from ..utils import cached_property, expand_path
|
from ..utils import cached_property, expand_path
|
||||||
from ..utils.compat import text_type
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ConfigParser import RawConfigParser
|
from ConfigParser import RawConfigParser
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from configparser import RawConfigParser
|
from configparser import RawConfigParser
|
||||||
|
|
||||||
GENERAL_ALL = frozenset(['status_path', 'python2']) # XXX: Py2
|
GENERAL_ALL = frozenset(['status_path'])
|
||||||
GENERAL_REQUIRED = frozenset(['status_path'])
|
GENERAL_REQUIRED = frozenset(['status_path'])
|
||||||
SECTION_NAME_CHARS = frozenset(chain(string.ascii_letters, string.digits, '_'))
|
SECTION_NAME_CHARS = frozenset(chain(string.ascii_letters, string.digits, '_'))
|
||||||
|
|
||||||
|
|
@ -68,7 +67,7 @@ def _validate_pair_section(pair_config):
|
||||||
|
|
||||||
for i, collection in enumerate(collections):
|
for i, collection in enumerate(collections):
|
||||||
try:
|
try:
|
||||||
if isinstance(collection, (text_type, bytes)):
|
if isinstance(collection, (str, bytes)):
|
||||||
collection_name = collection
|
collection_name = collection
|
||||||
elif isinstance(collection, list):
|
elif isinstance(collection, list):
|
||||||
e = ValueError(
|
e = ValueError(
|
||||||
|
|
@ -78,11 +77,11 @@ def _validate_pair_section(pair_config):
|
||||||
if len(collection) != 3:
|
if len(collection) != 3:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
if not isinstance(collection[0], (text_type, bytes)):
|
if not isinstance(collection[0], (str, bytes)):
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
for x in collection[1:]:
|
for x in collection[1:]:
|
||||||
if x is not None and not isinstance(x, (text_type, bytes)):
|
if x is not None and not isinstance(x, (str, bytes)):
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
collection_name = collection[0]
|
collection_name = collection[0]
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,12 @@ import functools
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .config import CollectionConfig
|
from .config import CollectionConfig
|
||||||
from .utils import JobFailed, cli_logger, coerce_native, \
|
from .utils import JobFailed, cli_logger, collections_for_pair, \
|
||||||
collections_for_pair, get_status_name, handle_cli_error, load_status, \
|
get_status_name, handle_cli_error, load_status, save_status, \
|
||||||
save_status, storage_class_from_config, storage_instance_from_config
|
storage_class_from_config, storage_instance_from_config
|
||||||
|
|
||||||
from .. import exceptions
|
from .. import exceptions
|
||||||
from ..sync import sync
|
from ..sync import sync
|
||||||
from ..utils.compat import to_unicode
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_pair(wq, pair_name, collections, config, callback, **kwargs):
|
def prepare_pair(wq, pair_name, collections, config, callback, **kwargs):
|
||||||
|
|
@ -23,9 +22,6 @@ def prepare_pair(wq, pair_name, collections, config, callback, **kwargs):
|
||||||
# spawn one worker less because we can reuse the current one
|
# spawn one worker less because we can reuse the current one
|
||||||
new_workers = -1
|
new_workers = -1
|
||||||
for collection_name in (collections or all_collections):
|
for collection_name in (collections or all_collections):
|
||||||
# XXX: PY2 hack
|
|
||||||
if collection_name is not None:
|
|
||||||
collection_name = to_unicode(collection_name, 'utf-8')
|
|
||||||
try:
|
try:
|
||||||
config_a, config_b = all_collections[collection_name]
|
config_a, config_b = all_collections[collection_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
@ -51,12 +47,12 @@ 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(coerce_native(status_name)))
|
cli_logger.info('Syncing {}'.format(status_name))
|
||||||
|
|
||||||
status = load_status(general['status_path'], pair.name,
|
status = load_status(general['status_path'], pair.name,
|
||||||
collection.name, data_type='items') or {}
|
collection.name, data_type='items') or {}
|
||||||
cli_logger.debug('Loaded status for {}'
|
cli_logger.debug('Loaded status for {}'
|
||||||
.format(coerce_native(status_name)))
|
.format(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)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ from . import cli_logger
|
||||||
from .. import BUGTRACKER_HOME, DOCS_HOME, exceptions
|
from .. import BUGTRACKER_HOME, DOCS_HOME, exceptions
|
||||||
from ..sync import IdentConflict, StorageEmpty, SyncConflict
|
from ..sync import IdentConflict, StorageEmpty, SyncConflict
|
||||||
from ..utils import expand_path, get_storage_init_args
|
from ..utils import expand_path, get_storage_init_args
|
||||||
from ..utils.compat import to_native
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import Queue as queue
|
import Queue as queue
|
||||||
|
|
@ -147,8 +146,7 @@ def handle_cli_error(status_name=None):
|
||||||
import traceback
|
import traceback
|
||||||
tb = traceback.format_tb(tb)
|
tb = traceback.format_tb(tb)
|
||||||
if status_name:
|
if status_name:
|
||||||
msg = 'Unknown error occured for {}'.format(
|
msg = 'Unknown error occured for {}'.format(status_name)
|
||||||
coerce_native(status_name))
|
|
||||||
else:
|
else:
|
||||||
msg = 'Unknown error occured'
|
msg = 'Unknown error occured'
|
||||||
|
|
||||||
|
|
@ -293,7 +291,7 @@ def _handle_collection_not_found(config, collection, e=None):
|
||||||
|
|
||||||
def _print_collections(base_config, discovered):
|
def _print_collections(base_config, discovered):
|
||||||
instance_name = base_config['instance_name']
|
instance_name = base_config['instance_name']
|
||||||
cli_logger.info('{}:'.format(coerce_native(instance_name)))
|
cli_logger.info('{}:'.format(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:
|
||||||
|
|
@ -308,7 +306,7 @@ def _print_collections(base_config, discovered):
|
||||||
|
|
||||||
cli_logger.info(' - {}{}'.format(
|
cli_logger.info(' - {}{}'.format(
|
||||||
json.dumps(collection),
|
json.dumps(collection),
|
||||||
' ("{}")'.format(coerce_native(displayname))
|
' ("{}")'.format(displayname)
|
||||||
if displayname and displayname != collection
|
if displayname and displayname != collection
|
||||||
else ''
|
else ''
|
||||||
))
|
))
|
||||||
|
|
@ -465,7 +463,7 @@ def handle_storage_init_error(cls, config):
|
||||||
u'{} storage doesn\'t take the parameters: {}'
|
u'{} storage doesn\'t take the parameters: {}'
|
||||||
.format(cls.storage_name, u', '.join(invalid)))
|
.format(cls.storage_name, u', '.join(invalid)))
|
||||||
|
|
||||||
if not problems: # XXX: Py2: Proper reraise
|
if not problems:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
raise exceptions.UserError(
|
raise exceptions.UserError(
|
||||||
|
|
@ -585,18 +583,3 @@ def assert_permissions(path, wanted):
|
||||||
cli_logger.warning('Correcting permissions of {} from {:o} to {:o}'
|
cli_logger.warning('Correcting permissions of {} from {:o} to {:o}'
|
||||||
.format(path, permissions, wanted))
|
.format(path, permissions, wanted))
|
||||||
os.chmod(path, wanted)
|
os.chmod(path, wanted)
|
||||||
|
|
||||||
|
|
||||||
def coerce_native(x, encoding='utf-8'):
|
|
||||||
# XXX: Remove with Python 3 only
|
|
||||||
try:
|
|
||||||
return str(x)
|
|
||||||
except UnicodeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
return to_native(x, encoding=encoding)
|
|
||||||
except UnicodeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return repr(x)
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import functools
|
||||||
|
|
||||||
from .. import exceptions, sync
|
from .. import exceptions, sync
|
||||||
from ..utils import uniq
|
from ..utils import uniq
|
||||||
from ..utils.compat import to_native, to_unicode, with_metaclass
|
|
||||||
from ..utils.vobject import Item # noqa
|
from ..utils.vobject import Item # noqa
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -25,7 +24,7 @@ class StorageMeta(type):
|
||||||
return super(StorageMeta, cls).__init__(name, bases, d)
|
return super(StorageMeta, cls).__init__(name, bases, d)
|
||||||
|
|
||||||
|
|
||||||
class Storage(with_metaclass(StorageMeta)):
|
class Storage(metaclass=StorageMeta):
|
||||||
|
|
||||||
'''Superclass of all storages, mainly useful to summarize the interface to
|
'''Superclass of all storages, mainly useful to summarize the interface to
|
||||||
implement.
|
implement.
|
||||||
|
|
@ -77,9 +76,7 @@ class Storage(with_metaclass(StorageMeta)):
|
||||||
self.read_only = bool(read_only)
|
self.read_only = bool(read_only)
|
||||||
|
|
||||||
if collection and instance_name:
|
if collection and instance_name:
|
||||||
# XXX: PY2 hack
|
instance_name = '{}/{}'.format(instance_name, collection)
|
||||||
instance_name = '{}/{}'.format(instance_name,
|
|
||||||
to_native(collection, 'utf-8'))
|
|
||||||
self.instance_name = instance_name
|
self.instance_name = instance_name
|
||||||
self.collection = collection
|
self.collection = collection
|
||||||
|
|
||||||
|
|
@ -241,4 +238,4 @@ class Storage(with_metaclass(StorageMeta)):
|
||||||
|
|
||||||
|
|
||||||
def normalize_meta_value(value):
|
def normalize_meta_value(value):
|
||||||
return to_unicode(value or u'').strip()
|
return (value or u'').strip()
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import urllib.parse as urlparse
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
from inspect import getfullargspec
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
|
|
@ -12,7 +14,6 @@ from .base import Item, Storage, normalize_meta_value
|
||||||
from .http import HTTP_STORAGE_PARAMETERS, USERAGENT, prepare_auth, \
|
from .http import HTTP_STORAGE_PARAMETERS, USERAGENT, prepare_auth, \
|
||||||
prepare_client_cert, prepare_verify
|
prepare_client_cert, prepare_verify
|
||||||
from .. import exceptions, utils
|
from .. import exceptions, utils
|
||||||
from ..utils.compat import PY2, getargspec_ish, text_type, to_native
|
|
||||||
|
|
||||||
|
|
||||||
dav_logger = logging.getLogger(__name__)
|
dav_logger = logging.getLogger(__name__)
|
||||||
|
|
@ -22,7 +23,7 @@ CALDAV_DT_FORMAT = '%Y%m%dT%H%M%SZ'
|
||||||
|
|
||||||
def _generate_path_reserved_chars():
|
def _generate_path_reserved_chars():
|
||||||
for x in "/?#[]!$&'()*+,;":
|
for x in "/?#[]!$&'()*+,;":
|
||||||
x = utils.compat.urlquote(x, '')
|
x = urlparse.quote(x, '')
|
||||||
yield x.upper()
|
yield x.upper()
|
||||||
yield x.lower()
|
yield x.lower()
|
||||||
|
|
||||||
|
|
@ -42,13 +43,11 @@ def _normalize_href(base, href):
|
||||||
'''Normalize the href to be a path only relative to hostname and
|
'''Normalize the href to be a path only relative to hostname and
|
||||||
schema.'''
|
schema.'''
|
||||||
orig_href = href
|
orig_href = href
|
||||||
base = to_native(base, 'utf-8')
|
|
||||||
href = to_native(href, 'utf-8')
|
|
||||||
if not href:
|
if not href:
|
||||||
raise ValueError(href)
|
raise ValueError(href)
|
||||||
|
|
||||||
x = utils.compat.urlparse.urljoin(base, href)
|
x = urlparse.urljoin(base, href)
|
||||||
x = utils.compat.urlparse.urlsplit(x).path
|
x = urlparse.urlsplit(x).path
|
||||||
|
|
||||||
# Encoding issues:
|
# Encoding issues:
|
||||||
# - https://github.com/owncloud/contacts/issues/581
|
# - https://github.com/owncloud/contacts/issues/581
|
||||||
|
|
@ -58,9 +57,9 @@ def _normalize_href(base, href):
|
||||||
if _contains_quoted_reserved_chars(x):
|
if _contains_quoted_reserved_chars(x):
|
||||||
break
|
break
|
||||||
old_x = x
|
old_x = x
|
||||||
x = utils.compat.urlunquote(x)
|
x = urlparse.unquote(x)
|
||||||
|
|
||||||
x = utils.compat.urlquote(x, '/@%:')
|
x = urlparse.quote(x, '/@%:')
|
||||||
|
|
||||||
if orig_href == x:
|
if orig_href == x:
|
||||||
dav_logger.debug('Already normalized: {!r}'.format(x))
|
dav_logger.debug('Already normalized: {!r}'.format(x))
|
||||||
|
|
@ -129,7 +128,7 @@ class Discover(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_collection_from_url(url):
|
def _get_collection_from_url(url):
|
||||||
_, collection = url.rstrip('/').rsplit('/', 1)
|
_, collection = url.rstrip('/').rsplit('/', 1)
|
||||||
return utils.compat.urlunquote(collection)
|
return urlparse.unquote(collection)
|
||||||
|
|
||||||
def find_dav(self):
|
def find_dav(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -166,7 +165,7 @@ class Discover(object):
|
||||||
rv = root.find('.//{DAV:}current-user-principal/{DAV:}href')
|
rv = root.find('.//{DAV:}current-user-principal/{DAV:}href')
|
||||||
if rv is None:
|
if rv is None:
|
||||||
raise InvalidXMLResponse()
|
raise InvalidXMLResponse()
|
||||||
return utils.compat.urlparse.urljoin(response.url, rv.text)
|
return urlparse.urljoin(response.url, rv.text)
|
||||||
|
|
||||||
def find_home(self, url=None):
|
def find_home(self, url=None):
|
||||||
if url is None:
|
if url is None:
|
||||||
|
|
@ -182,7 +181,7 @@ class Discover(object):
|
||||||
rv = root.find('.//' + self._homeset_tag + '/{DAV:}href')
|
rv = root.find('.//' + self._homeset_tag + '/{DAV:}href')
|
||||||
if rv is None:
|
if rv is None:
|
||||||
raise InvalidXMLResponse('Couldn\'t find home-set.')
|
raise InvalidXMLResponse('Couldn\'t find home-set.')
|
||||||
return utils.compat.urlparse.urljoin(response.url, rv.text)
|
return urlparse.urljoin(response.url, rv.text)
|
||||||
|
|
||||||
def find_collections(self, url=None):
|
def find_collections(self, url=None):
|
||||||
if url is None:
|
if url is None:
|
||||||
|
|
@ -202,7 +201,7 @@ class Discover(object):
|
||||||
if href is None:
|
if href is None:
|
||||||
raise InvalidXMLResponse('Missing href tag for collection '
|
raise InvalidXMLResponse('Missing href tag for collection '
|
||||||
'props.')
|
'props.')
|
||||||
href = utils.compat.urlparse.urljoin(r.url, href.text)
|
href = urlparse.urljoin(r.url, href.text)
|
||||||
if href not in done:
|
if href not in done:
|
||||||
done.add(href)
|
done.add(href)
|
||||||
yield {'href': href}
|
yield {'href': href}
|
||||||
|
|
@ -224,9 +223,9 @@ class Discover(object):
|
||||||
return c
|
return c
|
||||||
|
|
||||||
home = self.find_home()
|
home = self.find_home()
|
||||||
url = utils.compat.urlparse.urljoin(
|
url = urlparse.urljoin(
|
||||||
home,
|
home,
|
||||||
utils.compat.urlquote(collection, '/@')
|
urlparse.quote(collection, '/@')
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -252,7 +251,8 @@ class Discover(object):
|
||||||
</D:set>
|
</D:set>
|
||||||
</D:mkcol>
|
</D:mkcol>
|
||||||
'''.format(
|
'''.format(
|
||||||
to_native(etree.tostring(etree.Element(self._resourcetype)))
|
etree.tostring(etree.Element(self._resourcetype),
|
||||||
|
encoding='unicode')
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.session.request(
|
response = self.session.request(
|
||||||
|
|
@ -299,7 +299,7 @@ class DavSession(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def init_and_remaining_args(cls, **kwargs):
|
def init_and_remaining_args(cls, **kwargs):
|
||||||
argspec = getargspec_ish(cls.__init__)
|
argspec = getfullargspec(cls.__init__)
|
||||||
self_args, remainder = \
|
self_args, remainder = \
|
||||||
utils.split_dict(kwargs, argspec.args.__contains__)
|
utils.split_dict(kwargs, argspec.args.__contains__)
|
||||||
|
|
||||||
|
|
@ -321,12 +321,12 @@ class DavSession(object):
|
||||||
|
|
||||||
@utils.cached_property
|
@utils.cached_property
|
||||||
def parsed_url(self):
|
def parsed_url(self):
|
||||||
return utils.compat.urlparse.urlparse(self.url)
|
return urlparse.urlparse(self.url)
|
||||||
|
|
||||||
def request(self, method, path, **kwargs):
|
def request(self, method, path, **kwargs):
|
||||||
url = self.url
|
url = self.url
|
||||||
if path:
|
if path:
|
||||||
url = utils.compat.urlparse.urljoin(self.url, path)
|
url = urlparse.urljoin(self.url, path)
|
||||||
|
|
||||||
more = dict(self._settings)
|
more = dict(self._settings)
|
||||||
more.update(kwargs)
|
more.update(kwargs)
|
||||||
|
|
@ -379,7 +379,6 @@ class DavStorage(Storage):
|
||||||
self.session_class.init_and_remaining_args(**kwargs)
|
self.session_class.init_and_remaining_args(**kwargs)
|
||||||
super(DavStorage, self).__init__(**kwargs)
|
super(DavStorage, self).__init__(**kwargs)
|
||||||
|
|
||||||
if not PY2:
|
|
||||||
import inspect
|
import inspect
|
||||||
__init__.__signature__ = inspect.signature(session_class.__init__)
|
__init__.__signature__ = inspect.signature(session_class.__init__)
|
||||||
|
|
||||||
|
|
@ -602,7 +601,7 @@ class DavStorage(Storage):
|
||||||
</D:prop>
|
</D:prop>
|
||||||
</D:propfind>
|
</D:propfind>
|
||||||
'''.format(
|
'''.format(
|
||||||
to_native(etree.tostring(etree.Element(xpath)))
|
etree.tostring(etree.Element(xpath), encoding='unicode')
|
||||||
)
|
)
|
||||||
|
|
||||||
headers = self.session.get_default_headers()
|
headers = self.session.get_default_headers()
|
||||||
|
|
@ -639,7 +638,7 @@ class DavStorage(Storage):
|
||||||
</D:prop>
|
</D:prop>
|
||||||
</D:set>
|
</D:set>
|
||||||
</D:propertyupdate>
|
</D:propertyupdate>
|
||||||
'''.format(to_native(etree.tostring(element)))
|
'''.format(etree.tostring(element, encoding='unicode'))
|
||||||
|
|
||||||
self.session.request(
|
self.session.request(
|
||||||
'PROPPATCH', '',
|
'PROPPATCH', '',
|
||||||
|
|
@ -723,11 +722,11 @@ class CaldavStorage(DavStorage):
|
||||||
namespace = dict(datetime.__dict__)
|
namespace = dict(datetime.__dict__)
|
||||||
namespace['start_date'] = self.start_date = \
|
namespace['start_date'] = self.start_date = \
|
||||||
(eval(start_date, namespace)
|
(eval(start_date, namespace)
|
||||||
if isinstance(start_date, (bytes, text_type))
|
if isinstance(start_date, (bytes, str))
|
||||||
else start_date)
|
else start_date)
|
||||||
self.end_date = \
|
self.end_date = \
|
||||||
(eval(end_date, namespace)
|
(eval(end_date, namespace)
|
||||||
if isinstance(end_date, (bytes, text_type))
|
if isinstance(end_date, (bytes, str))
|
||||||
else end_date)
|
else end_date)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ from atomicwrites import atomic_write
|
||||||
from .base import Item, Storage, normalize_meta_value
|
from .base import Item, 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, expand_path, generate_href, get_etag_from_file
|
||||||
from ..utils.compat import text_type, to_native
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -42,7 +41,7 @@ class FilesystemStorage(Storage):
|
||||||
def __init__(self, path, fileext, encoding='utf-8', post_hook=None,
|
def __init__(self, path, fileext, encoding='utf-8', post_hook=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
super(FilesystemStorage, self).__init__(**kwargs)
|
super(FilesystemStorage, self).__init__(**kwargs)
|
||||||
path = expand_path(to_native(path, encoding))
|
path = expand_path(path)
|
||||||
checkdir(path, create=False)
|
checkdir(path, create=False)
|
||||||
self.path = path
|
self.path = path
|
||||||
self.encoding = encoding
|
self.encoding = encoding
|
||||||
|
|
@ -70,11 +69,9 @@ class FilesystemStorage(Storage):
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_collection(cls, collection, **kwargs):
|
def create_collection(cls, collection, **kwargs):
|
||||||
kwargs = dict(kwargs)
|
kwargs = dict(kwargs)
|
||||||
encoding = kwargs.get('encoding', 'utf-8')
|
path = kwargs['path']
|
||||||
path = to_native(kwargs['path'], encoding)
|
|
||||||
|
|
||||||
if collection is not None:
|
if collection is not None:
|
||||||
collection = to_native(collection, encoding)
|
|
||||||
path = os.path.join(path, collection)
|
path = os.path.join(path, collection)
|
||||||
|
|
||||||
checkdir(expand_path(path), create=True)
|
checkdir(expand_path(path), create=True)
|
||||||
|
|
@ -84,7 +81,7 @@ class FilesystemStorage(Storage):
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def _get_filepath(self, href):
|
def _get_filepath(self, href):
|
||||||
return os.path.join(self.path, to_native(href, self.encoding))
|
return os.path.join(self.path, href)
|
||||||
|
|
||||||
def _get_href(self, ident):
|
def _get_href(self, ident):
|
||||||
return generate_href(ident) + self.fileext
|
return generate_href(ident) + self.fileext
|
||||||
|
|
@ -108,7 +105,7 @@ class FilesystemStorage(Storage):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def upload(self, item):
|
def upload(self, item):
|
||||||
if not isinstance(item.raw, text_type):
|
if not isinstance(item.raw, str):
|
||||||
raise TypeError('item.raw must be a unicode string.')
|
raise TypeError('item.raw must be a unicode string.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -151,7 +148,7 @@ class FilesystemStorage(Storage):
|
||||||
if etag != actual_etag:
|
if etag != actual_etag:
|
||||||
raise exceptions.WrongEtagError(etag, actual_etag)
|
raise exceptions.WrongEtagError(etag, actual_etag)
|
||||||
|
|
||||||
if not isinstance(item.raw, text_type):
|
if not isinstance(item.raw, str):
|
||||||
raise TypeError('item.raw must be a unicode string.')
|
raise TypeError('item.raw must be a unicode string.')
|
||||||
|
|
||||||
with atomic_write(fpath, mode='wb', overwrite=True) as f:
|
with atomic_write(fpath, mode='wb', overwrite=True) as f:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
from atomicwrites import atomic_write
|
from atomicwrites import atomic_write
|
||||||
|
|
||||||
|
|
@ -10,7 +11,8 @@ import click
|
||||||
from click_threading import get_ui_worker
|
from click_threading import get_ui_worker
|
||||||
|
|
||||||
from . import base, dav
|
from . import base, dav
|
||||||
from .. import exceptions, utils
|
from .. import exceptions
|
||||||
|
from ..utils import expand_path, open_graphical_browser
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -37,7 +39,7 @@ class GoogleSession(dav.DavSession):
|
||||||
if not have_oauth2:
|
if not have_oauth2:
|
||||||
raise exceptions.UserError('requests-oauthlib not installed')
|
raise exceptions.UserError('requests-oauthlib not installed')
|
||||||
|
|
||||||
token_file = utils.expand_path(token_file)
|
token_file = expand_path(token_file)
|
||||||
ui_worker = get_ui_worker()
|
ui_worker = get_ui_worker()
|
||||||
f = lambda: self._init_token(token_file, client_id, client_secret)
|
f = lambda: self._init_token(token_file, client_id, client_secret)
|
||||||
ui_worker.put(f)
|
ui_worker.put(f)
|
||||||
|
|
@ -75,7 +77,7 @@ class GoogleSession(dav.DavSession):
|
||||||
access_type='offline', approval_prompt='force')
|
access_type='offline', approval_prompt='force')
|
||||||
click.echo('Opening {} ...'.format(authorization_url))
|
click.echo('Opening {} ...'.format(authorization_url))
|
||||||
try:
|
try:
|
||||||
utils.open_graphical_browser(authorization_url)
|
open_graphical_browser(authorization_url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(str(e))
|
logger.warning(str(e))
|
||||||
|
|
||||||
|
|
@ -118,7 +120,7 @@ class GoogleCalendarStorage(dav.CaldavStorage):
|
||||||
parts = url.rstrip('/').split('/')
|
parts = url.rstrip('/').split('/')
|
||||||
parts.pop()
|
parts.pop()
|
||||||
collection = parts.pop()
|
collection = parts.pop()
|
||||||
return utils.compat.urlunquote(collection)
|
return urlparse.unquote(collection)
|
||||||
|
|
||||||
storage_name = 'google_calendar'
|
storage_name = 'google_calendar'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
from .base import Item, Storage
|
from .base import Item, Storage
|
||||||
from .. import exceptions
|
from .. import exceptions
|
||||||
from ..utils import expand_path
|
from ..utils import expand_path
|
||||||
from ..utils.compat import iteritems, text_type, urlparse
|
|
||||||
from ..utils.http import request
|
from ..utils.http import request
|
||||||
from ..utils.vobject import split_collection
|
from ..utils.vobject import split_collection
|
||||||
|
|
||||||
|
|
@ -39,7 +40,7 @@ def prepare_auth(auth, username, password):
|
||||||
|
|
||||||
|
|
||||||
def prepare_verify(verify, verify_fingerprint):
|
def prepare_verify(verify, verify_fingerprint):
|
||||||
if isinstance(verify, (text_type, bytes)):
|
if isinstance(verify, (str, bytes)):
|
||||||
verify = expand_path(verify)
|
verify = expand_path(verify)
|
||||||
elif not isinstance(verify, bool):
|
elif not isinstance(verify, bool):
|
||||||
raise exceptions.UserError('Invalid value for verify ({}), '
|
raise exceptions.UserError('Invalid value for verify ({}), '
|
||||||
|
|
@ -47,7 +48,7 @@ def prepare_verify(verify, verify_fingerprint):
|
||||||
.format(verify))
|
.format(verify))
|
||||||
|
|
||||||
if verify_fingerprint is not None:
|
if verify_fingerprint is not None:
|
||||||
if not isinstance(verify_fingerprint, (bytes, text_type)):
|
if not isinstance(verify_fingerprint, (bytes, str)):
|
||||||
raise exceptions.UserError('Invalid value for verify_fingerprint '
|
raise exceptions.UserError('Invalid value for verify_fingerprint '
|
||||||
'({}), must be a string or null.'
|
'({}), must be a string or null.'
|
||||||
.format(verify_fingerprint))
|
.format(verify_fingerprint))
|
||||||
|
|
@ -64,7 +65,7 @@ def prepare_verify(verify, verify_fingerprint):
|
||||||
|
|
||||||
|
|
||||||
def prepare_client_cert(cert):
|
def prepare_client_cert(cert):
|
||||||
if isinstance(cert, (text_type, bytes)):
|
if isinstance(cert, (str, bytes)):
|
||||||
cert = expand_path(cert)
|
cert = expand_path(cert)
|
||||||
elif isinstance(cert, list):
|
elif isinstance(cert, list):
|
||||||
cert = tuple(map(prepare_client_cert, cert))
|
cert = tuple(map(prepare_client_cert, cert))
|
||||||
|
|
@ -154,7 +155,7 @@ class HttpStorage(Storage):
|
||||||
etag = item.hash
|
etag = item.hash
|
||||||
self._items[item.ident] = item, etag
|
self._items[item.ident] = item, etag
|
||||||
|
|
||||||
return ((href, etag) for href, (item, etag) in iteritems(self._items))
|
return ((href, etag) for href, (item, etag) in self._items.items())
|
||||||
|
|
||||||
def get(self, href):
|
def get(self, href):
|
||||||
if self._items is None:
|
if self._items is None:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ things, and plugging in an account "just works".
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import quote as urlquote, urljoin
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
@ -21,9 +22,6 @@ DRAFT_VERSION = '05'
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
urljoin = utils.compat.urlparse.urljoin
|
|
||||||
urlquote = utils.compat.urlquote
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_slash(dir):
|
def _ensure_slash(dir):
|
||||||
return dir.rstrip('/') + '/'
|
return dir.rstrip('/') + '/'
|
||||||
|
|
@ -33,7 +31,7 @@ def _iter_listing(json):
|
||||||
new_listing = '@context' in json # draft-02 and beyond
|
new_listing = '@context' in json # draft-02 and beyond
|
||||||
if new_listing:
|
if new_listing:
|
||||||
json = json['items']
|
json = json['items']
|
||||||
for name, info in utils.compat.iteritems(json):
|
for name, info in json.items():
|
||||||
if not new_listing:
|
if not new_listing:
|
||||||
info = {'ETag': info}
|
info = {'ETag': info}
|
||||||
yield name, info
|
yield name, info
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from atomicwrites import atomic_write
|
||||||
from .base import Item, Storage
|
from .base import Item, Storage
|
||||||
from .. import exceptions
|
from .. import exceptions
|
||||||
from ..utils import checkfile, expand_path
|
from ..utils import checkfile, expand_path
|
||||||
from ..utils.compat import iteritems, itervalues
|
|
||||||
from ..utils.vobject import join_collection, split_collection
|
from ..utils.vobject import join_collection, split_collection
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -167,7 +166,7 @@ class SingleFileStorage(Storage):
|
||||||
etag = item.hash
|
etag = item.hash
|
||||||
self._items[item.ident] = item, etag
|
self._items[item.ident] = item, etag
|
||||||
|
|
||||||
return ((href, etag) for href, (item, etag) in iteritems(self._items))
|
return ((href, etag) for href, (item, etag) in self._items.items())
|
||||||
|
|
||||||
def get(self, href):
|
def get(self, href):
|
||||||
if self._items is None or not self._at_once:
|
if self._items is None or not self._at_once:
|
||||||
|
|
@ -218,7 +217,7 @@ class SingleFileStorage(Storage):
|
||||||
'synchronization and make sure absolutely no other program is '
|
'synchronization and make sure absolutely no other program is '
|
||||||
'writing into the same file.'.format(self.path))
|
'writing into the same file.'.format(self.path))
|
||||||
text = join_collection(
|
text = join_collection(
|
||||||
(item.raw for item, etag in itervalues(self._items)),
|
item.raw for item, etag in self._items.values()
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with atomic_write(self.path, mode='wb', overwrite=True) as f:
|
with atomic_write(self.path, mode='wb', overwrite=True) as f:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import logging
|
||||||
|
|
||||||
from . import exceptions
|
from . import exceptions
|
||||||
from .utils import uniq
|
from .utils import uniq
|
||||||
from .utils.compat import iteritems, text_type
|
|
||||||
|
|
||||||
sync_logger = logging.getLogger(__name__)
|
sync_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -92,7 +91,7 @@ class StorageSyncer(object):
|
||||||
def prepare_idents(self):
|
def prepare_idents(self):
|
||||||
href_to_status = dict((meta['href'], (ident, meta))
|
href_to_status = dict((meta['href'], (ident, meta))
|
||||||
for ident, meta
|
for ident, meta
|
||||||
in iteritems(self.status))
|
in self.status.items())
|
||||||
|
|
||||||
prefetch = {}
|
prefetch = {}
|
||||||
self.idents = {}
|
self.idents = {}
|
||||||
|
|
@ -206,11 +205,11 @@ def sync(storage_a, storage_b, status, conflict_resolution=None,
|
||||||
|
|
||||||
a_info = storage_a.syncer_class(storage_a, dict(
|
a_info = storage_a.syncer_class(storage_a, dict(
|
||||||
(ident, meta_a)
|
(ident, meta_a)
|
||||||
for ident, (meta_a, meta_b) in iteritems(status)
|
for ident, (meta_a, meta_b) in status.items()
|
||||||
))
|
))
|
||||||
b_info = storage_b.syncer_class(storage_b, dict(
|
b_info = storage_b.syncer_class(storage_b, dict(
|
||||||
(ident, meta_b)
|
(ident, meta_b)
|
||||||
for ident, (meta_a, meta_b) in iteritems(status)
|
for ident, (meta_a, meta_b) in status.items()
|
||||||
))
|
))
|
||||||
|
|
||||||
a_info.prepare_idents()
|
a_info.prepare_idents()
|
||||||
|
|
@ -275,7 +274,7 @@ def _action_update(ident, source, dest):
|
||||||
dest_href = dest_meta['href']
|
dest_href = dest_meta['href']
|
||||||
dest_etag = dest.storage.update(dest_href, source_meta['item'],
|
dest_etag = dest.storage.update(dest_href, source_meta['item'],
|
||||||
dest_meta['etag'])
|
dest_meta['etag'])
|
||||||
assert isinstance(dest_etag, (bytes, text_type))
|
assert isinstance(dest_etag, (bytes, str))
|
||||||
|
|
||||||
source.status[ident] = _compress_meta(source_meta)
|
source.status[ident] = _compress_meta(source_meta)
|
||||||
dest.status[ident] = {
|
dest.status[ident] = {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from .compat import getargspec_ish, iteritems, to_unicode
|
from inspect import getfullargspec
|
||||||
|
|
||||||
from .. import exceptions
|
from .. import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ def expand_path(p):
|
||||||
|
|
||||||
def split_dict(d, f):
|
def split_dict(d, f):
|
||||||
'''Puts key into first dict if f(key), otherwise in second dict'''
|
'''Puts key into first dict if f(key), otherwise in second dict'''
|
||||||
a, b = split_sequence(iteritems(d), lambda item: f(item[0]))
|
a, b = split_sequence(d.items(), lambda item: f(item[0]))
|
||||||
return dict(a), dict(b)
|
return dict(a), dict(b)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -77,7 +78,7 @@ def get_storage_init_specs(cls, stop_at=object):
|
||||||
if cls is stop_at:
|
if cls is stop_at:
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
spec = getargspec_ish(cls.__init__)
|
spec = getfullargspec(cls.__init__)
|
||||||
traverse_superclass = getattr(cls.__init__, '_traverse_superclass', True)
|
traverse_superclass = getattr(cls.__init__, '_traverse_superclass', True)
|
||||||
if traverse_superclass:
|
if traverse_superclass:
|
||||||
if traverse_superclass is True: # noqa
|
if traverse_superclass is True: # noqa
|
||||||
|
|
@ -178,7 +179,7 @@ def generate_href(ident=None, safe=SAFE_UID_CHARS):
|
||||||
UUID.
|
UUID.
|
||||||
'''
|
'''
|
||||||
if not ident or not href_safe(ident, safe):
|
if not ident or not href_safe(ident, safe):
|
||||||
return to_unicode(uuid.uuid4().hex)
|
return str(uuid.uuid4())
|
||||||
else:
|
else:
|
||||||
return ident
|
return ident
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import sys
|
|
||||||
|
|
||||||
PY2 = sys.version_info[0] == 2
|
|
||||||
|
|
||||||
if sys.version_info < (3, 3) and \
|
|
||||||
sys.version_info[:2] != (2, 7): # pragma: no cover
|
|
||||||
raise RuntimeError(
|
|
||||||
'vdirsyncer only works on Python versions 2.7.x and 3.3+'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def to_unicode(x, encoding='ascii'):
|
|
||||||
if not isinstance(x, text_type):
|
|
||||||
x = x.decode(encoding)
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def to_bytes(x, encoding='ascii'):
|
|
||||||
if not isinstance(x, bytes):
|
|
||||||
x = x.encode(encoding)
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def _wrap_native(f, encoding='utf-8'):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapper(x, *a, **kw):
|
|
||||||
to_orig = to_unicode if isinstance(x, text_type) else to_bytes
|
|
||||||
return to_orig(f(to_native(x, encoding), *a, **kw), encoding)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
if PY2: # pragma: no cover
|
|
||||||
import urlparse
|
|
||||||
import urllib as _urllib
|
|
||||||
from inspect import getargspec as getargspec_ish # noqa
|
|
||||||
|
|
||||||
# Horrible hack to make urllib play nice with u'...' urls from requests
|
|
||||||
urlquote = _wrap_native(_urllib.quote)
|
|
||||||
urlunquote = _wrap_native(_urllib.unquote)
|
|
||||||
|
|
||||||
text_type = unicode # noqa
|
|
||||||
iteritems = lambda x: x.iteritems()
|
|
||||||
itervalues = lambda x: x.itervalues()
|
|
||||||
to_native = to_bytes
|
|
||||||
|
|
||||||
else: # pragma: no cover
|
|
||||||
import urllib.parse as urlparse
|
|
||||||
from inspect import getfullargspec as getargspec_ish # noqa
|
|
||||||
|
|
||||||
urlquote = urlparse.quote
|
|
||||||
urlunquote = urlparse.unquote
|
|
||||||
text_type = str
|
|
||||||
iteritems = lambda x: x.items()
|
|
||||||
itervalues = lambda x: x.values()
|
|
||||||
to_native = to_unicode
|
|
||||||
|
|
||||||
|
|
||||||
def with_metaclass(meta, *bases):
|
|
||||||
'''Original code from six, by Benjamin Peterson.'''
|
|
||||||
class metaclass(meta):
|
|
||||||
def __new__(cls, name, this_bases, d):
|
|
||||||
return meta(name, bases, d)
|
|
||||||
return type.__new__(metaclass, 'temporary_class', (), {})
|
|
||||||
|
|
@ -4,14 +4,9 @@ import hashlib
|
||||||
from itertools import chain, tee
|
from itertools import chain, tee
|
||||||
|
|
||||||
from . import cached_property, uniq
|
from . import cached_property, uniq
|
||||||
from .compat import itervalues, text_type, to_unicode
|
|
||||||
|
|
||||||
|
|
||||||
def _prepare_props(*x):
|
IGNORE_PROPS = (
|
||||||
return tuple(map(to_unicode, x))
|
|
||||||
|
|
||||||
|
|
||||||
IGNORE_PROPS = _prepare_props(
|
|
||||||
# PRODID is changed by radicale for some reason after upload
|
# PRODID is changed by radicale for some reason after upload
|
||||||
'PRODID',
|
'PRODID',
|
||||||
# X-RADICALE-NAME is used by radicale, because hrefs don't really exist in
|
# X-RADICALE-NAME is used by radicale, because hrefs don't really exist in
|
||||||
|
|
@ -34,7 +29,6 @@ IGNORE_PROPS = _prepare_props(
|
||||||
'DTSTAMP',
|
'DTSTAMP',
|
||||||
'UID',
|
'UID',
|
||||||
)
|
)
|
||||||
del _prepare_props
|
|
||||||
|
|
||||||
|
|
||||||
class Item(object):
|
class Item(object):
|
||||||
|
|
@ -43,7 +37,7 @@ class Item(object):
|
||||||
VCARD'''
|
VCARD'''
|
||||||
|
|
||||||
def __init__(self, raw):
|
def __init__(self, raw):
|
||||||
assert isinstance(raw, text_type)
|
assert isinstance(raw, str)
|
||||||
self._raw = raw
|
self._raw = raw
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
|
@ -111,7 +105,7 @@ def hash_item(text):
|
||||||
|
|
||||||
|
|
||||||
def split_collection(text):
|
def split_collection(text):
|
||||||
assert isinstance(text, text_type)
|
assert isinstance(text, str)
|
||||||
inline = []
|
inline = []
|
||||||
items = {} # uid => item
|
items = {} # uid => item
|
||||||
ungrouped_items = []
|
ungrouped_items = []
|
||||||
|
|
@ -141,7 +135,7 @@ def split_collection(text):
|
||||||
for main in _Component.parse(text, multiple=True):
|
for main in _Component.parse(text, multiple=True):
|
||||||
inner(main, main)
|
inner(main, main)
|
||||||
|
|
||||||
for item in chain(itervalues(items), ungrouped_items):
|
for item in chain(items.values(), ungrouped_items):
|
||||||
item.subcomponents.extend(inline)
|
item.subcomponents.extend(inline)
|
||||||
yield u'\r\n'.join(item.dump_lines())
|
yield u'\r\n'.join(item.dump_lines())
|
||||||
|
|
||||||
|
|
@ -240,7 +234,7 @@ class _Component(object):
|
||||||
def parse(cls, lines, multiple=False):
|
def parse(cls, lines, multiple=False):
|
||||||
if isinstance(lines, bytes):
|
if isinstance(lines, bytes):
|
||||||
lines = lines.decode('utf-8')
|
lines = lines.decode('utf-8')
|
||||||
if isinstance(lines, text_type):
|
if isinstance(lines, str):
|
||||||
lines = lines.splitlines()
|
lines = lines.splitlines()
|
||||||
|
|
||||||
stack = []
|
stack = []
|
||||||
|
|
@ -301,7 +295,7 @@ class _Component(object):
|
||||||
self.props = new_lines
|
self.props = new_lines
|
||||||
|
|
||||||
def __setitem__(self, key, val):
|
def __setitem__(self, key, val):
|
||||||
assert isinstance(val, text_type)
|
assert isinstance(val, str)
|
||||||
assert u'\n' not in val
|
assert u'\n' not in val
|
||||||
del self[key]
|
del self[key]
|
||||||
line = u'{}:{}'.format(key, val)
|
line = u'{}:{}'.format(key, val)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue