From 18d8bb9fc23f012d897506bc9e22966c2ae19e6d Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 8 Sep 2016 12:18:36 +0200 Subject: [PATCH] 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 --- .travis.yml | 68 ---------------------- CHANGELOG.rst | 5 ++ docs/installation.rst | 2 +- scripts/make_travisconf.py | 8 +-- tests/cli/test_fetchparams.py | 2 - tests/cli/test_sync.py | 6 +- tests/cli/test_utils.py | 28 +-------- tests/conftest.py | 6 -- tests/storage/__init__.py | 12 ++-- tests/storage/servers/radicale/__init__.py | 4 +- tests/utils/test_main.py | 7 +-- vdirsyncer/__init__.py | 11 ++++ vdirsyncer/cli/__init__.py | 27 +-------- vdirsyncer/cli/config.py | 9 ++- vdirsyncer/cli/tasks.py | 14 ++--- vdirsyncer/cli/utils.py | 25 ++------ vdirsyncer/storage/base.py | 9 +-- vdirsyncer/storage/dav.py | 51 ++++++++-------- vdirsyncer/storage/filesystem.py | 13 ++--- vdirsyncer/storage/google.py | 10 ++-- vdirsyncer/storage/http.py | 11 ++-- vdirsyncer/storage/remotestorage.py | 6 +- vdirsyncer/storage/singlefile.py | 5 +- vdirsyncer/sync.py | 9 ++- vdirsyncer/utils/__init__.py | 9 +-- vdirsyncer/utils/compat.py | 66 --------------------- vdirsyncer/utils/vobject.py | 18 ++---- 27 files changed, 107 insertions(+), 334 deletions(-) delete mode 100644 vdirsyncer/utils/compat.py diff --git a/.travis.yml b/.travis.yml index dc035c8..1796b32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,70 +14,6 @@ "language": "python", "matrix": { "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", "python": "3.3" @@ -186,10 +122,6 @@ "env": "BUILD=test DAV_SERVER=davical REQUIREMENTS=minimal BUILD_PRS=false", "python": "3.5" }, - { - "env": "BUILD=style BUILD_PRS=true", - "python": "pypy" - }, { "env": "BUILD=test", "language": "generic", diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 57e3e23..962b91e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,11 @@ Package maintainers and users who have to manually update their installation may want to subscribe to `GitHub's tag feed `_. +Version 0.13.0 +============== + +- Python 2 is no longer supported at all. See :gh:`219`. + Version 0.12.1 ============== diff --git a/docs/installation.rst b/docs/installation.rst index 2d6b4a2..7e3bd57 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -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 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`` - ``zlib`` diff --git a/scripts/make_travisconf.py b/scripts/make_travisconf.py index e088896..cecb194 100644 --- a/scripts/make_travisconf.py +++ b/scripts/make_travisconf.py @@ -38,7 +38,7 @@ cfg['script'] = [script(""" 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({ 'python': python, '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", "davical") rs_servers = ("mysteryshack",) - elif python == "2.7": - dav_servers = ("owncloud", "nextcloud", "baikal", "davical") - rs_servers = ("mysteryshack",) - elif python == "pypy": - dav_servers = () - rs_servers = () else: dav_servers = ("radicale",) rs_servers = () diff --git a/tests/cli/test_fetchparams.py b/tests/cli/test_fetchparams.py index 78aba81..263fc9d 100644 --- a/tests/cli/test_fetchparams.py +++ b/tests/cli/test_fetchparams.py @@ -9,7 +9,6 @@ import pytest from vdirsyncer import exceptions from vdirsyncer.cli.fetchparams import STRATEGIES, expand_fetch_params -from vdirsyncer.utils.compat import PY2 @pytest.fixture @@ -90,7 +89,6 @@ def test_key_conflict(monkeypatch, mystrategy): 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)) def test_fuzzing(s, t, mystrategy): config = expand_fetch_params({ diff --git a/tests/cli/test_sync.py b/tests/cli/test_sync.py index a93951d..843ca34 100644 --- a/tests/cli/test_sync.py +++ b/tests/cli/test_sync.py @@ -9,8 +9,6 @@ from hypothesis import example, given import pytest -from vdirsyncer.utils.compat import PY2, to_native, to_unicode - def test_simple_run(tmpdir, runner): runner.write_with_general(dedent(''' @@ -277,7 +275,6 @@ def test_multiple_pairs(tmpdir, runner): st.characters( blacklist_characters=set( 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 blacklist_categories=set(['Cs']) @@ -289,7 +286,6 @@ def test_multiple_pairs(tmpdir, runner): )) @example(collections=[u'persönlich']) def test_create_collections(subtest, collections): - collections = set(to_native(x, 'utf-8') for x in collections) @subtest def test_inner(tmpdir, runner): @@ -325,7 +321,7 @@ def test_create_collections(subtest, collections): # Quoted from # https://stackoverflow.com/questions/18137554/how-to-convert-path-to-mac-os-x-path-the-almost-nfd-normal-form # noqa u = lambda xs: set( - unicodedata.normalize('NFKD', to_unicode(x, 'utf-8')) + unicodedata.normalize('NFKD', x) for x in xs ) assert u(x.basename for x in tmpdir.join('foo').listdir()) == \ diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index 39330e5..0add86a 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -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.cli.utils import coerce_native, handle_cli_error - - -@given(one_of( - binary(), - booleans(), - complex_numbers(), - floats(), - integers(), - none(), - text() -)) -def test_coerce_native_fuzzing(s): - coerce_native(s) +from vdirsyncer.cli.utils import handle_cli_error def test_handle_cli_error(capsys): diff --git a/tests/conftest.py b/tests/conftest.py index 688b3cd..e0a07d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,12 +17,6 @@ def setup_logging(): 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: import pytest_benchmark except ImportError: diff --git a/tests/storage/__init__.py b/tests/storage/__init__.py index 908b06b..0ec4b94 100644 --- a/tests/storage/__init__.py +++ b/tests/storage/__init__.py @@ -3,6 +3,7 @@ import random import textwrap +from urllib.parse import quote as urlquote, unquote as urlunquote import hypothesis.strategies as st from hypothesis import given @@ -11,7 +12,6 @@ import pytest import vdirsyncer.exceptions as exceptions 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, \ assert_item_equals, normalize_item, printable_characters_strategy @@ -80,8 +80,8 @@ class StorageTests(object): hrefs.sort() assert hrefs == sorted(s.list()) for href, etag in hrefs: - assert isinstance(href, (text_type, bytes)) - assert isinstance(etag, (text_type, bytes)) + assert isinstance(href, (str, bytes)) + assert isinstance(etag, (str, bytes)) assert s.has(href) item, etag2 = s.get(href) assert etag == etag2 @@ -114,7 +114,7 @@ class StorageTests(object): new_item = get_item(uid=item.uid) new_etag = s.update(href, new_item, etag) # 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) def test_update_nonexisting(self, s, get_item): @@ -162,7 +162,7 @@ class StorageTests(object): assert dict( (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 def test_repr(self, s, get_storage_args): @@ -277,7 +277,7 @@ class StorageTests(object): s.set_meta('displayname', x) rv = s.get_meta('displayname') assert rv == x - assert isinstance(rv, text_type) + assert isinstance(rv, str) @given(value=st.one_of( st.none(), diff --git a/tests/storage/servers/radicale/__init__.py b/tests/storage/servers/radicale/__init__.py index 3abfab2..ae0f2f7 100644 --- a/tests/storage/servers/radicale/__init__.py +++ b/tests/storage/servers/radicale/__init__.py @@ -3,9 +3,9 @@ import os 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.requests_intercept diff --git a/tests/utils/test_main.py b/tests/utils/test_main.py index 1790f2a..6e84846 100644 --- a/tests/utils/test_main.py +++ b/tests/utils/test_main.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import logging -import platform import click_log @@ -13,7 +12,6 @@ from vdirsyncer import utils # These modules might be uninitialized and unavailable if not explicitly # imported -import vdirsyncer.utils.compat # noqa import vdirsyncer.utils.http # noqa @@ -43,11 +41,8 @@ def test_request_ssl(httpsserver): def _fingerprints_broken(): 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') - return broken_urllib3 and not tolerant_python + return broken_urllib3 @pytest.mark.skipif(_fingerprints_broken(), diff --git a/vdirsyncer/__init__.py b/vdirsyncer/__init__.py index f02cc06..e658625 100644 --- a/vdirsyncer/__init__.py +++ b/vdirsyncer/__init__.py @@ -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 import requests if 'dist-packages' not in requests.__file__: @@ -41,3 +51,4 @@ def _detect_faulty_requests(): # pragma: no cover sys.exit(1) _detect_faulty_requests() +del _detect_faulty_requests diff --git a/vdirsyncer/cli/__init__.py b/vdirsyncer/cli/__init__.py index 2c6c80c..a4b2309 100644 --- a/vdirsyncer/cli/__init__.py +++ b/vdirsyncer/cli/__init__.py @@ -8,8 +8,7 @@ import click import click_log -from .. import PROJECT_HOME, __version__, exceptions -from ..utils.compat import PY2 +from .. import __version__ cli_logger = logging.getLogger(__name__) @@ -38,29 +37,6 @@ def catch_errors(f): 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_log.init('vdirsyncer') @click_log.simple_verbosity_option() @@ -76,7 +52,6 @@ def app(ctx, config): if not ctx.config: ctx.config = load_config(config) - _check_python2(ctx.config) main = app diff --git a/vdirsyncer/cli/config.py b/vdirsyncer/cli/config.py index 87a4bd4..e2c296e 100644 --- a/vdirsyncer/cli/config.py +++ b/vdirsyncer/cli/config.py @@ -7,14 +7,13 @@ from . import cli_logger from .fetchparams import expand_fetch_params from .. import PROJECT_HOME, exceptions from ..utils import cached_property, expand_path -from ..utils.compat import text_type try: from ConfigParser import RawConfigParser except ImportError: from configparser import RawConfigParser -GENERAL_ALL = frozenset(['status_path', 'python2']) # XXX: Py2 +GENERAL_ALL = frozenset(['status_path']) GENERAL_REQUIRED = frozenset(['status_path']) 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): try: - if isinstance(collection, (text_type, bytes)): + if isinstance(collection, (str, bytes)): collection_name = collection elif isinstance(collection, list): e = ValueError( @@ -78,11 +77,11 @@ def _validate_pair_section(pair_config): if len(collection) != 3: raise e - if not isinstance(collection[0], (text_type, bytes)): + if not isinstance(collection[0], (str, bytes)): raise e 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 collection_name = collection[0] diff --git a/vdirsyncer/cli/tasks.py b/vdirsyncer/cli/tasks.py index 1303892..96e5574 100644 --- a/vdirsyncer/cli/tasks.py +++ b/vdirsyncer/cli/tasks.py @@ -4,13 +4,12 @@ import functools import json from .config import CollectionConfig -from .utils import JobFailed, cli_logger, coerce_native, \ - collections_for_pair, get_status_name, handle_cli_error, load_status, \ - save_status, storage_class_from_config, storage_instance_from_config +from .utils import JobFailed, cli_logger, collections_for_pair, \ + get_status_name, handle_cli_error, load_status, save_status, \ + storage_class_from_config, storage_instance_from_config from .. import exceptions from ..sync import sync -from ..utils.compat import to_unicode 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 new_workers = -1 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: config_a, config_b = all_collections[collection_name] except KeyError: @@ -51,12 +47,12 @@ def sync_collection(wq, collection, general, force_delete): status_name = get_status_name(pair.name, collection.name) 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, collection.name, data_type='items') or {} cli_logger.debug('Loaded status for {}' - .format(coerce_native(status_name))) + .format(status_name)) a = storage_instance_from_config(collection.config_a) b = storage_instance_from_config(collection.config_b) diff --git a/vdirsyncer/cli/utils.py b/vdirsyncer/cli/utils.py index 6562891..80249c4 100644 --- a/vdirsyncer/cli/utils.py +++ b/vdirsyncer/cli/utils.py @@ -19,7 +19,6 @@ from . import cli_logger from .. import BUGTRACKER_HOME, DOCS_HOME, exceptions from ..sync import IdentConflict, StorageEmpty, SyncConflict from ..utils import expand_path, get_storage_init_args -from ..utils.compat import to_native try: import Queue as queue @@ -147,8 +146,7 @@ def handle_cli_error(status_name=None): import traceback tb = traceback.format_tb(tb) if status_name: - msg = 'Unknown error occured for {}'.format( - coerce_native(status_name)) + msg = 'Unknown error occured for {}'.format(status_name) else: msg = 'Unknown error occured' @@ -293,7 +291,7 @@ def _handle_collection_not_found(config, collection, e=None): def _print_collections(base_config, discovered): 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(): collection = args['collection'] if collection is None: @@ -308,7 +306,7 @@ def _print_collections(base_config, discovered): cli_logger.info(' - {}{}'.format( json.dumps(collection), - ' ("{}")'.format(coerce_native(displayname)) + ' ("{}")'.format(displayname) if displayname and displayname != collection else '' )) @@ -465,7 +463,7 @@ def handle_storage_init_error(cls, config): u'{} storage doesn\'t take the parameters: {}' .format(cls.storage_name, u', '.join(invalid))) - if not problems: # XXX: Py2: Proper reraise + if not problems: raise e raise exceptions.UserError( @@ -585,18 +583,3 @@ def assert_permissions(path, wanted): cli_logger.warning('Correcting permissions of {} from {:o} to {:o}' .format(path, permissions, 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) diff --git a/vdirsyncer/storage/base.py b/vdirsyncer/storage/base.py index ee8c890..ba5621d 100644 --- a/vdirsyncer/storage/base.py +++ b/vdirsyncer/storage/base.py @@ -5,7 +5,6 @@ import functools from .. import exceptions, sync from ..utils import uniq -from ..utils.compat import to_native, to_unicode, with_metaclass from ..utils.vobject import Item # noqa @@ -25,7 +24,7 @@ class StorageMeta(type): 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 implement. @@ -77,9 +76,7 @@ class Storage(with_metaclass(StorageMeta)): self.read_only = bool(read_only) if collection and instance_name: - # XXX: PY2 hack - instance_name = '{}/{}'.format(instance_name, - to_native(collection, 'utf-8')) + instance_name = '{}/{}'.format(instance_name, collection) self.instance_name = instance_name self.collection = collection @@ -241,4 +238,4 @@ class Storage(with_metaclass(StorageMeta)): def normalize_meta_value(value): - return to_unicode(value or u'').strip() + return (value or u'').strip() diff --git a/vdirsyncer/storage/dav.py b/vdirsyncer/storage/dav.py index bbadd1e..8024823 100644 --- a/vdirsyncer/storage/dav.py +++ b/vdirsyncer/storage/dav.py @@ -2,9 +2,11 @@ import datetime import logging - +import urllib.parse as urlparse import xml.etree.ElementTree as etree +from inspect import getfullargspec + import requests 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, \ prepare_client_cert, prepare_verify from .. import exceptions, utils -from ..utils.compat import PY2, getargspec_ish, text_type, to_native dav_logger = logging.getLogger(__name__) @@ -22,7 +23,7 @@ CALDAV_DT_FORMAT = '%Y%m%dT%H%M%SZ' def _generate_path_reserved_chars(): for x in "/?#[]!$&'()*+,;": - x = utils.compat.urlquote(x, '') + x = urlparse.quote(x, '') yield x.upper() yield x.lower() @@ -42,13 +43,11 @@ def _normalize_href(base, href): '''Normalize the href to be a path only relative to hostname and schema.''' orig_href = href - base = to_native(base, 'utf-8') - href = to_native(href, 'utf-8') if not href: raise ValueError(href) - x = utils.compat.urlparse.urljoin(base, href) - x = utils.compat.urlparse.urlsplit(x).path + x = urlparse.urljoin(base, href) + x = urlparse.urlsplit(x).path # Encoding issues: # - https://github.com/owncloud/contacts/issues/581 @@ -58,9 +57,9 @@ def _normalize_href(base, href): if _contains_quoted_reserved_chars(x): break 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: dav_logger.debug('Already normalized: {!r}'.format(x)) @@ -129,7 +128,7 @@ class Discover(object): @staticmethod def _get_collection_from_url(url): _, collection = url.rstrip('/').rsplit('/', 1) - return utils.compat.urlunquote(collection) + return urlparse.unquote(collection) def find_dav(self): try: @@ -166,7 +165,7 @@ class Discover(object): rv = root.find('.//{DAV:}current-user-principal/{DAV:}href') if rv is None: raise InvalidXMLResponse() - return utils.compat.urlparse.urljoin(response.url, rv.text) + return urlparse.urljoin(response.url, rv.text) def find_home(self, url=None): if url is None: @@ -182,7 +181,7 @@ class Discover(object): rv = root.find('.//' + self._homeset_tag + '/{DAV:}href') if rv is None: 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): if url is None: @@ -202,7 +201,7 @@ class Discover(object): if href is None: raise InvalidXMLResponse('Missing href tag for collection ' 'props.') - href = utils.compat.urlparse.urljoin(r.url, href.text) + href = urlparse.urljoin(r.url, href.text) if href not in done: done.add(href) yield {'href': href} @@ -224,9 +223,9 @@ class Discover(object): return c home = self.find_home() - url = utils.compat.urlparse.urljoin( + url = urlparse.urljoin( home, - utils.compat.urlquote(collection, '/@') + urlparse.quote(collection, '/@') ) try: @@ -252,7 +251,8 @@ class Discover(object): '''.format( - to_native(etree.tostring(etree.Element(self._resourcetype))) + etree.tostring(etree.Element(self._resourcetype), + encoding='unicode') ) response = self.session.request( @@ -299,7 +299,7 @@ class DavSession(object): @classmethod def init_and_remaining_args(cls, **kwargs): - argspec = getargspec_ish(cls.__init__) + argspec = getfullargspec(cls.__init__) self_args, remainder = \ utils.split_dict(kwargs, argspec.args.__contains__) @@ -321,12 +321,12 @@ class DavSession(object): @utils.cached_property def parsed_url(self): - return utils.compat.urlparse.urlparse(self.url) + return urlparse.urlparse(self.url) def request(self, method, path, **kwargs): url = self.url if path: - url = utils.compat.urlparse.urljoin(self.url, path) + url = urlparse.urljoin(self.url, path) more = dict(self._settings) more.update(kwargs) @@ -379,9 +379,8 @@ class DavStorage(Storage): self.session_class.init_and_remaining_args(**kwargs) super(DavStorage, self).__init__(**kwargs) - if not PY2: - import inspect - __init__.__signature__ = inspect.signature(session_class.__init__) + import inspect + __init__.__signature__ = inspect.signature(session_class.__init__) @classmethod def discover(cls, **kwargs): @@ -602,7 +601,7 @@ class DavStorage(Storage): '''.format( - to_native(etree.tostring(etree.Element(xpath))) + etree.tostring(etree.Element(xpath), encoding='unicode') ) headers = self.session.get_default_headers() @@ -639,7 +638,7 @@ class DavStorage(Storage): - '''.format(to_native(etree.tostring(element))) + '''.format(etree.tostring(element, encoding='unicode')) self.session.request( 'PROPPATCH', '', @@ -723,11 +722,11 @@ class CaldavStorage(DavStorage): namespace = dict(datetime.__dict__) namespace['start_date'] = self.start_date = \ (eval(start_date, namespace) - if isinstance(start_date, (bytes, text_type)) + if isinstance(start_date, (bytes, str)) else start_date) self.end_date = \ (eval(end_date, namespace) - if isinstance(end_date, (bytes, text_type)) + if isinstance(end_date, (bytes, str)) else end_date) @staticmethod diff --git a/vdirsyncer/storage/filesystem.py b/vdirsyncer/storage/filesystem.py index 356c1e6..f65306f 100644 --- a/vdirsyncer/storage/filesystem.py +++ b/vdirsyncer/storage/filesystem.py @@ -10,7 +10,6 @@ from atomicwrites import atomic_write from .base import Item, Storage, normalize_meta_value from .. import exceptions from ..utils import checkdir, expand_path, generate_href, get_etag_from_file -from ..utils.compat import text_type, to_native logger = logging.getLogger(__name__) @@ -42,7 +41,7 @@ class FilesystemStorage(Storage): def __init__(self, path, fileext, encoding='utf-8', post_hook=None, **kwargs): super(FilesystemStorage, self).__init__(**kwargs) - path = expand_path(to_native(path, encoding)) + path = expand_path(path) checkdir(path, create=False) self.path = path self.encoding = encoding @@ -70,11 +69,9 @@ class FilesystemStorage(Storage): @classmethod def create_collection(cls, collection, **kwargs): kwargs = dict(kwargs) - encoding = kwargs.get('encoding', 'utf-8') - path = to_native(kwargs['path'], encoding) + path = kwargs['path'] if collection is not None: - collection = to_native(collection, encoding) path = os.path.join(path, collection) checkdir(expand_path(path), create=True) @@ -84,7 +81,7 @@ class FilesystemStorage(Storage): return kwargs 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): return generate_href(ident) + self.fileext @@ -108,7 +105,7 @@ class FilesystemStorage(Storage): raise 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.') try: @@ -151,7 +148,7 @@ class FilesystemStorage(Storage): if 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.') with atomic_write(fpath, mode='wb', overwrite=True) as f: diff --git a/vdirsyncer/storage/google.py b/vdirsyncer/storage/google.py index b6230ab..47d8809 100644 --- a/vdirsyncer/storage/google.py +++ b/vdirsyncer/storage/google.py @@ -2,6 +2,7 @@ import json import logging +import urllib.parse as urlparse from atomicwrites import atomic_write @@ -10,7 +11,8 @@ import click from click_threading import get_ui_worker from . import base, dav -from .. import exceptions, utils +from .. import exceptions +from ..utils import expand_path, open_graphical_browser logger = logging.getLogger(__name__) @@ -37,7 +39,7 @@ class GoogleSession(dav.DavSession): if not have_oauth2: 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() f = lambda: self._init_token(token_file, client_id, client_secret) ui_worker.put(f) @@ -75,7 +77,7 @@ class GoogleSession(dav.DavSession): access_type='offline', approval_prompt='force') click.echo('Opening {} ...'.format(authorization_url)) try: - utils.open_graphical_browser(authorization_url) + open_graphical_browser(authorization_url) except Exception as e: logger.warning(str(e)) @@ -118,7 +120,7 @@ class GoogleCalendarStorage(dav.CaldavStorage): parts = url.rstrip('/').split('/') parts.pop() collection = parts.pop() - return utils.compat.urlunquote(collection) + return urlparse.unquote(collection) storage_name = 'google_calendar' diff --git a/vdirsyncer/storage/http.py b/vdirsyncer/storage/http.py index ffb08cf..7731c74 100644 --- a/vdirsyncer/storage/http.py +++ b/vdirsyncer/storage/http.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- +import urllib.parse as urlparse + from .base import Item, Storage from .. import exceptions from ..utils import expand_path -from ..utils.compat import iteritems, text_type, urlparse from ..utils.http import request from ..utils.vobject import split_collection @@ -39,7 +40,7 @@ def prepare_auth(auth, username, password): def prepare_verify(verify, verify_fingerprint): - if isinstance(verify, (text_type, bytes)): + if isinstance(verify, (str, bytes)): verify = expand_path(verify) elif not isinstance(verify, bool): raise exceptions.UserError('Invalid value for verify ({}), ' @@ -47,7 +48,7 @@ def prepare_verify(verify, verify_fingerprint): .format(verify)) 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 ' '({}), must be a string or null.' .format(verify_fingerprint)) @@ -64,7 +65,7 @@ def prepare_verify(verify, verify_fingerprint): def prepare_client_cert(cert): - if isinstance(cert, (text_type, bytes)): + if isinstance(cert, (str, bytes)): cert = expand_path(cert) elif isinstance(cert, list): cert = tuple(map(prepare_client_cert, cert)) @@ -154,7 +155,7 @@ class HttpStorage(Storage): etag = item.hash 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): if self._items is None: diff --git a/vdirsyncer/storage/remotestorage.py b/vdirsyncer/storage/remotestorage.py index 714e571..1325d38 100644 --- a/vdirsyncer/storage/remotestorage.py +++ b/vdirsyncer/storage/remotestorage.py @@ -7,6 +7,7 @@ things, and plugging in an account "just works". ''' import logging +from urllib.parse import quote as urlquote, urljoin import click @@ -21,9 +22,6 @@ DRAFT_VERSION = '05' logger = logging.getLogger(__name__) -urljoin = utils.compat.urlparse.urljoin -urlquote = utils.compat.urlquote - def _ensure_slash(dir): return dir.rstrip('/') + '/' @@ -33,7 +31,7 @@ def _iter_listing(json): new_listing = '@context' in json # draft-02 and beyond if new_listing: json = json['items'] - for name, info in utils.compat.iteritems(json): + for name, info in json.items(): if not new_listing: info = {'ETag': info} yield name, info diff --git a/vdirsyncer/storage/singlefile.py b/vdirsyncer/storage/singlefile.py index 209d742..9057123 100644 --- a/vdirsyncer/storage/singlefile.py +++ b/vdirsyncer/storage/singlefile.py @@ -12,7 +12,6 @@ from atomicwrites import atomic_write from .base import Item, Storage from .. import exceptions from ..utils import checkfile, expand_path -from ..utils.compat import iteritems, itervalues from ..utils.vobject import join_collection, split_collection logger = logging.getLogger(__name__) @@ -167,7 +166,7 @@ class SingleFileStorage(Storage): etag = item.hash 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): 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 ' 'writing into the same file.'.format(self.path)) text = join_collection( - (item.raw for item, etag in itervalues(self._items)), + item.raw for item, etag in self._items.values() ) try: with atomic_write(self.path, mode='wb', overwrite=True) as f: diff --git a/vdirsyncer/sync.py b/vdirsyncer/sync.py index ee6d84a..75b0570 100644 --- a/vdirsyncer/sync.py +++ b/vdirsyncer/sync.py @@ -14,7 +14,6 @@ import logging from . import exceptions from .utils import uniq -from .utils.compat import iteritems, text_type sync_logger = logging.getLogger(__name__) @@ -92,7 +91,7 @@ class StorageSyncer(object): def prepare_idents(self): href_to_status = dict((meta['href'], (ident, meta)) for ident, meta - in iteritems(self.status)) + in self.status.items()) prefetch = {} 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( (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( (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() @@ -275,7 +274,7 @@ def _action_update(ident, source, dest): dest_href = dest_meta['href'] dest_etag = dest.storage.update(dest_href, source_meta['item'], dest_meta['etag']) - assert isinstance(dest_etag, (bytes, text_type)) + assert isinstance(dest_etag, (bytes, str)) source.status[ident] = _compress_meta(source_meta) dest.status[ident] = { diff --git a/vdirsyncer/utils/__init__.py b/vdirsyncer/utils/__init__.py index 707d606..e1a0acb 100644 --- a/vdirsyncer/utils/__init__.py +++ b/vdirsyncer/utils/__init__.py @@ -5,7 +5,8 @@ import os import sys import uuid -from .compat import getargspec_ish, iteritems, to_unicode +from inspect import getfullargspec + from .. import exceptions @@ -25,7 +26,7 @@ def expand_path(p): def split_dict(d, f): '''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) @@ -77,7 +78,7 @@ def get_storage_init_specs(cls, stop_at=object): if cls is stop_at: return () - spec = getargspec_ish(cls.__init__) + spec = getfullargspec(cls.__init__) traverse_superclass = getattr(cls.__init__, '_traverse_superclass', True) if traverse_superclass: if traverse_superclass is True: # noqa @@ -178,7 +179,7 @@ def generate_href(ident=None, safe=SAFE_UID_CHARS): UUID. ''' if not ident or not href_safe(ident, safe): - return to_unicode(uuid.uuid4().hex) + return str(uuid.uuid4()) else: return ident diff --git a/vdirsyncer/utils/compat.py b/vdirsyncer/utils/compat.py deleted file mode 100644 index d3285b9..0000000 --- a/vdirsyncer/utils/compat.py +++ /dev/null @@ -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', (), {}) diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index 1e20cb9..21ed306 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -4,14 +4,9 @@ import hashlib from itertools import chain, tee from . import cached_property, uniq -from .compat import itervalues, text_type, to_unicode -def _prepare_props(*x): - return tuple(map(to_unicode, x)) - - -IGNORE_PROPS = _prepare_props( +IGNORE_PROPS = ( # PRODID is changed by radicale for some reason after upload 'PRODID', # X-RADICALE-NAME is used by radicale, because hrefs don't really exist in @@ -34,7 +29,6 @@ IGNORE_PROPS = _prepare_props( 'DTSTAMP', 'UID', ) -del _prepare_props class Item(object): @@ -43,7 +37,7 @@ class Item(object): VCARD''' def __init__(self, raw): - assert isinstance(raw, text_type) + assert isinstance(raw, str) self._raw = raw @cached_property @@ -111,7 +105,7 @@ def hash_item(text): def split_collection(text): - assert isinstance(text, text_type) + assert isinstance(text, str) inline = [] items = {} # uid => item ungrouped_items = [] @@ -141,7 +135,7 @@ def split_collection(text): for main in _Component.parse(text, multiple=True): inner(main, main) - for item in chain(itervalues(items), ungrouped_items): + for item in chain(items.values(), ungrouped_items): item.subcomponents.extend(inline) yield u'\r\n'.join(item.dump_lines()) @@ -240,7 +234,7 @@ class _Component(object): def parse(cls, lines, multiple=False): if isinstance(lines, bytes): lines = lines.decode('utf-8') - if isinstance(lines, text_type): + if isinstance(lines, str): lines = lines.splitlines() stack = [] @@ -301,7 +295,7 @@ class _Component(object): self.props = new_lines def __setitem__(self, key, val): - assert isinstance(val, text_type) + assert isinstance(val, str) assert u'\n' not in val del self[key] line = u'{}:{}'.format(key, val)