From c8e32f6bef06244234d5adf031b4867ca6eb6276 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Tue, 18 Feb 2014 20:56:08 +0100 Subject: [PATCH 1/2] Add config command --- beets/ui/commands.py | 58 ++++++++++++++++++ beets/util/confit.py | 15 ++++- test/_common.py | 10 ++++ test/test_config_command.py | 115 ++++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 test/test_config_command.py diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 88c09a010..8622c139a 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -23,6 +23,8 @@ import time import itertools import codecs from datetime import datetime +import yaml +import platform import beets from beets import ui @@ -35,6 +37,7 @@ from beets import importer from beets import util from beets.util import syspath, normpath, ancestry, displayable_path from beets.util.functemplate import Template +from beets.util.confit import ConfigTypeError from beets import library from beets import config @@ -1286,3 +1289,58 @@ def write_func(lib, opts, args): write_items(lib, decargs(args), opts.pretend) write_cmd.func = write_func default_commands.append(write_cmd) + + +config_cmd = ui.Subcommand('config', help='show or edit the user configuration') +config_cmd.parser.add_option('-p', '--paths', action='store_true', + help='show files that configuration was loaded from') +config_cmd.parser.add_option('-e', '--edit', action='store_true', + help='edit user configuration with $EDITOR') +config_cmd.parser.add_option('-d', '--defaults', action='store_true', + help='include the default configuration') +def _config_get(view): + try: + keys = view.keys() + except ConfigTypeError: + return view.get() + else: + return dict((key, _config_get(view[key])) for key in view.keys()) +def config_func(lib, opts, args): + # Make sure lazy configuration is loaded + config.resolve() + + if not opts.defaults: + # Remove default source + config.sources = [source for source in config.sources if not source.default] + + if opts.paths: + for source in config.sources: + if source.filename: + print(source.filename) + elif opts.edit: + path = config.user_config_path() + + if 'EDITOR' in os.environ: + editor = os.environ['EDITOR'] + args = [editor, editor, path] + elif platform.system() == 'Darwin': + args = ['open', 'open', '-n', path] + elif platform.system() == 'Windows': + # On windows we can execute arbitrary files. The os will + # take care of starting an appropriate application + args = [path, path] + else: + # Assume Unix + args = ['xdg-open', 'xdg-open', path] + + try: + os.execlp(*args) + except OSError: + raise ui.UserError("Could not edit configuration. Please" + "set the EDITOR environment variable.") + else: + config_dict = _config_get(config) + print(yaml.safe_dump(config_dict, default_flow_style=False)) + +config_cmd.func = config_func +default_commands.append(config_cmd) diff --git a/beets/util/confit.py b/beets/util/confit.py index aa0bd1e1b..be12c42e4 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -621,11 +621,18 @@ class Configuration(RootView): if read: self.read() + def user_config_path(self): + """Points to the location of the user configuration. + + The file may not exist. + """ + return os.path.join(self.config_dir(), CONFIG_FILENAME) + def _add_user_source(self): """Add ``ConfigSource`` for the configuration file in ``config_dir()`` if it exists. """ - filename = os.path.join(self.config_dir(), CONFIG_FILENAME) + filename = self.user_config_path() if os.path.isfile(filename): self.add(ConfigSource(load_yaml(filename) or {}, filename)) @@ -720,3 +727,9 @@ class LazyConfig(Configuration): # Buffer additions to beginning. self._lazy_prefix[:0] = self.sources del self.sources[:] + + def clear(self): + """Remove all sources from this configuration.""" + del self.sources[:] + self._lazy_suffix = [] + self._lazy_prefix = [] diff --git a/test/_common.py b/test/_common.py index 00640f5bc..3a2011cd8 100644 --- a/test/_common.py +++ b/test/_common.py @@ -268,3 +268,13 @@ def platform_posix(): os.path = posixpath yield os.path = old_path + +@contextmanager +def system_mock(name): + import platform + old_system = platform.system + platform.system = lambda: name + try: + yield + finally: + platform.system = old_system diff --git a/test/test_config_command.py b/test/test_config_command.py new file mode 100644 index 000000000..5ef285dcd --- /dev/null +++ b/test/test_config_command.py @@ -0,0 +1,115 @@ +import os +import yaml + +from beets import ui +from beets import config + +import _common + + +class ConfigCommandTest(_common.TestCase): + + def setUp(self): + super(ConfigCommandTest, self).setUp() + self.io.install() + + if 'EDITOR' in os.environ: + del os.environ['EDITOR'] + + os.environ['BEETSDIR'] = self.temp_dir + self.config_path = os.path.join(self.temp_dir, 'config.yaml') + with open(self.config_path, 'w') as file: + file.write('library: lib\n') + file.write('option: value') + + self.cli_config_path = os.path.join(self.temp_dir, 'cli_config.yaml') + with open(self.cli_config_path, 'w') as file: + file.write('option: cli overwrite') + + config.clear() + config._materialized = False + + def tearDown(self): + super(ConfigCommandTest, self).tearDown() + self.execlp_restore() + + def test_show_user_config(self): + ui._raw_main(['config']) + output = yaml.load(self.io.getoutput()) + self.assertEqual(output['option'], 'value') + + def test_show_user_config_with_defaults(self): + ui._raw_main(['config', '-d']) + output = yaml.load(self.io.getoutput()) + self.assertEqual(output['option'], 'value') + self.assertEqual(output['library'], 'lib') + self.assertEqual(output['import']['timid'], False) + + def test_show_user_config_with_cli(self): + ui._raw_main(['--config', self.cli_config_path, 'config']) + output = yaml.load(self.io.getoutput()) + self.assertEqual(output['library'], 'lib') + self.assertEqual(output['option'], 'cli overwrite') + + def test_config_paths(self): + ui._raw_main(['config', '-p']) + paths = self.io.getoutput().split('\n') + self.assertEqual(len(paths), 2) + self.assertEqual(paths[0], self.config_path) + + def test_config_paths_with_cli(self): + ui._raw_main(['--config', self.cli_config_path, 'config', '-p']) + paths = self.io.getoutput().split('\n') + self.assertEqual(len(paths), 3) + self.assertEqual(paths[0], self.cli_config_path) + + def test_edit_config_with_editor_env(self): + self.execlp_stub() + os.environ['EDITOR'] = 'myeditor' + + ui._raw_main(['config', '-e']) + self.assertEqual(self._execlp_call, ['myeditor', self.config_path]) + + def test_edit_config_with_open(self): + self.execlp_stub() + + with _common.system_mock('Darwin'): + ui._raw_main(['config', '-e']) + self.assertEqual(self._execlp_call, ['open', '-n', self.config_path]) + + + def test_edit_config_with_xdg_open(self): + self.execlp_stub() + + with _common.system_mock('Linux'): + ui._raw_main(['config', '-e']) + self.assertEqual(self._execlp_call, ['xdg-open', self.config_path]) + + def test_edit_config_with_windows_exec(self): + self.execlp_stub() + + with _common.system_mock('Windows'): + ui._raw_main(['config', '-e']) + self.assertEqual(self._execlp_call, [self.config_path]) + + def test_config_editor_not_found(self): + def raise_os_error(*args): + raise OSError + os.execlp = raise_os_error + with self.assertRaises(ui.UserError) as user_error: + ui._raw_main(['config', '-e']) + self.assertIn('Could not edit configuration', + str(user_error.exception.args[0])) + + + def execlp_stub(self): + self._execlp_call = None + def _execlp_stub(file, *args): + self._execlp_call = [file] + list(args[1:]) + + self._orig_execlp = os.execlp + os.execlp = _execlp_stub + + def execlp_restore(self): + if hasattr(self, '_orig_execlp'): + os.execlp = self._orig_execlp From 575dc9d0c1fb4e8a2678eced243ceae2b9677933 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Tue, 18 Feb 2014 21:29:31 +0100 Subject: [PATCH 2/2] Documentation for config command --- docs/reference/cli.rst | 24 ++++++++++++++++++++++++ docs/reference/config.rst | 16 ++++++---------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 9ab18f26e..eb4115e54 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -304,6 +304,30 @@ fields Show the item and album metadata fields available for use in :doc:`query` and :doc:`pathformat`. Includes any template fields provided by plugins. +.. _config-cmd: + +config +`````` +:: + + beet config [-pd] + beet config -e + +Show or edit the user configuration. Without any options this command +prints a YAML representation of the current user configuration. If the +``--path`` option is given it instead prints the aboslute path to the +user configuration file. Note that this path may not exist. The +``--default`` option can be set with or without the ``--path`` option. +If it is set it also load the default configuration from the beets +package. Showing the configuration or the paths also works if an +additional ``--config`` option is given on the command line. + +If the ``--edit`` option is given, beets will open the user configuration +in an editor. If the ``EDITOR`` environment variable is set it uses that +command to start the editor. Otherwise, beets tries the ``open`` command on +OSX, the ``xdg-open`` command on Unixes and will try to execute the +configuration file directly on Windows. + .. _global-flags: Global Flags diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 040db565a..c470eac89 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -3,16 +3,12 @@ Configuration 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, 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, 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`_. +``config.yaml``. The ``beets config -p`` shows you the location where +beets expects its configuration file to be placed. You can start editing +it right away by running ``beets config -e``. This will open the +configuration file in your default text editor. The section +`Configuration Location`_ explains in depth the possible paths for +configuration files and how to change them. 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