diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 0d9917266..0429ac058 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -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. diff --git a/beets/util/confit.py b/beets/util/confit.py index c66355147..9f3cd5cc7 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -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):