mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-03 10:25:51 +00:00
Move config tools to own file
This commit is contained in:
parent
565ef2e96e
commit
32abaae9b9
3 changed files with 179 additions and 175 deletions
|
|
@ -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
177
vdirsyncer/cli/config.py
Normal 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))
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue