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