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. have been added.
- New global command line option `--config`, to specify an alternative config - New global command line option `--config`, to specify an alternative config
file. See :gh:`409`. file. See :gh:`409`.
- The ``collections`` parameter can now be used to synchronize
differently-named collections with each other.
Version 0.9.3 Version 0.9.3
============= =============

View file

@ -64,6 +64,12 @@ Pair Section
The special values ``"from a"`` and ``"from b"``, tell vdirsyncer to try The special values ``"from a"`` and ``"from b"``, tell vdirsyncer to try
autodiscovery on a specific storage. 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: Examples:
- ``collections = ["from b", "foo", "bar"]`` makes vdirsyncer synchronize the - ``collections = ["from b", "foo", "bar"]`` makes vdirsyncer synchronize the
@ -72,6 +78,10 @@ Pair Section
- ``collections = ["from b", from a"]`` makes vdirsyncer synchronize all - ``collections = ["from b", from a"]`` makes vdirsyncer synchronize all
existing collections on either side. 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_resolution``: Optional, define how conflicts should be handled. A
conflict occurs when one item (event, task) changed on both sides since the conflict occurs when one item (event, task) changed on both sides since the
last sync. last sync.

View file

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

View file

@ -81,3 +81,48 @@ def test_discover_on_unsupported_storage(tmpdir, runner):
result = runner.invoke(['discover']) result = runner.invoke(['discover'])
assert result.exception assert result.exception
assert 'doesn\'t support collection discovery' in result.output 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: if collections is None:
return return
e = ValueError('`collections` parameter must be a list of collection '
'names (strings!) or `null`.')
if not isinstance(collections, list): 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): collection_names = set()
raise e
if len(set(collections)) != len(collections): for i, collection in enumerate(collections):
raise ValueError('Duplicate values in collections parameter.') 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): 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) a_discovered = _discover_from_config(pair.config_a)
b_discovered = _discover_from_config(pair.config_b) b_discovered = _discover_from_config(pair.config_b)
for shortcut in set(shortcuts): for shortcut in shortcuts:
if shortcut == 'from a': if shortcut == 'from a':
collections = a_discovered collections = a_discovered
elif shortcut == 'from b': elif shortcut == 'from b':
@ -285,17 +285,28 @@ def _collections_for_pair_impl(status_path, pair):
collections = [shortcut] collections = [shortcut]
for collection in collections: for collection in collections:
try: if isinstance(collection, list):
a_args = a_discovered[collection] try:
except KeyError: collection, collection_a, collection_b = collection
a_args = _handle_collection_not_found(pair.config_a, except ValueError:
collection) raise exceptions.UserError(
'Expected string or list of length 3, '
'{} found instead.'
.format(collection))
else:
collection_a = collection_b = collection
try: 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: except KeyError:
b_args = _handle_collection_not_found(pair.config_b, b_args = _handle_collection_not_found(pair.config_b,
collection) collection_b)
yield collection, (a_args, b_args) yield collection, (a_args, b_args)