mirror of
https://github.com/beetbox/beets.git
synced 2026-01-03 22:42:44 +01:00
default file locations
Due mostly to some improvements in Confit, we now have a reasonable way to define the default filenames of auxiliary data files. These are relative to the beets config directory (i.e., alongside config.yaml).
This commit is contained in:
parent
51e9c519d4
commit
123189b393
4 changed files with 138 additions and 67 deletions
|
|
@ -1,4 +1,4 @@
|
|||
library: ~/.beetsmusic.blb
|
||||
library: library.db
|
||||
directory: ~/Music
|
||||
|
||||
import:
|
||||
|
|
@ -40,3 +40,5 @@ paths:
|
|||
default: $albumartist/$album%aunique{}/$track $title
|
||||
singleton: Non-Album/$artist/$title
|
||||
comp: Compilations/$album%aunique{}/$track $title
|
||||
|
||||
statefile: state.pickle
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ action = enum(
|
|||
)
|
||||
|
||||
QUEUE_SIZE = 128
|
||||
STATE_FILE = os.path.expanduser('~/.beetsstate')
|
||||
SINGLE_ARTIST_THRESH = 0.25
|
||||
VARIOUS_ARTISTS = u'Various Artists'
|
||||
|
||||
|
|
@ -146,14 +145,14 @@ def _resume():
|
|||
def _open_state():
|
||||
"""Reads the state file, returning a dictionary."""
|
||||
try:
|
||||
with open(STATE_FILE) as f:
|
||||
with open(config['statefile'].as_filename()) as f:
|
||||
return pickle.load(f)
|
||||
except (IOError, EOFError):
|
||||
return {}
|
||||
def _save_state(state):
|
||||
"""Writes the state dictionary out to disk."""
|
||||
try:
|
||||
with open(STATE_FILE, 'w') as f:
|
||||
with open(config['statefile'].as_filename(), 'w') as f:
|
||||
pickle.dump(state, f)
|
||||
except IOError as exc:
|
||||
log.error(u'state file could not be written: %s' % unicode(exc))
|
||||
|
|
|
|||
|
|
@ -87,7 +87,38 @@ class ConfigReadError(ConfigError):
|
|||
super(ConfigReadError, self).__init__(message)
|
||||
|
||||
|
||||
# Views and data access logic.
|
||||
# Views and sources.
|
||||
|
||||
class ConfigSource(dict):
|
||||
"""A dictionary augmented with metadata about the source of the
|
||||
configuration.
|
||||
"""
|
||||
def __init__(self, value, filename=None, default=False):
|
||||
super(ConfigSource, self).__init__(value)
|
||||
if filename is not None and not isinstance(filename, BASESTRING):
|
||||
raise TypeError('filename must be a string or None')
|
||||
self.filename = filename
|
||||
self.default = default
|
||||
|
||||
def __repr__(self):
|
||||
return 'ConfigSource({0}, {1}, {2})'.format(
|
||||
super(ConfigSource, self).__repr__(),
|
||||
repr(self.filename),
|
||||
repr(self.default)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def of(self, value):
|
||||
"""Given either a dictionary or a `ConfigSource` object, return
|
||||
a `ConfigSource` object. This lets a function accept either type
|
||||
of object as an argument.
|
||||
"""
|
||||
if isinstance(value, ConfigSource):
|
||||
return value
|
||||
elif isinstance(value, dict):
|
||||
return ConfigSource(value)
|
||||
else:
|
||||
raise TypeError('source value must be a dict')
|
||||
|
||||
class ConfigView(object):
|
||||
"""A configuration "view" is a query into a program's configuration
|
||||
|
|
@ -103,42 +134,26 @@ class ConfigView(object):
|
|||
configuration in Python-like syntax (e.g., ``foo['bar'][42]``).
|
||||
"""
|
||||
|
||||
def get_all(self):
|
||||
"""Generates all available values for the view in the order of
|
||||
the configuration's sources. (Each source may have at most one
|
||||
value for each view.) If no values are available, no values are
|
||||
generated. If a type error is encountered when traversing a
|
||||
source to resolve the view, a ConfigTypeError may be raised.
|
||||
def resolve(self):
|
||||
"""The core (internal) data retrieval method. Generates (value,
|
||||
source) pairs for each source that contains a value for this
|
||||
view. May raise ConfigTypeError if a type error occurs while
|
||||
traversing a source.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, typ=None):
|
||||
"""Returns the canonical value for the view. This amounts to the
|
||||
first item in ``view.get_all()``. If the view cannot be
|
||||
resolved, this method raises a NotFoundError.
|
||||
def first(self):
|
||||
"""Returns a (value, source) pair for the first object found for
|
||||
this view. This amounts to the first element returned by
|
||||
`resolve`. If no values are available, a NotFoundError is
|
||||
raised.
|
||||
"""
|
||||
values = self.get_all()
|
||||
|
||||
# Get the first value.
|
||||
pairs = self.resolve()
|
||||
try:
|
||||
value = iter_first(values)
|
||||
return iter_first(pairs)
|
||||
except ValueError:
|
||||
raise NotFoundError("{0} not found".format(self.name))
|
||||
|
||||
# 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(
|
||||
self.name, typ.__name__, type(value).__name__
|
||||
)
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
def add(self, value):
|
||||
"""Set the *default* value for this configuration view. The
|
||||
specified value is added as the lowest-priority configuration
|
||||
|
|
@ -153,6 +168,11 @@ class ConfigView(object):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def root(self):
|
||||
"""The RootView object from which this view is descended.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
return '<ConfigView: %s>' % self.name
|
||||
|
||||
|
|
@ -214,7 +234,7 @@ class ConfigView(object):
|
|||
"""
|
||||
keys = []
|
||||
|
||||
for dic in self.get_all():
|
||||
for dic, _ in self.resolve():
|
||||
try:
|
||||
cur_keys = dic.keys()
|
||||
except AttributeError:
|
||||
|
|
@ -255,7 +275,7 @@ class ConfigView(object):
|
|||
intended to be used when the view indicates a list; this method
|
||||
will concatenate the contents of the list from all sources.
|
||||
"""
|
||||
for collection in self.get_all():
|
||||
for collection, _ in self.resolve():
|
||||
try:
|
||||
it = iter(collection)
|
||||
except TypeError:
|
||||
|
|
@ -267,14 +287,49 @@ class ConfigView(object):
|
|||
for value in it:
|
||||
yield value
|
||||
|
||||
# Explicit validators/converters.
|
||||
# Validation and conversion.
|
||||
|
||||
def get(self, typ=None):
|
||||
"""Returns the canonical value for the view, checked against the
|
||||
passed-in type. If the value is not an instance of the given
|
||||
type, a ConfigTypeError is raised. May also raise a
|
||||
NotFoundError.
|
||||
"""
|
||||
value, _ = self.first()
|
||||
|
||||
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(
|
||||
self.name, typ.__name__, type(value).__name__
|
||||
)
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
def as_filename(self):
|
||||
"""Get a string as a normalized filename, made absolute and with
|
||||
tilde expanded.
|
||||
tilde expanded. If the value comes from a default source, the
|
||||
path is considered relative to the application's config
|
||||
directory. If it comes from another file source, the filename is
|
||||
expanded as if it were relative to that directory. Otherwise, it
|
||||
is relative to the current working directory.
|
||||
"""
|
||||
value = STRING(self.get())
|
||||
return os.path.abspath(os.path.expanduser(value))
|
||||
path, source = self.first()
|
||||
path = os.path.expanduser(STRING(path))
|
||||
|
||||
if source.default:
|
||||
# From defaults: relative to the app's directory.
|
||||
path = os.path.join(self.root().config_dir(), path)
|
||||
|
||||
elif source.filename is not None:
|
||||
# Relative to source filename's directory.
|
||||
path = os.path.join(os.path.dirname(source.filename), path)
|
||||
|
||||
return os.path.abspath(path)
|
||||
|
||||
def as_choice(self, choices):
|
||||
"""Ensure that the value is among a collection of choices and
|
||||
|
|
@ -333,13 +388,20 @@ class RootView(ConfigView):
|
|||
self.name = ROOT_NAME
|
||||
|
||||
def add(self, obj):
|
||||
self.sources.append(obj)
|
||||
self.sources.append(ConfigSource.of(obj))
|
||||
|
||||
def set(self, value):
|
||||
self.sources.insert(0, value)
|
||||
self.sources.insert(0, ConfigSource.of(value))
|
||||
|
||||
def get_all(self):
|
||||
return self.sources
|
||||
def resolve(self):
|
||||
return ((dict(s), s) for s in self.sources)
|
||||
|
||||
def clear(self):
|
||||
"""Remove all sources from this configuration."""
|
||||
del self.sources[:]
|
||||
|
||||
def root(self):
|
||||
return self
|
||||
|
||||
class Subview(ConfigView):
|
||||
"""A subview accessed via a subscript of a parent view."""
|
||||
|
|
@ -363,8 +425,8 @@ class Subview(ConfigView):
|
|||
else:
|
||||
self.name += '{0}'.format(repr(self.key))
|
||||
|
||||
def get_all(self):
|
||||
for collection in self.parent.get_all():
|
||||
def resolve(self):
|
||||
for collection, source in self.parent.resolve():
|
||||
try:
|
||||
value = collection[self.key]
|
||||
except IndexError:
|
||||
|
|
@ -380,7 +442,7 @@ class Subview(ConfigView):
|
|||
self.parent.name, type(collection).__name__
|
||||
)
|
||||
)
|
||||
yield value
|
||||
yield value, source
|
||||
|
||||
def set(self, value):
|
||||
self.parent.set({self.key: value})
|
||||
|
|
@ -388,6 +450,9 @@ class Subview(ConfigView):
|
|||
def add(self, value):
|
||||
self.parent.add({self.key: value})
|
||||
|
||||
def root(self):
|
||||
return self.parent.root()
|
||||
|
||||
|
||||
# Config file paths, including platform-specific paths and in-package
|
||||
# defaults.
|
||||
|
|
@ -539,28 +604,25 @@ class Configuration(RootView):
|
|||
for confdir in config_dirs():
|
||||
yield os.path.join(confdir, self.appname)
|
||||
|
||||
def _filenames(self, user=True, defaults=True):
|
||||
"""Get a list of filenames for configuration files. The files
|
||||
must actually exist and are placed in the order that they should
|
||||
be prioritized. The `user` and `defaults` flags control whether
|
||||
files should be used from discovered configuration directories
|
||||
and from the in-package defaults directory; set either of these
|
||||
to `False` to disable searching for them.
|
||||
def _user_sources(self):
|
||||
"""Generate `ConfigSource` objects for each user configuration
|
||||
file in the program's search directories.
|
||||
"""
|
||||
out = []
|
||||
for appdir in self._search_dirs():
|
||||
filename = os.path.join(appdir, CONFIG_FILENAME)
|
||||
if os.path.isfile(filename):
|
||||
yield ConfigSource(load_yaml(filename), filename)
|
||||
|
||||
# Search standard directories.
|
||||
if user:
|
||||
for appdir in self._search_dirs():
|
||||
out.append(os.path.join(appdir, CONFIG_FILENAME))
|
||||
|
||||
# Search the package for a defaults file.
|
||||
if defaults and self.modname:
|
||||
def _default_source(self):
|
||||
"""Return the default-value source for this program or `None` if
|
||||
it does not exist.
|
||||
"""
|
||||
if self.modname:
|
||||
pkg_path = _package_path(self.modname)
|
||||
if pkg_path:
|
||||
out.append(os.path.join(pkg_path, DEFAULT_FILENAME))
|
||||
|
||||
return [p for p in out if os.path.isfile(p)]
|
||||
filename = os.path.join(pkg_path, DEFAULT_FILENAME)
|
||||
if os.path.isfile(filename):
|
||||
return ConfigSource(load_yaml(filename), filename, True)
|
||||
|
||||
def read(self, user=True, defaults=True):
|
||||
"""Find and read the files for this configuration and set them
|
||||
|
|
@ -568,9 +630,13 @@ class Configuration(RootView):
|
|||
discovered user configuration files or the in-package defaults,
|
||||
set `user` or `defaults` to `False`.
|
||||
"""
|
||||
self.sources = []
|
||||
for filename in self._filenames(user, defaults):
|
||||
self.sources.append(load_yaml(filename))
|
||||
if user:
|
||||
for source in self._user_sources():
|
||||
self.add(source)
|
||||
if defaults:
|
||||
source = self._default_source()
|
||||
if source:
|
||||
self.add(source)
|
||||
|
||||
def config_dir(self):
|
||||
"""Get the path to the directory containing the highest-priority
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ This release entirely revamps beets' configuration system.
|
|||
``autoembed``, etc.) but is now consistently called ``auto``.
|
||||
* Reorganized import config options: The various ``import_*`` options are now
|
||||
organized under an ``import:`` heading and their prefixes have been removed.
|
||||
* New default file locations: The default filename of the library database is
|
||||
now ``library.db`` in the same directory as the config file, as opposed to
|
||||
``~/.beetsmusic.blb`` previously. Similarly, the runtime state file is now
|
||||
called ``state.pickle`` in the same directory instead of ``~/.beetsstate``.
|
||||
|
||||
1.0rc2 (in development)
|
||||
-----------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue