Repository: beetbox/beets

Issue:
Let `beet config -e` work even when the configuration file can't be parsed
Under some circumstances, beets and/or the `config` command fail, which can be fixed by changing the config file - thus, the user has to find & edit that by hand, since the command fails. Examples:
1. Upon upgrade to v1.3.9, the discogs plugin wanted an auth token from me, which it requested on beets startup. Since I didn't supply one, beets failed, until I removed the plugins activation from the configuration
2. Make a syntax error in the config file, e.g. a yaml "collection" without any value in it, and watch all beets commands fail since it cannot read the file. This works even when editing "through" beets.



Discussion:
Good point, although neither of these will be easy to solve.

Can you elaborate on how beets "failed" when discogs was broken? Was there a traceback?

```
$ beet version
To authenticate with Discogs, visit:
http://www.discogs.com/oauth/authorize?oauth_token=dLTKUZxKWdAbABnORoErTjorzZwEZpijPbzjLZXp
Enter the code: 
error: Discogs authorization failed
$ echo $?                                                                                                                                :(
1
```

Dunno if this counts as broken, I just didn't supply a code (since the link asks to log in with an account, which I don't have)

Aha, thanks for clarifying! I'm pushing a fix now that makes the plugin only ask for authentication when running an import, so that should solve that half.

The other component here will be somewhat trickier. We need to parse the configuration file before dispatching commands (for plugins' sake), so parse errors can get in the way. I think the best way to make `config -e` still work, which is a good idea, would be to _hard-code_ that particular invocation. This would happen in `beets.ui._raw_main` after the global options are parsed but before we call `_setup`. I'll leave this open to remind us to do that.


Resolving patch:
diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py
--- a/beets/ui/__init__.py
+++ b/beets/ui/__init__.py
@@ -920,6 +920,13 @@ def _raw_main(args, lib=None):
                       help=optparse.SUPPRESS_HELP)
 
     options, subargs = parser.parse_global_options(args)
+
+    # Bypass _setup so that an invalid configuration does not prevent
+    # the editor from starting.
+    if subargs[0] == 'config' and ('-e' in subargs or '--edit' in subargs):
+        from beets.ui.commands import config_edit
+        return config_edit()
+
     subcommands, plugins, lib = _setup(options, lib)
     parser.add_subcommand(*subcommands)
 
diff --git a/beets/ui/commands.py b/beets/ui/commands.py
--- a/beets/ui/commands.py
+++ b/beets/ui/commands.py
@@ -1485,32 +1485,38 @@ def config_func(lib, opts, args):
 
     # Open in editor.
     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.")
+        config_edit()
 
     # Dump configuration.
     else:
         print(config.dump(full=opts.defaults))
 
 
+def config_edit():
+    """Open a program to edit the user configuration.
+    """
+    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.")
+
+
 config_cmd = ui.Subcommand('config',
                            help='show or edit the user configuration')
 config_cmd.parser.add_option(


Test patch:
diff --git a/test/test_config_command.py b/test/test_config_command.py
--- a/test/test_config_command.py
+++ b/test/test_config_command.py
@@ -10,6 +10,7 @@
 import _common
 from _common import unittest
 from helper import TestHelper, capture_stdout
+from beets.library import Library
 
 
 class ConfigCommandTest(unittest.TestCase, TestHelper):
@@ -105,6 +106,19 @@ def test_config_editor_not_found(self):
         self.assertIn('Could not edit configuration',
                       str(user_error.exception.args[0]))
 
+    def test_edit_invalid_config_file(self):
+        self.lib = Library(':memory:')
+        with open(self.config_path, 'w') as file:
+            file.write('invalid: [')
+        config.clear()
+        config._materialized = False
+
+        os.environ['EDITOR'] = 'myeditor'
+        with patch('os.execlp') as execlp:
+            self.run_command('config', '-e')
+        execlp.assert_called_once_with(
+            'myeditor', 'myeditor', self.config_path)
+
 
 def suite():
     return unittest.TestLoader().loadTestsFromName(__name__)