From 32abaae9b927f1a75f090050ab1e28414e5a0eb2 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 20 Aug 2015 15:09:03 +0200 Subject: [PATCH] Move config tools to own file --- vdirsyncer/cli/__init__.py | 2 +- vdirsyncer/cli/config.py | 177 +++++++++++++++++++++++++++++++++++++ vdirsyncer/cli/utils.py | 175 +----------------------------------- 3 files changed, 179 insertions(+), 175 deletions(-) create mode 100644 vdirsyncer/cli/config.py diff --git a/vdirsyncer/cli/__init__.py b/vdirsyncer/cli/__init__.py index f1f1cad..c56c078 100644 --- a/vdirsyncer/cli/__init__.py +++ b/vdirsyncer/cli/__init__.py @@ -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() diff --git a/vdirsyncer/cli/config.py b/vdirsyncer/cli/config.py new file mode 100644 index 0000000..0b653f0 --- /dev/null +++ b/vdirsyncer/cli/config.py @@ -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)) diff --git a/vdirsyncer/cli/utils.py b/vdirsyncer/cli/utils.py index 3464068..93cf8fb 100644 --- a/vdirsyncer/cli/utils.py +++ b/vdirsyncer/cli/utils.py @@ -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)