diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e37d957..9e152e9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 ============= diff --git a/docs/config.rst b/docs/config.rst index 90cfc71..06800c3 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -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. diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index e655f0a..6159d1c 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -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) diff --git a/tests/cli/test_discover.py b/tests/cli/test_discover.py index 0d5a802..5213440 100644 --- a/tests/cli/test_discover.py +++ b/tests/cli/test_discover.py @@ -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() diff --git a/vdirsyncer/cli/config.py b/vdirsyncer/cli/config.py index 08ee99b..5ae4e5c 100644 --- a/vdirsyncer/cli/config.py +++ b/vdirsyncer/cli/config.py @@ -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): diff --git a/vdirsyncer/cli/utils.py b/vdirsyncer/cli/utils.py index d4f2c73..489ba6f 100644 --- a/vdirsyncer/cli/utils.py +++ b/vdirsyncer/cli/utils.py @@ -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)