Move config tools to own file

This commit is contained in:
Markus Unterwaditzer 2015-08-20 15:09:03 +02:00
parent 565ef2e96e
commit 32abaae9b9
3 changed files with 179 additions and 175 deletions

View file

@ -74,7 +74,7 @@ def app(ctx):
'''
vdirsyncer -- synchronize calendars and contacts
'''
from .utils import load_config
from .config import load_config
if not ctx.config:
ctx.config = load_config()

177
vdirsyncer/cli/config.py Normal file
View file

@ -0,0 +1,177 @@
import json
import os
import string
from itertools import chain
from . import CliError, cli_logger
from .. import PROJECT_HOME
from ..utils import expand_path
from ..utils.compat import text_type
try:
from ConfigParser import RawConfigParser
except ImportError:
from configparser import RawConfigParser
GENERAL_ALL = frozenset(['status_path', 'password_command'])
GENERAL_REQUIRED = frozenset(['status_path'])
SECTION_NAME_CHARS = frozenset(chain(string.ascii_letters, string.digits, '_'))
def validate_section_name(name, section_type):
invalid = set(name) - SECTION_NAME_CHARS
if invalid:
chars_display = ''.join(sorted(SECTION_NAME_CHARS))
raise CliError('The {}-section "{}" contains invalid characters. Only '
'the following characters are allowed for storage and '
'pair names:\n{}'.format(section_type, name,
chars_display))
def _validate_general_section(general_config):
if 'passwordeval' in general_config:
# XXX: Deprecation
cli_logger.warning('The `passwordeval` parameter has been renamed to '
'`password_command`.')
invalid = set(general_config) - GENERAL_ALL
missing = GENERAL_REQUIRED - set(general_config)
problems = []
if invalid:
problems.append(u'general section doesn\'t take the parameters: {}'
.format(u', '.join(invalid)))
if missing:
problems.append(u'general section is missing the parameters: {}'
.format(u', '.join(missing)))
if problems:
raise CliError(u'Invalid general section. You should copy the example '
u'config from the repository and edit it: {}\n'
.format(PROJECT_HOME), problems=problems)
def _validate_pair_section(pair_config):
collections = pair_config.get('collections', None)
if collections is None:
return
e = ValueError('`collections` parameter must be a list of collection '
'names (strings!) or `null`.')
if not isinstance(collections, list) or \
any(not isinstance(x, (text_type, bytes)) for x in collections):
raise e
def load_config():
fname = os.environ.get('VDIRSYNCER_CONFIG', None)
if not fname:
fname = expand_path('~/.vdirsyncer/config')
if not os.path.exists(fname):
xdg_config_dir = os.environ.get('XDG_CONFIG_HOME',
expand_path('~/.config/'))
fname = os.path.join(xdg_config_dir, 'vdirsyncer/config')
try:
with open(fname) as f:
general, pairs, storages = read_config(f)
except Exception as e:
raise CliError('Error during reading config {}: {}'
.format(fname, e))
return general, pairs, storages
def read_config(f):
c = RawConfigParser()
c.readfp(f)
def get_options(s):
return dict(parse_options(c.items(s), section=s))
general = {}
pairs = {}
storages = {}
def handle_storage(storage_name, options):
storages.setdefault(storage_name, {}).update(options)
storages[storage_name]['instance_name'] = storage_name
def handle_pair(pair_name, options):
_validate_pair_section(options)
a, b = options.pop('a'), options.pop('b')
pairs[pair_name] = a, b, options
def handle_general(_, options):
if general:
raise CliError('More than one general section in config file.')
general.update(options)
def bad_section(name, options):
cli_logger.error('Unknown section: {}'.format(name))
handlers = {'storage': handle_storage, 'pair': handle_pair, 'general':
handle_general}
for section in c.sections():
if ' ' in section:
section_type, name = section.split(' ', 1)
else:
section_type = name = section
try:
validate_section_name(name, section_type)
f = handlers.get(section_type, bad_section)
f(name, get_options(section))
except ValueError as e:
raise CliError('Section `{}`: {}'.format(section, str(e)))
_validate_general_section(general)
if getattr(f, 'name', None):
general['status_path'] = os.path.join(
os.path.dirname(f.name),
expand_path(general['status_path'])
)
return general, pairs, storages
def parse_config_value(value):
try:
return json.loads(value)
except ValueError:
pass
for wrong, right in [
(('on', 'yes'), 'true'),
(('off', 'no'), 'false'),
(('none',), 'null')
]:
if value.lower() in wrong + (right,):
cli_logger.warning('You probably meant {} instead of "{}", which '
'will now be interpreted as a literal string.'
.format(right, value))
if '#' in value:
raise ValueError('Invalid value:{}\n'
'Use double quotes (") if you want to use hashes in '
'your value.')
if 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
# # my comment
raise ValueError('No multiline-values allowed:\n{}'.format(value))
return value
def parse_options(items, section=None):
for key, value in items:
try:
yield key, parse_config_value(value)
except ValueError as e:
raise ValueError('Section "{}", option "{}": {}'
.format(section, key, e))

View file

@ -6,9 +6,7 @@ import hashlib
import importlib
import json
import os
import string
import sys
from itertools import chain
from atomicwrites import atomic_write
@ -17,16 +15,9 @@ import click
import click_threading
from . import CliError, cli_logger
from .. import DOCS_HOME, PROJECT_HOME, exceptions
from .. import DOCS_HOME, exceptions
from ..sync import IdentConflict, StorageEmpty, SyncConflict
from ..utils import expand_path, get_class_init_args
from ..utils.compat import text_type
try:
from ConfigParser import RawConfigParser
except ImportError:
from configparser import RawConfigParser
try:
import Queue as queue
@ -68,11 +59,6 @@ storage_names = _StorageIndex()
del _StorageIndex
GENERAL_ALL = frozenset(['status_path', 'password_command'])
GENERAL_REQUIRED = frozenset(['status_path'])
SECTION_NAME_CHARS = frozenset(chain(string.ascii_letters, string.digits, '_'))
class JobFailed(RuntimeError):
pass
@ -135,16 +121,6 @@ def handle_cli_error(status_name=None):
cli_logger.exception(msg)
def validate_section_name(name, section_type):
invalid = set(name) - SECTION_NAME_CHARS
if invalid:
chars_display = ''.join(sorted(SECTION_NAME_CHARS))
raise CliError('The {}-section "{}" contains invalid characters. Only '
'the following characters are allowed for storage and '
'pair names:\n{}'.format(section_type, name,
chars_display))
def get_status_name(pair, collection):
if collection is None:
return pair
@ -306,113 +282,6 @@ def _collections_for_pair_impl(status_path, name_a, name_b, pair_name,
yield collection, (a_args, b_args)
def _validate_general_section(general_config):
if 'passwordeval' in general_config:
# XXX: Deprecation
cli_logger.warning('The `passwordeval` parameter has been renamed to '
'`password_command`.')
invalid = set(general_config) - GENERAL_ALL
missing = GENERAL_REQUIRED - set(general_config)
problems = []
if invalid:
problems.append(u'general section doesn\'t take the parameters: {}'
.format(u', '.join(invalid)))
if missing:
problems.append(u'general section is missing the parameters: {}'
.format(u', '.join(missing)))
if problems:
raise CliError(u'Invalid general section. You should copy the example '
u'config from the repository and edit it: {}\n'
.format(PROJECT_HOME), problems=problems)
def _validate_pair_section(pair_config):
collections = pair_config.get('collections', None)
if collections is None:
return
e = ValueError('`collections` parameter must be a list of collection '
'names (strings!) or `null`.')
if not isinstance(collections, list) or \
any(not isinstance(x, (text_type, bytes)) for x in collections):
raise e
def load_config():
fname = os.environ.get('VDIRSYNCER_CONFIG', None)
if not fname:
fname = expand_path('~/.vdirsyncer/config')
if not os.path.exists(fname):
xdg_config_dir = os.environ.get('XDG_CONFIG_HOME',
expand_path('~/.config/'))
fname = os.path.join(xdg_config_dir, 'vdirsyncer/config')
try:
with open(fname) as f:
general, pairs, storages = read_config(f)
except Exception as e:
raise CliError('Error during reading config {}: {}'
.format(fname, e))
return general, pairs, storages
def read_config(f):
c = RawConfigParser()
c.readfp(f)
def get_options(s):
return dict(parse_options(c.items(s), section=s))
general = {}
pairs = {}
storages = {}
def handle_storage(storage_name, options):
storages.setdefault(storage_name, {}).update(options)
storages[storage_name]['instance_name'] = storage_name
def handle_pair(pair_name, options):
_validate_pair_section(options)
a, b = options.pop('a'), options.pop('b')
pairs[pair_name] = a, b, options
def handle_general(_, options):
if general:
raise CliError('More than one general section in config file.')
general.update(options)
def bad_section(name, options):
cli_logger.error('Unknown section: {}'.format(name))
handlers = {'storage': handle_storage, 'pair': handle_pair, 'general':
handle_general}
for section in c.sections():
if ' ' in section:
section_type, name = section.split(' ', 1)
else:
section_type = name = section
try:
validate_section_name(name, section_type)
f = handlers.get(section_type, bad_section)
f(name, get_options(section))
except ValueError as e:
raise CliError('Section `{}`: {}'.format(section, str(e)))
_validate_general_section(general)
if getattr(f, 'name', None):
general['status_path'] = os.path.join(
os.path.dirname(f.name),
expand_path(general['status_path'])
)
return general, pairs, storages
def load_status(base_path, pair, collection=None, data_type=None):
assert data_type is not None
status_name = get_status_name(pair, collection)
@ -616,48 +485,6 @@ class WorkerQueue(object):
return self._queue.put(f)
def parse_config_value(value):
try:
return json.loads(value)
except ValueError:
pass
for wrong, right in [
(('on', 'yes'), 'true'),
(('off', 'no'), 'false'),
(('none',), 'null')
]:
if value.lower() in wrong + (right,):
cli_logger.warning('You probably meant {} instead of "{}", which '
'will now be interpreted as a literal string.'
.format(right, value))
if '#' in value:
raise ValueError('Invalid value:{}\n'
'Use double quotes (") if you want to use hashes in '
'your value.')
if 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
# # my comment
raise ValueError('No multiline-values allowed:\n{}'.format(value))
return value
def parse_options(items, section=None):
for key, value in items:
try:
yield key, parse_config_value(value)
except ValueError as e:
raise ValueError('Section "{}", option "{}": {}'
.format(section, key, e))
def format_storage_config(cls, header=True):
if header is True:
yield '[storage example_for_{}]'.format(cls.storage_name)