diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 3b6e5511..82e4569b 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -40,9 +40,6 @@ def main(): signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) try: - create_file_structures() - check_old_locations() - root_cmd = commands.RootCommand() config_cmd = commands.ConfigCommand() deps_cmd = commands.DepsCommand() @@ -61,6 +58,9 @@ def main(): args = root_cmd.parse(mopidy_args) + create_file_structures_and_config(args, installed_extensions) + check_old_locations() + config, config_errors = config_lib.load( args.config_files, installed_extensions, args.config_overrides) @@ -123,9 +123,22 @@ def main(): raise -def create_file_structures(): +def create_file_structures_and_config(args, extensions): path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy') - path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf') + path.get_or_create_dir(b'$XDG_CONFIG_DIR/mopidy') + + # Initialize whatever the last config file is with defaults + config_file = args.config_files[-1] + if os.path.exists(config_file): + return + + try: + default = config_lib.format_initial(extensions) + path.get_or_create_file(config_file, mkdir=False, content=default) + logger.info('Initialized %s with default config', config_file) + except IOError as e: + logger.warning('Unable to initialize %s with default config: %s', + config_file, e) def check_old_locations(): diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 6d66e253..be2205ca 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -10,7 +10,7 @@ import re from mopidy.config import keyring from mopidy.config.schemas import * # noqa from mopidy.config.types import * # noqa -from mopidy.utils import path +from mopidy.utils import path, versioning logger = logging.getLogger('mopidy.config') @@ -41,6 +41,18 @@ _proxy_schema['password'] = Secret(optional=True) _schemas = [_logging_schema, _loglevels_schema, _audio_schema, _proxy_schema] +_INITIAL_HELP = """ +# For further information about options in this file see: +# http://docs.mopidy.com/ +# +# The initial commented out values reflect the defaults as of: +# %(versions)s +# +# Available options and defaults might have changed since then, +# run `mopidy config` to see the current effective config and +# `mopidy --version` to check the current version. +""" + def read(config_file): """Helper to load config defaults in same way across core and extensions""" @@ -66,7 +78,25 @@ def format(config, extensions, comments=None, display=True): # need to know about extensions. schemas = _schemas[:] schemas.extend(e.get_config_schema() for e in extensions) - return _format(config, comments or {}, schemas, display) + return _format(config, comments or {}, schemas, display, False) + + +def format_initial(extensions): + config_dir = os.path.dirname(__file__) + defaults = [read(os.path.join(config_dir, 'default.conf'))] + defaults.extend(e.get_default_config() for e in extensions) + raw_config = _load([], defaults, []) + + schemas = _schemas[:] + schemas.extend(e.get_config_schema() for e in extensions) + + config, errors = _validate(raw_config, schemas) + + versions = ['Mopidy %s' % versioning.get_version()] + for extension in sorted(extensions, key=lambda ext: ext.dist_name): + versions.append('%s %s' % (extension.dist_name, extension.version)) + description = _INITIAL_HELP.strip() % {'versions': '\n# '.join(versions)} + return description + '\n\n' + _format(config, {}, schemas, False, True) def _load(files, defaults, overrides): @@ -128,7 +158,7 @@ def _validate(raw_config, schemas): return config, errors -def _format(config, comments, schemas, display): +def _format(config, comments, schemas, display, disable): output = [] for schema in schemas: serialized = schema.serialize( @@ -142,9 +172,11 @@ def _format(config, comments, schemas, display): if value is not None: output[-1] += b' ' + value if comment: - output[-1] += b' # ' + comment.capitalize() + output[-1] += b' ; ' + comment.capitalize() + if disable: + output[-1] = re.sub(r'^', b'#', output[-1], flags=re.M) output.append(b'') - return b'\n'.join(output) + return b'\n'.join(output).strip() def _preprocess(config_string): diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index c5aa6e45..32dcb721 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -37,14 +37,17 @@ def get_or_create_dir(dir_path): return dir_path -def get_or_create_file(file_path): +def get_or_create_file(file_path, mkdir=True, content=None): if not isinstance(file_path, bytes): raise ValueError('Path is not a bytestring.') file_path = expand_path(file_path) - get_or_create_dir(os.path.dirname(file_path)) + if mkdir: + get_or_create_dir(os.path.dirname(file_path)) if not os.path.isfile(file_path): logger.info('Creating file %s', file_path) - open(file_path, 'w').close() + with open(file_path, 'w') as fh: + if content: + fh.write(content) return file_path diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index ed9f8044..673fda73 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -102,17 +102,29 @@ class GetOrCreateFileTest(unittest.TestCase): def test_create_file_with_name_of_existing_dir_throws_ioerror(self): conflicting_dir = os.path.join(self.parent) - self.assertRaises(IOError, path.get_or_create_file, conflicting_dir) + with self.assertRaises(IOError): + path.get_or_create_file(conflicting_dir) def test_create_dir_with_unicode(self): with self.assertRaises(ValueError): file_path = unicode(os.path.join(self.parent, b'test')) path.get_or_create_file(file_path) - def test_create_dir_with_none(self): + def test_create_file_with_none(self): with self.assertRaises(ValueError): path.get_or_create_file(None) + def test_create_dir_without_mkdir(self): + file_path = os.path.join(self.parent, b'foo', b'bar') + with self.assertRaises(IOError): + path.get_or_create_file(file_path, mkdir=False) + + def test_create_dir_with_default_content(self): + file_path = os.path.join(self.parent, b'test') + created = path.get_or_create_file(file_path, content=b'foobar') + with open(created) as fh: + self.assertEqual(fh.read(), b'foobar') + class PathToFileURITest(unittest.TestCase): def test_simple_path(self):