Ability to sync differently named collections with each other (#423)

* Ability to sync differently named collections

* Fixes

* Fixes

* Add example
This commit is contained in:
Markus Unterwaditzer 2016-04-10 12:36:51 +02:00
parent 777eb35898
commit be8df955e9
6 changed files with 94 additions and 20 deletions

View file

@ -16,6 +16,8 @@ Version 0.10.0
have been added.
- New global command line option `--config`, to specify an alternative config
file. See :gh:`409`.
- The ``collections`` parameter can now be used to synchronize
differently-named collections with each other.
Version 0.9.3
=============

View file

@ -64,6 +64,12 @@ Pair Section
The special values ``"from a"`` and ``"from b"``, tell vdirsyncer to try
autodiscovery on a specific storage.
If the collection you want to sync doesn't have the same name on each side,
you may also use a value of the form ``["config_name", "name_a", "name_b"]``.
This will synchronize the collection ``name_a`` on side A with the collection
``name_b`` on side B. The ``config_name`` will be used for representation in
CLI arguments and logging.
Examples:
- ``collections = ["from b", "foo", "bar"]`` makes vdirsyncer synchronize the
@ -72,6 +78,10 @@ Pair Section
- ``collections = ["from b", from a"]`` makes vdirsyncer synchronize all
existing collections on either side.
- ``collections = [["bar", "bar_a", "bar_b"], "foo"]`` makes vdirsyncer
synchronize ``bar_a`` from side A with ``bar_b`` from side B, and also
synchronize ``foo`` on both sides with each other.
- ``conflict_resolution``: Optional, define how conflicts should be handled. A
conflict occurs when one item (event, task) changed on both sides since the
last sync.

View file

@ -199,7 +199,4 @@ def test_invalid_collections_arg():
with pytest.raises(exceptions.UserError) as excinfo:
_read_config(f)
assert (
'Section `pair foobar`: `collections` parameter must be a list of '
'collection names (strings!) or `null`.'
) in str(excinfo.value)
assert 'Expected string' in str(excinfo.value)

View file

@ -81,3 +81,48 @@ def test_discover_on_unsupported_storage(tmpdir, runner):
result = runner.invoke(['discover'])
assert result.exception
assert 'doesn\'t support collection discovery' in result.output
def test_discover_different_collection_names(tmpdir, runner):
foo = tmpdir.mkdir('foo')
bar = tmpdir.mkdir('bar')
runner.write_with_general(dedent('''
[storage foo]
type = filesystem
fileext = .txt
path = {foo}
[storage bar]
type = filesystem
fileext = .txt
path = {bar}
[pair foobar]
a = foo
b = bar
collections = [
["coll1", "coll_a1", "coll_b1"],
"coll2"
]
''').format(foo=str(foo), bar=str(bar)))
result = runner.invoke(['discover'], input='y\n' * 6)
assert not result.exception
coll_a1 = foo.join('coll_a1')
coll_b1 = bar.join('coll_b1')
assert coll_a1.exists()
assert coll_b1.exists()
result = runner.invoke(['sync'])
assert not result.exception
foo_txt = coll_a1.join('foo.txt')
foo_txt.write('BEGIN:VCALENDAR\nUID:foo\nEND:VCALENDAR')
result = runner.invoke(['sync'])
assert not result.exception
assert foo_txt.exists()
assert coll_b1.join('foo.txt').exists()

View file

@ -66,17 +66,26 @@ def _validate_pair_section(pair_config):
if collections is None:
return
e = ValueError('`collections` parameter must be a list of collection '
'names (strings!) or `null`.')
if not isinstance(collections, list):
raise e
raise ValueError('`collections` parameter must be a list or `null`.')
if any(not isinstance(x, (text_type, bytes)) for x in collections):
raise e
collection_names = set()
if len(set(collections)) != len(collections):
raise ValueError('Duplicate values in collections parameter.')
for i, collection in enumerate(collections):
if isinstance(collection, (text_type, bytes)):
collection_name = collection
elif isinstance(collection, list) and \
len(collection) == 3 and \
all(isinstance(x, (text_type, bytes)) for x in collection):
collection_name = collection[0]
else:
raise ValueError('`collections` parameter, position {i}:'
'Expected string or list of three strings.'
.format(i=i))
if collection_name in collection_names:
raise ValueError('Duplicate values in collections parameter.')
collection_names.add(collection_name)
def load_config(fname=None):

View file

@ -276,7 +276,7 @@ def _collections_for_pair_impl(status_path, pair):
a_discovered = _discover_from_config(pair.config_a)
b_discovered = _discover_from_config(pair.config_b)
for shortcut in set(shortcuts):
for shortcut in shortcuts:
if shortcut == 'from a':
collections = a_discovered
elif shortcut == 'from b':
@ -285,17 +285,28 @@ def _collections_for_pair_impl(status_path, pair):
collections = [shortcut]
for collection in collections:
try:
a_args = a_discovered[collection]
except KeyError:
a_args = _handle_collection_not_found(pair.config_a,
collection)
if isinstance(collection, list):
try:
collection, collection_a, collection_b = collection
except ValueError:
raise exceptions.UserError(
'Expected string or list of length 3, '
'{} found instead.'
.format(collection))
else:
collection_a = collection_b = collection
try:
b_args = b_discovered[collection]
a_args = a_discovered[collection_a]
except KeyError:
a_args = _handle_collection_not_found(pair.config_a,
collection_a)
try:
b_args = b_discovered[collection_b]
except KeyError:
b_args = _handle_collection_not_found(pair.config_b,
collection)
collection_b)
yield collection, (a_args, b_args)