New config format

See #141

Basically this tries to parse config values with JSON, if that fails,
the value is interpreted as string.

I'd greatly appreciate feedback on this and #141
This commit is contained in:
Markus Unterwaditzer 2014-12-02 19:09:16 +01:00
parent 566a988f32
commit 08c07c4be4
7 changed files with 76 additions and 39 deletions

View file

@ -2,6 +2,18 @@
Configuration Configuration
============= =============
Vdirsyncer uses an ini-like format for storing its configuration. All values
are JSON, invalid JSON will get interpreted as string::
"foo"
foo # Same as "foo"
42
["a", "b", "c"]
[a, b, c] # This doesn't work though!
true
false
null # Also known as None
.. _general_config: .. _general_config:
@ -46,11 +58,11 @@ Pair Section
already directly pointing to one collection each. Specifying a collection already directly pointing to one collection each. Specifying a collection
multiple times won't make vdirsyncer sync that collection more than once. multiple times won't make vdirsyncer sync that collection more than once.
Furthermore, there are the special values ``from a`` and ``from b``, which Furthermore, there are the special values ``"from a"`` and ``"from b"``,
tell vdirsyncer to try autodiscovery on a specific storage:: which tell vdirsyncer to try autodiscovery on a specific storage::
collections = from b,foo,bar # all in storage b + "foo" + "bar" collections = ["from b", "foo", "bar"] # all in storage b + "foo" + "bar"
collections = from b,from a # all in storage a + all in storage b collections = ["from b", from a"] # all in storage a + all in storage b
- ``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
@ -58,9 +70,9 @@ Pair Section
Valid values are: Valid values are:
- ``a wins`` and ``b wins``, where the whole item is taken from one side. - ``"a wins"`` and ``"b wins"``, where the whole item is taken from one side.
Vdirsyncer will not attempt to merge the two items. Vdirsyncer will not attempt to merge the two items.
- ``None``, the default, where an error is shown and no changes are done. - ``null``, the default, where an error is shown and no changes are done.
.. _storage_config: .. _storage_config:
@ -75,8 +87,8 @@ Storage Section
- ``type`` defines which kind of storage is defined. See :ref:`storages`. - ``type`` defines which kind of storage is defined. See :ref:`storages`.
- ``read_only`` defines whether the storage should be regarded as a read-only - ``read_only`` defines whether the storage should be regarded as a read-only
storage. The value ``True`` means synchronization will discard any changes storage. The value ``true`` means synchronization will discard any changes
made to the other side. The value ``False`` implies normal 2-way made to the other side. The value ``false`` implies normal 2-way
synchronization. synchronization.
- Any further parameters are passed on to the storage class. - Any further parameters are passed on to the storage class.
@ -92,7 +104,7 @@ Read-write storages
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
These storages generally support reading and changing of their items. Their These storages generally support reading and changing of their items. Their
default value for ``read_only`` is ``False``, but can be set to ``True`` if default value for ``read_only`` is ``false``, but can be set to ``true`` if
wished. wished.
.. autoclass:: CaldavStorage .. autoclass:: CaldavStorage
@ -107,7 +119,7 @@ Read-only storages
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
These storages don't support writing of their items, consequently ``read_only`` These storages don't support writing of their items, consequently ``read_only``
is set to ``True`` by default. Changing ``read_only`` to ``False`` on them is set to ``true`` by default. Changing ``read_only`` to ``false`` on them
leads to an error. leads to an error.
.. autoclass:: HttpStorage .. autoclass:: HttpStorage

View file

@ -97,7 +97,7 @@ is very tedious to do. Instead we will use a shortcut:
[pair my_contacts] [pair my_contacts]
... ...
collections = default,work collections = ["default", "work"]
This will synchronize This will synchronize
``https://owncloud.example.com/remote.php/carddav/addressbooks/bob/default/`` ``https://owncloud.example.com/remote.php/carddav/addressbooks/bob/default/``

View file

@ -29,13 +29,13 @@ b = bob_contacts_remote
# Omitting this parameter implies that the given path and URL in the # Omitting this parameter implies that the given path and URL in the
# corresponding `[storage <name>]` blocks are already pointing to a # corresponding `[storage <name>]` blocks are already pointing to a
# collection each. # collection each.
collections = default,work collections = ["default", "work"]
# To resolve a conflict the following values are possible: # To resolve a conflict the following values are possible:
# `None` - abort when collisions occur (default) # `None` - abort when collisions occur (default)
# `a wins` - assume a's items to be more up-to-date # `a wins` - assume a's items to be more up-to-date
# `b wins` - assume b's items to be more up-to-date # `b wins` - assume b's items to be more up-to-date
#conflict_resolution = None #conflict_resolution = null
[storage bob_contacts_local] [storage bob_contacts_local]
# A storage references actual data on a remote server or on the local disk. # A storage references actual data on a remote server or on the local disk.
@ -63,7 +63,7 @@ url = https://owncloud.example.com/remote.php/carddav/addressbooks/bob/
[pair bob_calendar] [pair bob_calendar]
a = bob_calendar_local a = bob_calendar_local
b = bob_calendar_remote b = bob_calendar_remote
collections = private,work collections = ["private", "work"]
[storage bob_calendar_local] [storage bob_calendar_local]
type = filesystem type = filesystem

View file

@ -201,7 +201,7 @@ def test_wrong_general_section(tmpdir):
config_file = tmpdir.join('config') config_file = tmpdir.join('config')
config_file.write(dedent(''' config_file.write(dedent('''
[general] [general]
wrong = yes wrong = true
''')) '''))
runner = CliRunner() runner = CliRunner()

View file

@ -213,8 +213,16 @@ def parse_pairs_args(pairs_args, all_pairs):
.format(pair, list(all_pairs))) .format(pair, list(all_pairs)))
if collection is None: if collection is None:
collections = [x.strip() or None for x in collections = pair_options.get('collections', [None])
pair_options.get('collections', '').split(',')] if isinstance(collections, str):
# XXX: Deprecation
orig_collections = collections
collections = [x.strip() or None
for x in collections.split(',')]
cli_logger.warning(
'{!r} is deprecated, please use:\ncollections = {}\n'
'The old form will be removed in 0.4.0.'
.format(orig_collections, json.dumps(collections)))
else: else:
collections = [collection] collections = [collection]

View file

@ -484,11 +484,19 @@ class CaldavStorage(DavStorage):
get_multi_data_query = '{urn:ietf:params:xml:ns:caldav}calendar-data' get_multi_data_query = '{urn:ietf:params:xml:ns:caldav}calendar-data'
def __init__(self, start_date=None, end_date=None, def __init__(self, start_date=None, end_date=None,
item_types='VTODO, VEVENT', **kwargs): item_types=('VTODO', 'VEVENT'), **kwargs):
super(CaldavStorage, self).__init__(**kwargs) super(CaldavStorage, self).__init__(**kwargs)
if isinstance(item_types, str): if isinstance(item_types, str):
item_types = filter(bool, orig_item_types = item_types
(x.strip() for x in item_types.split(','))) item_types = [x.strip() for x in item_types.split(',')]
# XXX: Deprecation
import json
dav_logger.warning(
'{!r} is deprecated, please use:\nitem_types = {}'
'The old form will be removed in 0.4.0.'
.format(orig_item_types, json.dumps(item_types)))
self.item_types = tuple(item_types) self.item_types = tuple(item_types)
if (start_date is None) != (end_date is None): if (start_date is None) != (end_date is None):
raise ValueError('If start_date is given, ' raise ValueError('If start_date is given, '

View file

@ -7,13 +7,14 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
''' '''
import json
import os import os
import threading import threading
import requests import requests
from requests.packages.urllib3.poolmanager import PoolManager from requests.packages.urllib3.poolmanager import PoolManager
from .compat import iteritems, urlparse from .compat import iteritems, text_type, urlparse
from .. import exceptions, log from .. import exceptions, log
from ..doubleclick import click, ctx from ..doubleclick import click, ctx
@ -57,28 +58,36 @@ def split_sequence(s, f):
def parse_config_value(value): def parse_config_value(value):
if len(value.splitlines()) > 1: if value in ('on', 'yes'):
# The reason we use comma-separated values instead of logger.warning('{} is deprecated for the config, please use true.\n'
# multiline-values for lists is simple: ConfigParser's barrier for 'The old form will be removed in 0.4.0.'
# mistaking an arbitrary line for the continuation of a value is .format(value))
# awfully low. The following example will also contain the second return True
# line in the value: if value in ('off', 'no'):
logger.warning('{} is deprecated for the config, please use false.\n'
'The old form will be removed in 0.4.0.'
.format(value))
return False
if value == 'None':
logger.warning('None is deprecated for the config, please use null.\n'
'The old form will be removed in 0.4.0.')
return None
try:
rv = json.loads(value)
except ValueError:
rv = value
if isinstance(rv, (bytes, text_type)) and len(value.splitlines()) > 1:
# ConfigParser's barrier for mistaking an arbitrary line for the
# continuation of a value is awfully low. The following example will
# also contain the second line in the value:
# #
# foo = bar # foo = bar
# # my comment # # my comment
raise ValueError('No multiline-values allowed.') raise ValueError('No multiline-values allowed:\n{!r}'.format(value))
if value.lower() in ('yes', 'true', 'on'): return rv
return True
elif value.lower() in ('no', 'false', 'off'):
return False
try:
return int(value)
except ValueError:
pass
return value
def parse_options(items, section=None): def parse_options(items, section=None):