Drop support for Python 3.5 and 3.6

This commit is contained in:
Hugo Osvaldo Barrera 2020-06-09 14:33:14 +02:00
parent 56b1fc2187
commit 9cb1f8d704
30 changed files with 77 additions and 135 deletions

View file

@ -19,75 +19,23 @@
"include": [ "include": [
{ {
"env": "BUILD=style", "env": "BUILD=style",
"python": "3.6" "python": "3.7"
}, },
{ {
"env": "BUILD=test REQUIREMENTS=release", "env": "BUILD=test REQUIREMENTS=release",
"python": "3.5" "python": "3.7"
},
{
"dist": "trusty",
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=release ",
"python": "3.5"
},
{
"dist": "trusty",
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=release ",
"python": "3.5"
},
{
"env": "BUILD=test REQUIREMENTS=minimal",
"python": "3.5"
},
{
"dist": "trusty",
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=minimal ",
"python": "3.5"
},
{
"dist": "trusty",
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=minimal ",
"python": "3.5"
},
{
"env": "BUILD=test REQUIREMENTS=release",
"python": "3.6"
}, },
{ {
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=release ", "env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=release ",
"python": "3.6" "python": "3.7"
}, },
{ {
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=release ", "env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=release ",
"python": "3.6" "python": "3.7"
}, },
{ {
"env": "BUILD=test-storage DAV_SERVER=fastmail REQUIREMENTS=release ", "env": "BUILD=test-storage DAV_SERVER=fastmail REQUIREMENTS=release ",
"if": "NOT (type IN (pull_request))", "if": "NOT (type IN (pull_request))",
"python": "3.6"
},
{
"env": "BUILD=test REQUIREMENTS=minimal",
"python": "3.6"
},
{
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=minimal ",
"python": "3.6"
},
{
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=minimal ",
"python": "3.6"
},
{
"env": "BUILD=test REQUIREMENTS=release",
"python": "3.7"
},
{
"env": "BUILD=test-storage DAV_SERVER=radicale REQUIREMENTS=release ",
"python": "3.7"
},
{
"env": "BUILD=test-storage DAV_SERVER=xandikos REQUIREMENTS=release ",
"python": "3.7" "python": "3.7"
}, },
{ {
@ -128,7 +76,7 @@
}, },
{ {
"env": "BUILD=test ETESYNC_TESTS=true REQUIREMENTS=latest", "env": "BUILD=test ETESYNC_TESTS=true REQUIREMENTS=latest",
"python": "3.6" "python": "3.7"
} }
] ]
}, },

View file

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

View file

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

View file

@ -3,8 +3,7 @@
import itertools import itertools
import json import json
python_versions = ("3.5", "3.6", "3.7", "3.8") python_versions = ["3.7", "3.8"]
latest_python = "3.6"
cfg = {} cfg = {}
@ -34,7 +33,7 @@ matrix = []
cfg['matrix'] = {'include': matrix, 'fast_finish': True} cfg['matrix'] = {'include': matrix, 'fast_finish': True}
matrix.append({ matrix.append({
'python': latest_python, 'python': python_versions[0],
'env': 'BUILD=style' 'env': 'BUILD=style'
}) })
@ -51,7 +50,7 @@ for python, requirements in itertools.product(
'env': f"BUILD=test REQUIREMENTS={requirements}", 'env': f"BUILD=test REQUIREMENTS={requirements}",
}) })
if python == latest_python and requirements == "release": if python == python_versions[0] and requirements == "release":
dav_servers += ("fastmail",) dav_servers += ("fastmail",)
for dav_server in dav_servers: for dav_server in dav_servers:
@ -61,8 +60,6 @@ for python, requirements in itertools.product(
f"DAV_SERVER={dav_server} " f"DAV_SERVER={dav_server} "
f"REQUIREMENTS={requirements} ") f"REQUIREMENTS={requirements} ")
} }
if python == '3.5':
job['dist'] = 'trusty'
build_prs = dav_server not in ("fastmail", "davical", "icloud") build_prs = dav_server not in ("fastmail", "davical", "icloud")
if not build_prs: if not build_prs:
@ -71,7 +68,7 @@ for python, requirements in itertools.product(
matrix.append(job) matrix.append(job)
matrix.append({ matrix.append({
'python': latest_python, 'python': python_versions[0],
'env': ("BUILD=test " 'env': ("BUILD=test "
"ETESYNC_TESTS=true " "ETESYNC_TESTS=true "
"REQUIREMENTS=latest") "REQUIREMENTS=latest")

View file

@ -87,8 +87,6 @@ setup(
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: POSIX', 'Operating System :: POSIX',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Topic :: Internet', 'Topic :: Internet',

View file

@ -19,7 +19,7 @@ from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \
def get_server_mixin(server_name): def get_server_mixin(server_name):
from . import __name__ as base from . import __name__ as base
x = __import__('{}.servers.{}'.format(base, server_name), fromlist=['']) x = __import__(f'{base}.servers.{server_name}', fromlist=[''])
return x.ServerMixin return x.ServerMixin
@ -183,7 +183,7 @@ class StorageTests:
def test_discover(self, requires_collections, get_storage_args, get_item): def test_discover(self, requires_collections, get_storage_args, get_item):
collections = set() collections = set()
for i in range(1, 5): for i in range(1, 5):
collection = 'test{}'.format(i) collection = f'test{i}'
s = self.storage_class(**get_storage_args(collection=collection)) s = self.storage_class(**get_storage_args(collection=collection))
assert not list(s.list()) assert not list(s.list())
s.upload(get_item()) s.upload(get_item())

View file

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

View file

@ -461,7 +461,7 @@ def test_partial_sync(tmpdir, runner, partial_sync):
fileext = ".txt" fileext = ".txt"
path = "{base}/bar" path = "{base}/bar"
'''.format( '''.format(
partial_sync=('partial_sync = "{}"\n'.format(partial_sync) partial_sync=(f'partial_sync = "{partial_sync}"\n'
if partial_sync else ''), if partial_sync else ''),
base=str(tmpdir) base=str(tmpdir)
))) )))

View file

@ -47,7 +47,7 @@ def test_key_conflict(monkeypatch, mystrategy):
@given(s=st.text(), t=st.text(min_size=1)) @given(s=st.text(), t=st.text(min_size=1))
def test_fuzzing(s, t, mystrategy): def test_fuzzing(s, t, mystrategy):
config = expand_fetch_params({ config = expand_fetch_params({
'{}.fetch'.format(s): ['mystrategy', t] f'{s}.fetch': ['mystrategy', t]
}) })
assert config[s] == t assert config[s] == t

View file

@ -253,7 +253,7 @@ def test_conflict_resolution_both_etags_new(winning_storage):
b.update(href_b, item_b, etag_b) b.update(href_b, item_b, etag_b)
with pytest.raises(SyncConflict): with pytest.raises(SyncConflict):
sync(a, b, status) sync(a, b, status)
sync(a, b, status, conflict_resolution='{} wins'.format(winning_storage)) sync(a, b, status, conflict_resolution=f'{winning_storage} wins')
assert items(a) == items(b) == { assert items(a) == items(b) == {
item_a.raw if winning_storage == 'a' else item_b.raw item_a.raw if winning_storage == 'a' else item_b.raw
} }
@ -563,7 +563,7 @@ class SyncMachine(RuleBasedStateMachine):
uid=uid_strategy, uid=uid_strategy,
etag=st.text()) etag=st.text())
def upload(self, storage, uid, etag): def upload(self, storage, uid, etag):
item = Item('UID:{}'.format(uid)) item = Item(f'UID:{uid}')
storage.items[uid] = (etag, item) storage.items[uid] = (etag, item)
@rule(storage=Storage, href=st.text()) @rule(storage=Storage, href=st.text())

View file

@ -18,11 +18,11 @@ def test_repair_uids(uid):
s.items = { s.items = {
'one': ( 'one': (
'asdf', 'asdf',
Item('BEGIN:VCARD\nFN:Hans\nUID:{}\nEND:VCARD'.format(uid)) Item(f'BEGIN:VCARD\nFN:Hans\nUID:{uid}\nEND:VCARD')
), ),
'two': ( 'two': (
'asdf', 'asdf',
Item('BEGIN:VCARD\nFN:Peppi\nUID:{}\nEND:VCARD'.format(uid)) Item(f'BEGIN:VCARD\nFN:Peppi\nUID:{uid}\nEND:VCARD')
) )
} }
@ -40,7 +40,7 @@ def test_repair_uids(uid):
@settings(suppress_health_check=HealthCheck.all()) @settings(suppress_health_check=HealthCheck.all())
def test_repair_unsafe_uids(uid): def test_repair_unsafe_uids(uid):
s = MemoryStorage() s = MemoryStorage()
item = Item('BEGIN:VCARD\nUID:{}\nEND:VCARD'.format(uid)) item = Item(f'BEGIN:VCARD\nUID:{uid}\nEND:VCARD')
href, etag = s.upload(item) href, etag = s.upload(item)
assert s.get(href)[0].uid == uid assert s.get(href)[0].uid == uid
assert not href_safe(uid) assert not href_safe(uid)
@ -58,7 +58,7 @@ def test_repair_unsafe_uids(uid):
('perfectly-fine', 'b@dh0mbr3') ('perfectly-fine', 'b@dh0mbr3')
]) ])
def test_repair_unsafe_href(uid, href): def test_repair_unsafe_href(uid, href):
item = Item('BEGIN:VCARD\nUID:{}\nEND:VCARD'.format(uid)) item = Item(f'BEGIN:VCARD\nUID:{uid}\nEND:VCARD')
new_item = repair_item(href, item, set(), True) new_item = repair_item(href, item, set(), True)
assert new_item.raw != item.raw assert new_item.raw != item.raw
assert new_item.uid != item.uid assert new_item.uid != item.uid

View file

@ -221,7 +221,7 @@ def test_replace_uid(template, uid):
item = vobject.Item(template.format(r=123, uid=123)).with_uid(uid) item = vobject.Item(template.format(r=123, uid=123)).with_uid(uid)
assert item.uid == uid assert item.uid == uid
if uid: if uid:
assert item.raw.count('\nUID:{}'.format(uid)) == 1 assert item.raw.count(f'\nUID:{uid}') == 1
else: else:
assert '\nUID:' not in item.raw assert '\nUID:' not in item.raw
@ -317,7 +317,7 @@ class VobjectMachine(RuleBasedStateMachine):
params=st.lists(st.tuples(value_strategy, value_strategy))) params=st.lists(st.tuples(value_strategy, value_strategy)))
def add_prop_raw(self, c, key, value, params): def add_prop_raw(self, c, key, value, params):
params_str = ','.join(k + '=' + v for k, v in params) params_str = ','.join(k + '=' + v for k, v in params)
c.props.insert(0, '{};{}:{}'.format(key, params_str, value)) c.props.insert(0, f'{key};{params_str}:{value}')
assert c[key] == value assert c[key] == value
assert key in c assert key in c
assert c.get(key) == value assert c.get(key) == value

View file

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

View file

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

View file

@ -101,7 +101,7 @@ class _ConfigReader:
def _parse_section(self, section_type, name, options): def _parse_section(self, section_type, name, options):
validate_section_name(name, section_type) validate_section_name(name, section_type)
if name in self._seen_names: if name in self._seen_names:
raise ValueError('Name "{}" already used.'.format(name)) raise ValueError(f'Name "{name}" already used.')
self._seen_names.add(name) self._seen_names.add(name)
if section_type == 'general': if section_type == 'general':
@ -163,7 +163,7 @@ class Config:
try: try:
self.pairs[name] = PairConfig(self, name, options) self.pairs[name] = PairConfig(self, name, options)
except ValueError as e: except ValueError as e:
raise exceptions.UserError('Pair {}: {}'.format(name, e)) raise exceptions.UserError(f'Pair {name}: {e}')
@classmethod @classmethod
def from_fileobject(cls, f): def from_fileobject(cls, f):

View file

@ -211,7 +211,7 @@ def _print_collections(instance_name, get_discovered):
logger.warning('Failed to discover collections for {}, use `-vdebug` ' logger.warning('Failed to discover collections for {}, use `-vdebug` '
'to see the full traceback.'.format(instance_name)) 'to see the full traceback.'.format(instance_name))
return return
logger.info('{}:'.format(instance_name)) logger.info(f'{instance_name}:')
for args in discovered.values(): for args in discovered.values():
collection = args['collection'] collection = args['collection']
if collection is None: if collection is None:
@ -226,7 +226,7 @@ def _print_collections(instance_name, get_discovered):
logger.info(' - {}{}'.format( logger.info(' - {}{}'.format(
json.dumps(collection), json.dumps(collection),
' ("{}")'.format(displayname) f' ("{displayname}")'
if displayname and displayname != collection if displayname and displayname != collection
else '' else ''
)) ))

View file

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

View file

@ -45,7 +45,7 @@ def sync_collection(wq, collection, general, force_delete):
status_name = get_status_name(pair.name, collection.name) status_name = get_status_name(pair.name, collection.name)
try: try:
cli_logger.info('Syncing {}'.format(status_name)) cli_logger.info(f'Syncing {status_name}')
a = storage_instance_from_config(collection.config_a) a = storage_instance_from_config(collection.config_a)
b = storage_instance_from_config(collection.config_b) b = storage_instance_from_config(collection.config_b)
@ -110,7 +110,7 @@ def repair_collection(config, collection, repair_unsafe_uid):
config['type'] = storage_type config['type'] = storage_type
storage = storage_instance_from_config(config) storage = storage_instance_from_config(config)
cli_logger.info('Repairing {}/{}'.format(storage_name, collection)) cli_logger.info(f'Repairing {storage_name}/{collection}')
cli_logger.warning('Make sure no other program is talking to the server.') cli_logger.warning('Make sure no other program is talking to the server.')
repair_storage(storage, repair_unsafe_uid=repair_unsafe_uid) repair_storage(storage, repair_unsafe_uid=repair_unsafe_uid)
@ -121,7 +121,7 @@ def metasync_collection(wq, collection, general):
status_name = get_status_name(pair.name, collection.name) status_name = get_status_name(pair.name, collection.name)
try: try:
cli_logger.info('Metasyncing {}'.format(status_name)) cli_logger.info(f'Metasyncing {status_name}')
status = load_status(general['status_path'], pair.name, status = load_status(general['status_path'], pair.name,
collection.name, data_type='metadata') or {} collection.name, data_type='metadata') or {}

View file

@ -144,11 +144,11 @@ def handle_cli_error(status_name=None, e=None):
import traceback import traceback
tb = traceback.format_tb(tb) tb = traceback.format_tb(tb)
if status_name: if status_name:
msg = 'Unknown error occurred for {}'.format(status_name) msg = f'Unknown error occurred for {status_name}'
else: else:
msg = 'Unknown error occurred' msg = 'Unknown error occurred'
msg += ': {}\nUse `-vdebug` to see the full traceback.'.format(e) msg += f': {e}\nUse `-vdebug` to see the full traceback.'
cli_logger.error(msg) cli_logger.error(msg)
cli_logger.debug(''.join(tb)) cli_logger.debug(''.join(tb))
@ -210,8 +210,7 @@ def manage_sync_status(base_path, pair_name, collection_name):
with open(path, 'rb') as f: with open(path, 'rb') as f:
if f.read(1) == b'{': if f.read(1) == b'{':
f.seek(0) f.seek(0)
# json.load doesn't work on binary files for Python 3.5 legacy_status = dict(json.load(f))
legacy_status = dict(json.loads(f.read().decode('utf-8')))
except (OSError, ValueError): except (OSError, ValueError):
pass pass
@ -247,7 +246,7 @@ def storage_class_from_config(config):
cls = storage_names[storage_name] cls = storage_names[storage_name]
except KeyError: except KeyError:
raise exceptions.UserError( raise exceptions.UserError(
'Unknown storage type: {}'.format(storage_name)) f'Unknown storage type: {storage_name}')
return cls, config return cls, config
@ -399,7 +398,7 @@ def handle_collection_not_found(config, collection, e=None):
storage_name = config.get('instance_name', None) storage_name = config.get('instance_name', None)
cli_logger.warning('{}No collection {} found for storage {}.' cli_logger.warning('{}No collection {} found for storage {}.'
.format('{}\n'.format(e) if e else '', .format(f'{e}\n' if e else '',
json.dumps(collection), storage_name)) json.dumps(collection), storage_name))
if click.confirm('Should vdirsyncer attempt to create it?'): if click.confirm('Should vdirsyncer attempt to create it?'):

View file

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

View file

@ -7,7 +7,7 @@ from . import DOCS_HOME, exceptions, __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
USERAGENT = 'vdirsyncer/{}'.format(__version__) USERAGENT = f'vdirsyncer/{__version__}'
def _detect_faulty_requests(): # pragma: no cover def _detect_faulty_requests(): # pragma: no cover
@ -133,7 +133,7 @@ def request(method, url, session=None, latin1_fallback=True,
func = session.request func = session.request
logger.debug('{} {}'.format(method, url)) logger.debug(f'{method} {url}')
logger.debug(kwargs.get('headers', {})) logger.debug(kwargs.get('headers', {}))
logger.debug(kwargs.get('data', None)) logger.debug(kwargs.get('data', None))
logger.debug('Sending request...') logger.debug('Sending request...')

View file

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

View file

@ -25,7 +25,7 @@ def repair_storage(storage, repair_unsafe_uid):
'The PRODID property may indicate which software ' 'The PRODID property may indicate which software '
'created this item.' 'created this item.'
.format(href)) .format(href))
logger.error('Item content: {!r}'.format(item.raw)) logger.error(f'Item content: {item.raw!r}')
continue continue
seen_uids.add(new_item.uid) seen_uids.add(new_item.uid)

View file

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

View file

@ -34,7 +34,7 @@ del _generate_path_reserved_chars
def _contains_quoted_reserved_chars(x): def _contains_quoted_reserved_chars(x):
for y in _path_reserved_chars: for y in _path_reserved_chars:
if y in x: if y in x:
dav_logger.debug('Unsafe character: {!r}'.format(y)) dav_logger.debug(f'Unsafe character: {y!r}')
return True return True
return False return False
@ -52,7 +52,7 @@ def _assert_multistatus_success(r):
except (ValueError, IndexError): except (ValueError, IndexError):
continue continue
if st < 200 or st >= 400: if st < 200 or st >= 400:
raise HTTPError('Server error: {}'.format(st)) raise HTTPError(f'Server error: {st}')
def _normalize_href(base, href): def _normalize_href(base, href):
@ -78,7 +78,7 @@ def _normalize_href(base, href):
x = urlparse.quote(x, '/@%:') x = urlparse.quote(x, '/@%:')
if orig_href == x: if orig_href == x:
dav_logger.debug('Already normalized: {!r}'.format(x)) dav_logger.debug(f'Already normalized: {x!r}')
else: else:
dav_logger.debug('Normalized URL from {!r} to {!r}' dav_logger.debug('Normalized URL from {!r} to {!r}'
.format(orig_href, x)) .format(orig_href, x))
@ -459,7 +459,7 @@ class DAVStorage(Storage):
for href in hrefs: for href in hrefs:
if href != self._normalize_href(href): if href != self._normalize_href(href):
raise exceptions.NotFoundError(href) raise exceptions.NotFoundError(href)
href_xml.append('<D:href>{}</D:href>'.format(href)) href_xml.append(f'<D:href>{href}</D:href>')
if not href_xml: if not href_xml:
return () return ()
@ -591,7 +591,7 @@ class DAVStorage(Storage):
props = _merge_xml(props) props = _merge_xml(props)
if props.find('{DAV:}resourcetype/{DAV:}collection') is not None: if props.find('{DAV:}resourcetype/{DAV:}collection') is not None:
dav_logger.debug('Skipping {!r}, is collection.'.format(href)) dav_logger.debug(f'Skipping {href!r}, is collection.')
continue continue
etag = getattr(props.find('{DAV:}getetag'), 'text', '') etag = getattr(props.find('{DAV:}getetag'), 'text', '')
@ -641,7 +641,7 @@ class DAVStorage(Storage):
except KeyError: except KeyError:
raise exceptions.UnsupportedMetadataError() raise exceptions.UnsupportedMetadataError()
xpath = '{{{}}}{}'.format(namespace, tagname) xpath = f'{{{namespace}}}{tagname}'
data = '''<?xml version="1.0" encoding="utf-8" ?> data = '''<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:"> <D:propfind xmlns:D="DAV:">
<D:prop> <D:prop>
@ -674,7 +674,7 @@ class DAVStorage(Storage):
except KeyError: except KeyError:
raise exceptions.UnsupportedMetadataError() raise exceptions.UnsupportedMetadataError()
lxml_selector = '{{{}}}{}'.format(namespace, tagname) lxml_selector = f'{{{namespace}}}{tagname}'
element = etree.Element(lxml_selector) element = etree.Element(lxml_selector)
element.text = normalize_meta_value(value) element.text = normalize_meta_value(value)

View file

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

View file

@ -80,7 +80,7 @@ class GoogleSession(dav.DAVSession):
# access_type and approval_prompt are Google specific # access_type and approval_prompt are Google specific
# extra parameters. # extra parameters.
access_type='offline', approval_prompt='force') access_type='offline', approval_prompt='force')
click.echo('Opening {} ...'.format(authorization_url)) click.echo(f'Opening {authorization_url} ...')
try: try:
open_graphical_browser(authorization_url) open_graphical_browser(authorization_url)
except Exception as e: except Exception as e:

View file

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

View file

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

View file

@ -205,14 +205,14 @@ def join_collection(items, wrappers=_default_join_wrappers):
if wrapper_type is not None: if wrapper_type is not None:
lines = chain(*( lines = chain(*(
['BEGIN:{}'.format(wrapper_type)], [f'BEGIN:{wrapper_type}'],
# XXX: wrapper_props is a list of lines (with line-wrapping), so # XXX: wrapper_props is a list of lines (with line-wrapping), so
# filtering out duplicate lines will almost certainly break # filtering out duplicate lines will almost certainly break
# multiline-values. Since the only props we usually need to # multiline-values. Since the only props we usually need to
# support are PRODID and VERSION, I don't care. # support are PRODID and VERSION, I don't care.
uniq(wrapper_props), uniq(wrapper_props),
lines, lines,
['END:{}'.format(wrapper_type)] [f'END:{wrapper_type}']
)) ))
return ''.join(line + '\r\n' for line in lines) return ''.join(line + '\r\n' for line in lines)
@ -299,14 +299,14 @@ class _Component:
return rv[0] return rv[0]
def dump_lines(self): def dump_lines(self):
yield 'BEGIN:{}'.format(self.name) yield f'BEGIN:{self.name}'
yield from self.props yield from self.props
for c in self.subcomponents: for c in self.subcomponents:
yield from c.dump_lines() yield from c.dump_lines()
yield 'END:{}'.format(self.name) yield f'END:{self.name}'
def __delitem__(self, key): def __delitem__(self, key):
prefix = ('{}:'.format(key), '{};'.format(key)) prefix = (f'{key}:', f'{key};')
new_lines = [] new_lines = []
lineiter = iter(self.props) lineiter = iter(self.props)
while True: while True:
@ -329,7 +329,7 @@ class _Component:
assert isinstance(val, str) assert isinstance(val, str)
assert '\n' not in val assert '\n' not in val
del self[key] del self[key]
line = '{}:{}'.format(key, val) line = f'{key}:{val}'
self.props.append(line) self.props.append(line)
def __contains__(self, obj): def __contains__(self, obj):
@ -342,8 +342,8 @@ class _Component:
raise ValueError(obj) raise ValueError(obj)
def __getitem__(self, key): def __getitem__(self, key):
prefix_without_params = '{}:'.format(key) prefix_without_params = f'{key}:'
prefix_with_params = '{};'.format(key) prefix_with_params = f'{key};'
iterlines = iter(self.props) iterlines = iter(self.props)
for line in iterlines: for line in iterlines:
if line.startswith(prefix_without_params): if line.startswith(prefix_without_params):