mirror of
https://github.com/beetbox/beets.git
synced 2026-01-04 06:53:27 +01:00
sync with confit validation redesign
This commit is contained in:
parent
d645734195
commit
405390ac3a
2 changed files with 79 additions and 72 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue