Merge branch 'remove_cascading_config' of https://github.com/geigerzaehler/beets into geigerzaehler-remove_cascading_config

This commit is contained in:
Adrian Sampson 2014-02-21 18:14:24 -05:00
commit 7b544a2205
4 changed files with 233 additions and 83 deletions

View file

@ -332,12 +332,11 @@ class ConfigView(object):
return value
def as_filename(self):
"""Get a string as a normalized filename, made absolute and with
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.
"""Get a string as a normalized as an absolute path.
If the value is a relative path it is expanded relative to the
root configuration's ``config_dir()``. Tilde is also expaned
with ``os.path.expanduser()``.
"""
path, source = self.first()
if not isinstance(path, BASESTRING):
@ -346,14 +345,10 @@ class ConfigView(object):
))
path = os.path.expanduser(STRING(path))
if source.default:
if not os.path.isabs(path):
# 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):
@ -508,20 +503,24 @@ def _package_path(name):
return os.path.dirname(os.path.abspath(filepath))
def config_dirs():
"""Returns a list of user configuration directories to be searched.
"""Returns a list of candidates for user configuration directories
on the system.
"""
paths = []
if platform.system() == 'Darwin':
paths = [UNIX_DIR_FALLBACK, MAC_DIR]
paths.append(UNIX_DIR_FALLBACK)
paths.append(MAC_DIR)
if UNIX_DIR_VAR in os.environ:
paths.append(os.environ[UNIX_DIR_VAR])
elif platform.system() == 'Windows':
paths.append(WINDOWS_DIR_FALLBACK)
if WINDOWS_DIR_VAR in os.environ:
paths = [os.environ[WINDOWS_DIR_VAR]]
else:
paths = [WINDOWS_DIR_FALLBACK]
paths.append(os.environ[WINDOWS_DIR_VAR])
else:
# Assume Unix.
paths = [UNIX_DIR_FALLBACK]
paths.append(UNIX_DIR_FALLBACK)
if UNIX_DIR_VAR in os.environ:
paths.insert(0, os.environ[UNIX_DIR_VAR])
paths.append(os.environ[UNIX_DIR_VAR])
# Expand and deduplicate paths.
out = []
@ -622,38 +621,24 @@ class Configuration(RootView):
if read:
self.read()
def _search_dirs(self):
"""Yield directories that will be searched for configuration
files for this application.
def _add_user_source(self):
"""Add ``ConfigSource`` for the configuration file in
``config_dir()`` if it exists.
"""
# Application's environment variable.
if self._env_var in os.environ:
path = os.environ[self._env_var]
yield os.path.abspath(os.path.expanduser(path))
filename = os.path.join(self.config_dir(), CONFIG_FILENAME)
if os.path.isfile(filename):
self.add(ConfigSource(load_yaml(filename) or {}, filename))
# Standard configuration directories.
for confdir in config_dirs():
yield os.path.join(confdir, self.appname)
def _user_sources(self):
"""Generate `ConfigSource` objects for each user configuration
file in the program's search directories.
"""
for appdir in self._search_dirs():
filename = os.path.join(appdir, CONFIG_FILENAME)
if os.path.isfile(filename):
yield ConfigSource(load_yaml(filename) or {}, filename)
def _default_source(self):
"""Return the default-value source for this program or `None` if
it does not exist.
def _add_default_source(self):
"""Adds ``ConfigSource`` for the default configuration of the
package if it exists
"""
if self.modname:
pkg_path = _package_path(self.modname)
if pkg_path:
filename = os.path.join(pkg_path, DEFAULT_FILENAME)
if os.path.isfile(filename):
return ConfigSource(load_yaml(filename), filename, True)
self.add(ConfigSource(load_yaml(filename), filename, True))
def read(self, user=True, defaults=True):
"""Find and read the files for this configuration and set them
@ -662,27 +647,32 @@ class Configuration(RootView):
set `user` or `defaults` to `False`.
"""
if user:
for source in self._user_sources():
self.add(source)
self._add_user_source()
if defaults:
source = self._default_source()
if source:
self.add(source)
self._add_default_source()
def config_dir(self):
"""Get the path to the directory containing the highest-priority
user configuration. If no user configuration is present, create a
suitable directory before returning it.
"""Path of the user configuration directory
If the BEETSDIR environment variable is set it returns its
value. Otherwise look for existing ``beets/config.yaml`` in
``config_dirs()`` and returns it. If none of the files exists it
return the last path searched and makes sure the directory exists.
"""
dirs = list(self._search_dirs())
if self._env_var in os.environ:
path = os.environ[self._env_var]
return os.path.abspath(os.path.expanduser(path))
dirs = []
for confdir in config_dirs():
dirs.append(os.path.join(confdir, self.appname))
# First, look for an existent configuration file.
for appdir in dirs:
if os.path.isfile(os.path.join(appdir, CONFIG_FILENAME)):
return appdir
# As a fallback, create the first-listed directory name.
appdir = dirs[0]
# Fallback to the last path
if not os.path.isdir(appdir):
os.makedirs(appdir)
return appdir

View file

@ -317,7 +317,10 @@ import ...``.
* ``-d DIRECTORY``: specify the library root directory.
* ``-v``: verbose mode; prints out a deluge of debugging information. Please use
this flag when reporting bugs.
* ``-c FILE``: read a specified YAML configuration file.
* ``-c FILE``: read a specified YAML :doc:`configuration file <config>`.
Beets also uses the ``BEETSDIR`` environment variable to look for
configuration and data.
.. only:: man

View file

@ -5,17 +5,14 @@ Beets has an extensive configuration system that lets you customize nearly
every aspect of its operation. To configure beets, you'll edit a file called
``config.yaml``. The location of this file depends on your OS:
* On Unix-like OSes (including OS X), you want ``~/.config/beets/config.yaml``.
* On Unix-like OSes, you want ``~/.config/beets/config.yaml``.
* On Windows, use ``%APPDATA%\beets\config.yaml``. This is usually in a
directory like ``C:\Users\You\AppData\Roaming``.
* On OS X, you can also use ``~/Library/Application Support/beets/config.yaml``
if you prefer that over the Unix-like ``~/.config``.
* If you prefer a different location, set the ``BEETSDIR`` environment
variable to a path; beets will then look for a ``config.yaml`` in that
directory.
* Or specify an *additional* configuration file to load using the ``--config
/path/to/file`` option on the command line. The options will be combined
with any options already specified your default config file.
* On OS X, it is ``~/Library/Application Support/beets/config.yaml``.
It is also possible to customize the location of the configuration file
and even use multiple layers of configuration. Just have a look at
`Configuration Location`_.
The config file uses `YAML`_ syntax. You can use the full power of YAML, but
most configuration options are simple key/value pairs. This means your config
@ -554,6 +551,64 @@ fact, just shorthand for the explicit queries ``singleton:true`` and
``comp:true``. In contrast, ``default`` is special and has no query equivalent:
the ``default`` format is only used if no queries match.
Configuration Location
----------------------
Beets has three layers of configuration; each overwriting the previous
one.
The first layer is the *default configuration*. It comes with your beets
distribution and cannot be changed. The second configuration layer is
the *user configuration*. Here you can customize the configuration to
use when running ``beet`` on your command line.
The path for the user configuration is given by
``$BEETSDIR/config.yaml``. Here ``BEETSDIR``, is the directory beets
uses to store its application specific data and resolve relative paths.
By default, ``BEETSDIR`` is determined by your system's convention for
storing application configuration, but may be set by the ``BEETSDIR``
environment variable. This allows you to manage mutliple beets
libraries with separate configurations. To be more precise, the
following algorithm is used to determine ``BEETSDIR``.
1. If the ``BEETSDIR`` environment variable is set, then use it and
stop.
2. Otherwise, generate a platform-dependent list of directories to
search.
- On Windows: ``~\AppData\Roaming\beets`` and then
``%APPDATA%\beets``, if the environment variable is set
- On non-Mac Unixes: ``~/.config/beets`` and then
``$XDG_CONFIG_DIR/beets``, if the environment variable is set
- On OS X: ``~/.config/beets``, then
``~/Library/Application Support/beets``, and finally
``$XDG_CONFIG_DIR/beets``, if the environment variable is set
3. Look in each directory in turn for a ``config.yaml``. Set
``BEETSDIR`` to the *first* directory that contains this file. If no
directory is found containing ``config.yaml``, then use the *last*
directory in the list.
Finally, the ``--config CONFIGFILE`` command line option serves as the
third layer. ``CONFIGFILE`` is the path of a YAML file that contains
additional configuration overwriting the user configuration. This is
helpful, for example, if you have different strategies for importing
files, each with its own set of importer configuration.
In addition some command line options overwrite configuration values.
For example the command ::
$ beets --library /path/to/lib import --timid /path/to/import
uses the ``library`` and ``importer.timid`` values from the command line
instead of the user configuration.
.. _config-example:
Example

View file

@ -471,9 +471,15 @@ class ConfigTest(_common.TestCase):
self.test_cmd = self._make_test_cmd()
commands.default_commands.append(self.test_cmd)
config_dir = os.path.join(self.temp_dir, '.config', 'beets')
os.makedirs(config_dir)
self.user_config_path = os.path.join(config_dir, 'config.yaml')
# Default user configuration
self.user_config_dir = os.path.join(self.temp_dir, '.config', 'beets')
os.makedirs(self.user_config_dir)
self.user_config_path = os.path.join(self.user_config_dir,
'config.yaml')
# Custom BEETSDIR
self.beetsdir = os.path.join(self.temp_dir, 'beetsdir')
os.makedirs(self.beetsdir)
self._reset_config()
@ -567,27 +573,10 @@ class ConfigTest(_common.TestCase):
ui._raw_main(['--config', config_path, 'test'])
self.assertEqual(config['anoption'].get(), 'value')
def test_beetsdir_config_file_overwrites_defaults(self):
with open(self.user_config_path, 'w') as file:
file.write('anoption: value')
env_config_path = os.path.join(self.temp_dir, 'config.yaml')
os.environ['BEETSDIR'] = self.temp_dir
with open(env_config_path, 'w') as file:
file.write('anoption: overwrite')
ui.main(['test'])
self.assertEqual(config['anoption'].get(), 'overwrite')
def test_cli_config_file_overwrites_user_defaults(self):
with open(self.user_config_path, 'w') as file:
file.write('anoption: value')
env_config_path = os.path.join(self.temp_dir, 'config.yaml')
os.environ['BEETSDIR'] = self.temp_dir
with open(env_config_path, 'w') as file:
file.write('anoption: overwrite')
cli_config_path = os.path.join(self.temp_dir, 'config.yaml')
with open(cli_config_path, 'w') as file:
file.write('anoption: cli overwrite')
@ -595,6 +584,77 @@ class ConfigTest(_common.TestCase):
ui._raw_main(['--config', cli_config_path, 'test'])
self.assertEqual(config['anoption'].get(), 'cli overwrite')
def test_cli_config_file_overwrites_beetsdir_defaults(self):
os.environ['BEETSDIR'] = self.beetsdir
env_config_path = os.path.join(self.beetsdir, 'config.yaml')
with open(env_config_path, 'w') as file:
file.write('anoption: value')
cli_config_path = os.path.join(self.temp_dir, 'config.yaml')
with open(cli_config_path, 'w') as file:
file.write('anoption: cli overwrite')
ui._raw_main(['--config', cli_config_path, 'test'])
self.assertEqual(config['anoption'].get(), 'cli overwrite')
@unittest.skip('Difficult to implement with optparse')
def test_multiple_cli_config_files(self):
cli_config_path_1 = os.path.join(self.temp_dir, 'config.yaml')
cli_config_path_2 = os.path.join(self.temp_dir, 'config_2.yaml')
with open(cli_config_path_1, 'w') as file:
file.write('first: value')
with open(cli_config_path_2, 'w') as file:
file.write('second: value')
ui._raw_main(['--config', cli_config_path_1,
'--config', cli_config_path_2, 'test'])
self.assertEqual(config['first'].get(), 'value')
self.assertEqual(config['second'].get(), 'value')
@unittest.skip('Difficult to implement with optparse')
def test_multiple_cli_config_overwrite(self):
cli_config_path = os.path.join(self.temp_dir, 'config.yaml')
cli_overwrite_config_path = os.path.join(self.temp_dir,
'overwrite_config.yaml')
with open(cli_config_path, 'w') as file:
file.write('anoption: value')
with open(cli_overwrite_config_path, 'w') as file:
file.write('anoption: overwrite')
ui._raw_main(['--config', cli_config_path,
'--config', cli_overwrite_config_path, 'test'])
self.assertEqual(config['anoption'].get(), 'cli overwrite')
def test_cli_config_paths_resolve_relative_to_user_dir(self):
cli_config_path = os.path.join(self.temp_dir, 'config.yaml')
with open(cli_config_path, 'w') as file:
file.write('library: beets.db\n')
file.write('statefile: state')
ui._raw_main(['--config', cli_config_path, 'test'])
self.assertEqual(config['library'].as_filename(),
os.path.join(self.user_config_dir, 'beets.db'))
self.assertEqual(config['statefile'].as_filename(),
os.path.join(self.user_config_dir, 'state'))
def test_cli_config_paths_resolve_relative_to_beetsdir(self):
os.environ['BEETSDIR'] = self.beetsdir
cli_config_path = os.path.join(self.temp_dir, 'config.yaml')
with open(cli_config_path, 'w') as file:
file.write('library: beets.db\n')
file.write('statefile: state')
ui._raw_main(['--config', cli_config_path, 'test'])
self.assertEqual(config['library'].as_filename(),
os.path.join(self.beetsdir, 'beets.db'))
self.assertEqual(config['statefile'].as_filename(),
os.path.join(self.beetsdir, 'state'))
def test_cli_config_file_loads_plugin_commands(self):
plugin_path = os.path.join(_common.RSRC, 'beetsplug')
@ -606,6 +666,48 @@ class ConfigTest(_common.TestCase):
ui._raw_main(['--config', cli_config_path, 'plugin'])
self.assertTrue(plugins.find_plugins()[0].is_test_plugin)
def test_beetsdir_config(self):
os.environ['BEETSDIR'] = self.beetsdir
env_config_path = os.path.join(self.beetsdir, 'config.yaml')
with open(env_config_path, 'w') as file:
file.write('anoption: overwrite')
config.read()
self.assertEqual(config['anoption'].get(), 'overwrite')
def test_beetsdir_config_does_not_load_default_user_config(self):
os.environ['BEETSDIR'] = self.beetsdir
with open(self.user_config_path, 'w') as file:
file.write('anoption: value')
config.read()
self.assertFalse(config['anoption'].exists())
def test_default_config_paths_resolve_relative_to_beetsdir(self):
os.environ['BEETSDIR'] = self.beetsdir
config.read()
self.assertEqual(config['library'].as_filename(),
os.path.join(self.beetsdir, 'library.db'))
self.assertEqual(config['statefile'].as_filename(),
os.path.join(self.beetsdir, 'state.pickle'))
def test_beetsdir_config_paths_resolve_relative_to_beetsdir(self):
os.environ['BEETSDIR'] = self.beetsdir
env_config_path = os.path.join(self.beetsdir, 'config.yaml')
with open(env_config_path, 'w') as file:
file.write('library: beets.db\n')
file.write('statefile: state')
config.read()
self.assertEqual(config['library'].as_filename(),
os.path.join(self.beetsdir, 'beets.db'))
self.assertEqual(config['statefile'].as_filename(),
os.path.join(self.beetsdir, 'state'))
class ShowdiffTest(_common.TestCase):
def setUp(self):