sync with confit validation redesign

This commit is contained in:
Adrian Sampson 2012-09-09 21:44:48 -07:00
parent d645734195
commit 405390ac3a
2 changed files with 79 additions and 72 deletions

View file

@ -406,37 +406,21 @@ def colordiff(a, b, highlight='red'):
return u''.join(a_out), u''.join(b_out)
def _as_pairs(view, value):
"""Confit validation function that reads a list of single-element
dictionaries as a list of pairs.
def get_path_formats():
"""Get the configuration's path formats as a list of query/template
pairs.
"""
if not isinstance(value, list):
raise confit.ConfigTypeError('{0} must be a list'.format(view.name))
out = []
for dic in value:
if not isinstance(dic, dict) or len(dic) != 1:
raise confit.ConfigTypeError(
'{0} elements must be single-element maps'.format(view.name)
)
out.append(dic.items()[0])
return out
def _as_path_formats(view, value):
"""Confit validation function that gets a list of path formats,
which are query/template pairs.
"""
pairs = _as_pairs(view, value)
pairs = config['paths'].as_pairs(True)
path_formats = []
for query, fmt in pairs:
query = PF_KEY_QUERIES.get(query, query) # Expand common queries.
path_formats.append((query, Template(fmt)))
# FIXME append defaults
return path_formats
def _as_replacements(view, value):
def get_replacements():
"""Confit validation function that reads regex/string pairs.
"""
pairs = _as_pairs(view, value)
pairs = config['replace'].as_pairs()
# FIXME handle regex compilation errors
return [(re.compile(k), v) for (k, v) in pairs]
@ -638,12 +622,12 @@ def _raw_main(args, configfh):
# Open library file.
try:
lib = library.Library(
config['library'].get(confit.as_filename),
config['directory'].get(confit.as_filename),
config['paths'].get(_as_path_formats),
config['library'].as_filename(),
config['directory'].as_filename(),
get_path_formats(),
config['art_filename'].get(unicode),
config['timeout'].get(confit.as_number),
config['replace'].get(_as_replacements),
config['timeout'].as_number(),
get_replacements(),
)
except sqlite3.OperationalError:
raise UserError("database file %s could not be opened" % FIXME)
@ -675,7 +659,7 @@ def main(args=None, configfh=None):
exc.log(log)
sys.exit(1)
except confit.ConfigError as exc:
xxx
FIXME
except IOError as exc:
if exc.errno == errno.EPIPE:
# "Broken pipe". End silently.

View file

@ -20,8 +20,9 @@ import os
import pkgutil
import sys
import yaml
import types
UNIX_DIR_VAR = 'XDG_DATA_HOME'
UNIX_DIR_VAR = 'XDG_CONFIG_HOME'
UNIX_DIR_FALLBACK = '~/.config'
WINDOWS_DIR_VAR = 'APPDATA'
WINDOWS_DIR_FALLBACK = '~\\AppData\\Roaming'
@ -35,7 +36,8 @@ DEFAULT_FILENAME = 'config_default.yaml'
PY3 = sys.version_info[0] == 3
STRING = str if PY3 else unicode
NUMERIC_TYPES = [int, float] if PY3 else [int, float, long]
NUMERIC_TYPES = (int, float) if PY3 else (int, float, long)
TYPE_TYPES = (type,) if PY3 else (type, types.ClassType)
def iter_first(sequence):
"""Get the first element from an iterable or raise a ValueError if
@ -122,9 +124,11 @@ class ConfigView(object):
except ValueError:
raise NotFoundError("{0} not found".format(self.name))
# Validate/convert.
if isinstance(typ, type):
# Check type of value.
# Validate type.
if typ is not None:
if not isinstance(typ, TYPE_TYPES):
raise TypeError('argument to get() must be a type')
if not isinstance(value, typ):
raise ConfigTypeError(
"{0} must be of type {1}, not {2}".format(
@ -132,10 +136,6 @@ class ConfigView(object):
)
)
elif typ is not None:
# typ must a callable that takes this view and the value.
value = typ(self, value)
return value
def __repr__(self):
@ -236,6 +236,64 @@ class ConfigView(object):
for value in it:
yield value
# Explicit validators/converters.
def as_filename(self):
"""Get a string as a normalized filename, made absolute and with
tilde expanded.
"""
value = STRING(self.get())
return os.path.abspath(os.path.expanduser(value))
def as_choice(self, choices):
"""Ensure that the value is among a collection of choices and
return it.
"""
value = self.get()
if value not in choices:
raise ConfigValueError(
'{0} must be one of {1}, not {2}'.format(
self.name, repr(value), repr(list(choices))
)
)
return value
def as_number(self):
"""Ensure that a value is of numeric type."""
value = self.get()
if isinstance(value, NUMERIC_TYPES):
return value
raise ConfigTypeError(
'{0} must be numeric, not {1}'.format(
self.name, type(value).__name__
)
)
def as_pairs(self, all_sources=False):
"""Ensure that the value is a list whose elements are either
pairs (two-element lists) or single-entry dictionaries (which
have a slightly nicer syntax in YAML). Return a list of pairs
(tuples). If `all_sources`, then the values from all sources are
concatenated.
"""
if all_sources:
it = self.all_contents()
else:
it = self.get(list)
out = []
for item in it:
if isinstance(item, list) and len(item) == 2:
out.append(tuple(item))
elif isinstance(item, dict) and len(item) == 1:
out.append(iter_first(item.items()))
else:
raise ConfigValueError(
'{0} must be a list of pairs'.format(self.name)
)
return out
class RootView(ConfigView):
"""The base of a view hierarchy. This view keeps track of the
sources that may be accessed by subviews.
@ -344,41 +402,6 @@ def config_dirs():
return out
# Validation and conversion helpers.
def as_filename(view, value):
"""Gets a string as a normalized filename, made absolute and with
tilde expanded.
"""
value = STRING(value)
return os.path.abspath(os.path.expanduser(value))
def as_choice(choices):
"""Returns a function that ensures that the value is one of a
collection of choices.
"""
def f(view, value):
if value not in choices:
raise ConfigValueError(
'{0} must be one of {1}, not {2}'.format(
view.name, repr(value), repr(list(choices))
)
)
return value
return f
def as_number(view, value):
"""Ensure that a value is of numeric type."""
for typ in NUMERIC_TYPES:
if isinstance(value, typ):
return value
raise ConfigTypeError(
'{0} must be numeric, not {1}'.format(
view.name, type(value).__name__
)
)
# YAML.
class Loader(yaml.SafeLoader):