diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 71ea9ec0..b64ac3b6 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -99,7 +99,7 @@ installation using ``pip install Mopidy-Something==dev`` to work. Before starting Mopidy, you must add your Soundspot username and password to the Mopidy configuration file:: - [ext.soundspot] + [soundspot] username = alice password = secret @@ -199,13 +199,13 @@ The default configuration for the extension is defined by the ``get_default_config()`` method in the ``Extension`` class which returns a :mod:`ConfigParser` compatible config section. The config section's name should be the same as the extension's short name, as defined in the ``entry_points`` -part of ``setup.py``, but prefixed with ``ext.``, for example -``ext.soundspot``. All extensions should include an ``enabled`` config which -should default to ``true``. Provide good defaults for all config values so that -as few users as possible will need to change them. The exception is if the -config value has security implications; in that case you should default to the -most secure configuration. Leave any configurations that doesn't have -meaningful defaults blank, like ``username`` and ``password``. +part of ``setup.py``, for example ``soundspot``. All extensions should include +an ``enabled`` config which should default to ``true``. Provide good defaults +for all config values so that as few users as possible will need to change +them. The exception is if the config value has security implications; in that +case you should default to the most secure configuration. Leave any +configurations that doesn't have meaningful defaults blank, like ``username`` +and ``password``. :: @@ -225,7 +225,7 @@ meaningful defaults blank, like ``username`` and ``password``. __version__ = '0.1' default_config = """ - [ext.soundspot] + [soundspot] enabled = true username = password = diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 2aeff179..1c0a8515 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import codecs -import ConfigParser +import ConfigParser as configparser import logging import optparse import os @@ -41,7 +41,8 @@ from mopidy.audio import Audio from mopidy.config import default_config, config_schemas from mopidy.core import Core from mopidy.utils import ( - deps, log, path, process, settings as settings_utils, versioning) + config as config_utils, deps, log, path, process, + settings as settings_utils, versioning) logger = logging.getLogger('mopidy.main') @@ -53,17 +54,23 @@ def main(): loop = gobject.MainLoop() options = parse_options() + config_files = options.config.split(':') + config_overrides = options.overrides try: - log.setup_logging(options.verbosity_level, options.save_debug_log) - check_old_folders() + # TODO: we need a two stage logging setup as we want logging for + # extension loading and config loading. + log.setup_logging(None, options.verbosity_level, options.save_debug_log) extensions = load_extensions() - load_config(options, extensions) + raw_config = load_config(config_files, config_overrides, extensions) + extensions = filter_enabled_extensions(raw_config, extensions) + config = validate_config(raw_config, extensions) + check_old_folders() setup_settings(options.interactive) - audio = setup_audio() - backends = setup_backends(extensions, audio) + audio = setup_audio(config) + backends = setup_backends(config, extensions, audio) core = setup_core(audio, backends) - setup_frontends(extensions, core) + setup_frontends(config, extensions, core) loop.run() except exceptions.SettingsError as ex: logger.error(ex.message) @@ -80,9 +87,24 @@ def main(): process.stop_remaining_actors() +def check_config_override(option, opt, override): + try: + section, remainder = override.split('/', 1) + key, value = remainder.split('=', 1) + return (section, key, value) + except ValueError: + raise optparse.OptionValueError( + 'option %s: must have the format section/key=value' % opt) + + def parse_options(): parser = optparse.OptionParser( version='Mopidy %s' % versioning.get_version()) + + # Ugly extension of optparse type checking magic :/ + optparse.Option.TYPES += ('config_override',) + optparse.Option.TYPE_CHECKER['config_override'] = check_config_override + # NOTE First argument to add_option must be bytestrings on Python < 2.6.2 # See https://github.com/mopidy/mopidy/issues/302 for details parser.add_option( @@ -106,22 +128,59 @@ def parse_options(): action='store_true', dest='save_debug_log', help='save debug log to "./mopidy.log"') parser.add_option( - b'--list-settings', - action='callback', - callback=settings_utils.list_settings_optparse_callback, - help='list current settings') + b'--show-config', + action='callback', callback=show_config_callback, + help='show current config') parser.add_option( b'--list-deps', action='callback', callback=deps.list_deps_optparse_callback, help='list dependencies and their versions') parser.add_option( - b'--debug-thread', - action='store_true', dest='debug_thread', - help='run background thread that dumps tracebacks on SIGUSR1') + b'--config', + action='store', dest='config', + default='/etc/mopidy/mopidy.conf:$XDG_CONFIG_DIR/mopidy/mopidy.conf', + help='config files to use, colon seperated, later files override') + parser.add_option( + b'-o', b'--option', + action='append', dest='overrides', type='config_override', + help='`section/key=value` values to override config options') return parser.parse_args(args=mopidy_args)[0] +def show_config_callback(option, opt, value, parser): + # TODO: don't use callback for this as --config or -o set after + # --show-config will be ignored. + files = getattr(parser.values, 'config', '').split(':') + overrides = getattr(parser.values, 'overrides', []) + + extensions = load_extensions() + raw_config = load_config(files, overrides, extensions) + enabled_extensions = filter_enabled_extensions(raw_config, extensions) + config = validate_config(raw_config, enabled_extensions) + + output = [] + for section_name, schema in config_schemas.items(): + options = config.get(section_name, {}) + if not options: + continue + output.append(schema.format(section_name, options)) + + for extension in extensions: + if extension in enabled_extensions: + schema = extension.get_config_schema() + options = config.get(extension.ext_name, {}) + output.append(schema.format(extension.ext_name, options)) + else: + lines = ['[%s]' % extension.ext_name, 'enabled = false', + '# Config hidden as extension is disabled'] + output.append('\n'.join(lines)) + + print '\n\n'.join(output) + sys.exit(0) + + def check_old_folders(): + # TODO: add old settings and pre extension storage locations? old_settings_folder = os.path.expanduser('~/.mopidy') if not os.path.isdir(old_settings_folder): @@ -136,9 +195,7 @@ def check_old_folders(): def load_extensions(): extensions = [] for entry_point in pkg_resources.iter_entry_points('mopidy.ext'): - logger.debug('Loading extension %s', entry_point.name) - - # TODO Filter out disabled extensions + logger.debug('Loading entry point: %s', entry_point) try: extension_class = entry_point.load() @@ -150,6 +207,9 @@ def load_extensions(): extension = extension_class() + logger.debug( + 'Loaded extension: %s %s', extension.dist_name, extension.version) + if entry_point.name != extension.ext_name: logger.warning( 'Disabled extension %(ep)s: entry point name (%(ep)s) ' @@ -157,8 +217,6 @@ def load_extensions(): {'ep': entry_point.name, 'ext': extension.ext_name}) continue - # TODO Validate configuration - try: extension.validate_environment() except exceptions.ExtensionError as ex: @@ -166,22 +224,39 @@ def load_extensions(): 'Disabled extension %s: %s', entry_point.name, ex.message) continue - logger.info( - 'Loaded extension %s: %s %s', - entry_point.name, extension.dist_name, extension.version) extensions.append(extension) + + names = (e.ext_name for e in extensions) + logging.debug('Discovered extensions: %s', ', '.join(names)) return extensions -def load_config(options, extensions): - parser = ConfigParser.RawConfigParser() +def filter_enabled_extensions(raw_config, extensions): + boolean = config_utils.Boolean() + enabled_extensions = [] + enabled_names = [] + disabled_names = [] - files = [ - '/etc/mopidy/mopidy.conf', - '~/.config/mopidy/mopidy.conf', - ] - # TODO Add config file given through `options` to `files` - # TODO Replace `files` with single file given through `options` + for extension in extensions: + # TODO: handle key and value errors. + enabled = raw_config[extension.ext_name]['enabled'] + if boolean.deserialize(enabled): + enabled_extensions.append(extension) + enabled_names.append(extension.ext_name) + else: + disabled_names.append(extension.ext_name) + + logging.info('Enabled extensions: %s', ', '.join(enabled_names)) + logging.info('Disabled extensions: %s', ', '.join(disabled_names)) + return enabled_extensions + + +def load_config(files, overrides, extensions): + parser = configparser.RawConfigParser() + + files = [path.expand_path(f) for f in files] + sources = ['builtin-defaults'] + files + ['command-line'] + logging.info('Loading config from: %s', ', '.join(sources)) # Read default core config parser.readfp(StringIO.StringIO(default_config)) @@ -192,7 +267,6 @@ def load_config(options, extensions): # Load config from a series of config files for filename in files: - filename = os.path.expanduser(filename) try: filehandle = codecs.open(filename, encoding='utf-8') parser.readfp(filehandle) @@ -203,28 +277,41 @@ def load_config(options, extensions): logger.error('Config file %s is not UTF-8 encoded', filename) process.exit_process() - # TODO Merge config values given through `options` into `config` + raw_config = {} + for section in parser.sections(): + raw_config[section] = dict(parser.items(section)) + for section, key, value in overrides or []: + raw_config.setdefault(section, {})[key] = value + + return raw_config + + +def validate_config(raw_config, extensions): # Collect config schemas to validate against sections_and_schemas = config_schemas.items() for extension in extensions: - section_name = 'ext.%s' % extension.ext_name - if parser.getboolean(section_name, 'enabled'): - sections_and_schemas.append( - (section_name, extension.get_config_schema())) + sections_and_schemas.append( + (extension.ext_name, extension.get_config_schema())) # Get validated config config = {} + errors = {} for section_name, schema in sections_and_schemas: - if not parser.has_section(section_name): - logger.error('Config section %s not found', section_name) - process.exit_process() + if section_name not in raw_config: + errors[section_name] = {section_name: 'section not found'} try: - config[section_name] = schema.convert(parser.items(section_name)) + items = raw_config[section_name].items() + config[section_name] = schema.convert(items) except exceptions.ConfigError as error: + errors[section_name] = error + + if errors: + for section_name, error in errors.items(): + logger.error('[%s] config errors:', section_name) for key in error: - logger.error('Config error: %s: %s', key, error[key]) - process.exit_process() + logger.error('%s %s', key, error[key]) + sys.exit(1) return config @@ -240,9 +327,9 @@ def setup_settings(interactive): sys.exit(1) -def setup_audio(): +def setup_audio(config): logger.info('Starting Mopidy audio') - return Audio.start().proxy() + return Audio.start(config=config).proxy() def stop_audio(): @@ -250,12 +337,12 @@ def stop_audio(): process.stop_actors_by_class(Audio) -def setup_backends(extensions, audio): +def setup_backends(config, extensions, audio): logger.info('Starting Mopidy backends') backends = [] for extension in extensions: for backend_class in extension.get_backend_classes(): - backend = backend_class.start(audio=audio).proxy() + backend = backend_class.start(config=config, audio=audio).proxy() backends.append(backend) return backends @@ -277,11 +364,11 @@ def stop_core(): process.stop_actors_by_class(Core) -def setup_frontends(extensions, core): +def setup_frontends(config, extensions, core): logger.info('Starting Mopidy frontends') for extension in extensions: for frontend_class in extension.get_frontend_classes(): - frontend_class.start(core=core) + frontend_class.start(config=config, core=core) def stop_frontends(extensions): diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 11d2741f..42dee084 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -38,7 +38,7 @@ class Audio(pykka.ThreadingActor): #: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState` state = PlaybackState.STOPPED - def __init__(self): + def __init__(self, config): super(Audio, self).__init__() self._playbin = None diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py index dd021445..65477ea2 100644 --- a/mopidy/backends/dummy.py +++ b/mopidy/backends/dummy.py @@ -22,8 +22,12 @@ from mopidy.backends import base from mopidy.models import Playlist, SearchResult +def create_dummy_backend_proxy(config=None, audio=None): + return DummyBackend.start(config=config, audio=audio).proxy() + + class DummyBackend(pykka.ThreadingActor, base.Backend): - def __init__(self, audio): + def __init__(self, config, audio): super(DummyBackend, self).__init__() self.library = DummyLibraryProvider(backend=self) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index bd9a6d1a..17fd659e 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -6,7 +6,7 @@ from mopidy.utils import config, formatting default_config = """ -[ext.local] +[local] # If the local extension should be enabled or not enabled = true diff --git a/mopidy/backends/local/actor.py b/mopidy/backends/local/actor.py index 75baeab2..abad75ca 100644 --- a/mopidy/backends/local/actor.py +++ b/mopidy/backends/local/actor.py @@ -13,7 +13,7 @@ logger = logging.getLogger('mopidy.backends.local') class LocalBackend(pykka.ThreadingActor, base.Backend): - def __init__(self, audio): + def __init__(self, config, audio): super(LocalBackend, self).__init__() self.library = LocalLibraryProvider(backend=self) diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index d9fdb630..0e32d4cd 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -7,7 +7,7 @@ from mopidy.utils import config, formatting default_config = """ -[ext.spotify] +[spotify] # If the Spotify extension should be enabled or not enabled = true diff --git a/mopidy/backends/spotify/actor.py b/mopidy/backends/spotify/actor.py index 5e90205b..67b4acdc 100644 --- a/mopidy/backends/spotify/actor.py +++ b/mopidy/backends/spotify/actor.py @@ -14,7 +14,7 @@ class SpotifyBackend(pykka.ThreadingActor, base.Backend): # Imports inside methods are to prevent loading of __init__.py to fail on # missing spotify dependencies. - def __init__(self, audio): + def __init__(self, config, audio): super(SpotifyBackend, self).__init__() from .library import SpotifyLibraryProvider diff --git a/mopidy/backends/stream/__init__.py b/mopidy/backends/stream/__init__.py index d14275b0..9a393bed 100644 --- a/mopidy/backends/stream/__init__.py +++ b/mopidy/backends/stream/__init__.py @@ -6,7 +6,7 @@ from mopidy.utils import config, formatting default_config = """ -[ext.stream] +[stream] # If the stream extension should be enabled or not enabled = true diff --git a/mopidy/backends/stream/actor.py b/mopidy/backends/stream/actor.py index f80ac7a9..d6eb31d3 100644 --- a/mopidy/backends/stream/actor.py +++ b/mopidy/backends/stream/actor.py @@ -13,7 +13,7 @@ logger = logging.getLogger('mopidy.backends.stream') class StreamBackend(pykka.ThreadingActor, base.Backend): - def __init__(self, audio): + def __init__(self, config, audio): super(StreamBackend, self).__init__() self.library = StreamLibraryProvider(backend=self) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index 5e9629a7..03bf0a87 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -6,7 +6,7 @@ from mopidy.utils import config, formatting default_config = """ -[ext.http] +[http] # If the HTTP extension should be enabled or not enabled = true diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 8ad0f026..54085471 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -23,7 +23,7 @@ logger = logging.getLogger('mopidy.frontends.http') class HttpFrontend(pykka.ThreadingActor, CoreListener): - def __init__(self, core): + def __init__(self, config, core): super(HttpFrontend, self).__init__() self.core = core self._setup_server() diff --git a/mopidy/frontends/lastfm/__init__.py b/mopidy/frontends/lastfm/__init__.py index 510856ea..f4bff0e5 100644 --- a/mopidy/frontends/lastfm/__init__.py +++ b/mopidy/frontends/lastfm/__init__.py @@ -6,7 +6,7 @@ from mopidy.utils import config, formatting default_config = """ -[ext.lastfm] +[lastfm] # If the Last.fm extension should be enabled or not enabled = true diff --git a/mopidy/frontends/lastfm/actor.py b/mopidy/frontends/lastfm/actor.py index 60a909e0..1e157d4f 100644 --- a/mopidy/frontends/lastfm/actor.py +++ b/mopidy/frontends/lastfm/actor.py @@ -20,7 +20,7 @@ API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' class LastfmFrontend(pykka.ThreadingActor, CoreListener): - def __init__(self, core): + def __init__(self, config, core): super(LastfmFrontend, self).__init__() self.lastfm = None self.last_start_time = None diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 518da54a..08bafd26 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -6,7 +6,7 @@ from mopidy.utils import config, formatting default_config = """ -[ext.mpd] +[mpd] # If the MPD extension should be enabled or not enabled = true diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 8907fe22..e288c24e 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -14,7 +14,7 @@ logger = logging.getLogger('mopidy.frontends.mpd') class MpdFrontend(pykka.ThreadingActor, CoreListener): - def __init__(self, core): + def __init__(self, config, core): super(MpdFrontend, self).__init__() hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 20ef0ea7..79806c47 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -6,7 +6,7 @@ from mopidy.utils import formatting, config default_config = """ -[ext.mpris] +[mpris] # If the MPRIS extension should be enabled or not enabled = true diff --git a/mopidy/frontends/mpris/actor.py b/mopidy/frontends/mpris/actor.py index 5e171826..11f87922 100644 --- a/mopidy/frontends/mpris/actor.py +++ b/mopidy/frontends/mpris/actor.py @@ -18,7 +18,7 @@ except ImportError as import_error: class MprisFrontend(pykka.ThreadingActor, CoreListener): - def __init__(self, core): + def __init__(self, config, core): super(MprisFrontend, self).__init__() self.core = core self.indicate_server = None diff --git a/mopidy/utils/config.py b/mopidy/utils/config.py index 6358796b..30cf873f 100644 --- a/mopidy/utils/config.py +++ b/mopidy/utils/config.py @@ -154,10 +154,10 @@ class List(ConfigValue): values = re.split(r'\s*\n\s*', value.strip()) else: values = re.split(r'\s*,\s*', value.strip()) - return [v for v in values if v] + return tuple([v for v in values if v]) def serialize(self, value): - return '\n '.join(v.encode('utf-8') for v in value) + return '\n ' + '\n '.join(v.encode('utf-8') for v in value) class LogLevel(ConfigValue): @@ -213,9 +213,9 @@ class ConfigSchema(object): """Logical group of config values that correspond to a config section. Schemas are set up by assigning config keys with config values to - instances. Once setup :meth:`convert` can be called with a list of `(key, - value)` tuples to process. For convienience we also support :meth:`format` - method that can used for printing out the converted values. + instances. Once setup :meth:`convert` can be called with a list of + ``(key, value)`` tuples to process. For convienience we also support + :meth:`format` method that can used for printing out the converted values. """ # TODO: Use collections.OrderedDict once 2.6 support is gone (#344) def __init__(self): @@ -263,17 +263,12 @@ class ConfigSchema(object): class ExtensionConfigSchema(ConfigSchema): """Sub-classed :class:`ConfigSchema` for use in extensions. - Ensures that `enabled` config value is present and that section name is - prefixed with ext. + Ensures that ``enabled`` config value is present. """ def __init__(self): super(ExtensionConfigSchema, self).__init__() self['enabled'] = Boolean() - def format(self, name, values): - return super(ExtensionConfigSchema, self).format( - 'ext.%s' % name, values) - class LogLevelConfigSchema(object): """Special cased schema for handling a config section with loglevels. diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index ae4ea0d9..d50f107f 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -7,7 +7,7 @@ from mopidy import settings from . import deps, versioning -def setup_logging(verbosity_level, save_debug_log): +def setup_logging(config, verbosity_level, save_debug_log): setup_root_logger() setup_console_logging(verbosity_level) if save_debug_log: diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 7d988a90..4e5a66cd 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -99,6 +99,7 @@ def split_path(path): def expand_path(path): + # TODO: expandvars as well? path = string.Template(path).safe_substitute(XDG_DIRS) path = os.path.expanduser(path) path = os.path.abspath(path) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 051f0f1c..b61476f5 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -184,42 +184,6 @@ def validate_settings(defaults, settings): return errors -def list_settings_optparse_callback(*args): - """ - Prints a list of all settings. - - Called by optparse when Mopidy is run with the :option:`--list-settings` - option. - """ - from mopidy import settings - print format_settings_list(settings) - sys.exit(0) - - -def format_settings_list(settings): - errors = settings.get_errors() - lines = [] - for (key, value) in sorted(settings.current.iteritems()): - default_value = settings.default.get(key) - masked_value = mask_value_if_secret(key, value) - lines.append('%s: %s' % ( - key, formatting.indent(pprint.pformat(masked_value), places=2))) - if value != default_value and default_value is not None: - lines.append( - ' Default: %s' % - formatting.indent(pprint.pformat(default_value), places=4)) - if errors.get(key) is not None: - lines.append(' Error: %s' % errors[key]) - return '\n'.join(lines) - - -def mask_value_if_secret(key, value): - if key.endswith('PASSWORD') and value: - return '********' - else: - return value - - def did_you_mean(setting, defaults): """Suggest most likely setting based on levenshtein.""" if not defaults: diff --git a/tests/audio/actor_test.py b/tests/audio/actor_test.py index 73c8c165..51786adb 100644 --- a/tests/audio/actor_test.py +++ b/tests/audio/actor_test.py @@ -17,7 +17,7 @@ class AudioTest(unittest.TestCase): settings.MIXER = 'fakemixer track_max_volume=65536' settings.OUTPUT = 'fakesink' self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) - self.audio = audio.Audio.start().proxy() + self.audio = audio.Audio.start(config=None).proxy() def tearDown(self): pykka.ActorRegistry.stop_all() @@ -60,7 +60,7 @@ class AudioTest(unittest.TestCase): def test_set_volume_with_mixer_max_below_100(self): settings.MIXER = 'fakemixer track_max_volume=40' - self.audio = audio.Audio.start().proxy() + self.audio = audio.Audio.start(config=None).proxy() for value in range(0, 101): self.assertTrue(self.audio.set_volume(value).get()) @@ -81,7 +81,7 @@ class AudioTest(unittest.TestCase): class AudioStateTest(unittest.TestCase): def setUp(self): - self.audio = audio.Audio() + self.audio = audio.Audio(config=None) def test_state_starts_as_stopped(self): self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) diff --git a/tests/backends/base/events.py b/tests/backends/base/events.py index 1d31a721..a5d9fa7b 100644 --- a/tests/backends/base/events.py +++ b/tests/backends/base/events.py @@ -9,9 +9,12 @@ from mopidy.backends import listener @mock.patch.object(listener.BackendListener, 'send') class BackendEventsTest(object): + config = {} + def setUp(self): self.audio = audio.DummyAudio.start().proxy() - self.backend = self.backend_class.start(audio=self.audio).proxy() + self.backend = self.backend_class.start( + config=self.config, audio=self.audio).proxy() self.core = core.Core.start(backends=[self.backend]).proxy() def tearDown(self): diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py index c75bec74..8390d2d6 100644 --- a/tests/backends/base/library.py +++ b/tests/backends/base/library.py @@ -23,9 +23,11 @@ class LibraryControllerTest(object): uri='file://' + path_to_data_dir('uri2'), name='track2', artists=artists[1:2], album=albums[1], date='2002', length=4000), Track()] + config = {} def setUp(self): - self.backend = self.backend_class.start(audio=None).proxy() + self.backend = self.backend_class.start( + config=self.config, audio=None).proxy() self.core = core.Core(backends=[self.backend]) self.library = self.core.library diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index e12d54a5..9ce73d31 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -18,10 +18,12 @@ from tests.backends.base import populate_tracklist class PlaybackControllerTest(object): tracks = [] + config = {} def setUp(self): self.audio = audio.DummyAudio.start().proxy() - self.backend = self.backend_class.start(audio=self.audio).proxy() + self.backend = self.backend_class.start( + config=self.config, audio=self.audio).proxy() self.core = core.Core(backends=[self.backend]) self.playback = self.core.playback self.tracklist = self.core.tracklist diff --git a/tests/backends/base/playlists.py b/tests/backends/base/playlists.py index 2184168f..00e32a6f 100644 --- a/tests/backends/base/playlists.py +++ b/tests/backends/base/playlists.py @@ -13,13 +13,16 @@ from tests import unittest, path_to_data_dir class PlaylistsControllerTest(object): + config = {} + def setUp(self): settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp() settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache') settings.LOCAL_MUSIC_PATH = path_to_data_dir('') self.audio = audio.DummyAudio.start().proxy() - self.backend = self.backend_class.start(audio=self.audio).proxy() + self.backend = self.backend_class.start( + config=self.config, audio=self.audio).proxy() self.core = core.Core(backends=[self.backend]) def tearDown(self): diff --git a/tests/backends/base/tracklist.py b/tests/backends/base/tracklist.py index 39fb020d..5140d3aa 100644 --- a/tests/backends/base/tracklist.py +++ b/tests/backends/base/tracklist.py @@ -13,10 +13,12 @@ from tests.backends.base import populate_tracklist class TracklistControllerTest(object): tracks = [] + config = {} def setUp(self): self.audio = audio.DummyAudio.start().proxy() - self.backend = self.backend_class.start(audio=self.audio).proxy() + self.backend = self.backend_class.start( + config=self.config, audio=self.audio).proxy() self.core = core.Core(audio=self.audio, backends=[self.backend]) self.controller = self.core.tracklist self.playback = self.core.playback diff --git a/tests/backends/local/events_test.py b/tests/backends/local/events_test.py index b35fad1a..5ccf0886 100644 --- a/tests/backends/local/events_test.py +++ b/tests/backends/local/events_test.py @@ -7,6 +7,7 @@ from tests.backends.base import events class LocalBackendEventsTest(events.BackendEventsTest, unittest.TestCase): backend_class = actor.LocalBackend + # TODO: setup config def setUp(self): settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache') diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 7bf8d565..ca90e40b 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -8,8 +8,8 @@ from tests.backends.base.library import LibraryControllerTest class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase): - backend_class = actor.LocalBackend + # TODO: setup config def setUp(self): settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache') diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 8d997d2e..e9b3954c 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -15,6 +15,7 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): backend_class = actor.LocalBackend tracks = [ Track(uri=generate_song(i), length=4464) for i in range(1, 4)] + # TODO: setup config def setUp(self): settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache') diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py index f3794cee..3dbc3a2a 100644 --- a/tests/backends/local/playlists_test.py +++ b/tests/backends/local/playlists_test.py @@ -17,6 +17,7 @@ class LocalPlaylistsControllerTest( PlaylistsControllerTest, unittest.TestCase): backend_class = actor.LocalBackend + # TODO: setup config def setUp(self): settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache') @@ -96,7 +97,7 @@ class LocalPlaylistsControllerTest( playlist = playlist.copy(tracks=[track]) playlist = self.core.playlists.save(playlist) - backend = self.backend_class(audio=self.audio) + backend = self.backend_class(config=self.config, audio=self.audio) self.assert_(backend.playlists.playlists) self.assertEqual( diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py index 0c47a5db..24c400fa 100644 --- a/tests/backends/local/tracklist_test.py +++ b/tests/backends/local/tracklist_test.py @@ -13,6 +13,7 @@ class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase): backend_class = actor.LocalBackend tracks = [ Track(uri=generate_song(i), length=4464) for i in range(1, 4)] + # TODO: setup config def setUp(self): settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache') diff --git a/tests/core/events_test.py b/tests/core/events_test.py index 11881db7..7f673b02 100644 --- a/tests/core/events_test.py +++ b/tests/core/events_test.py @@ -13,7 +13,7 @@ from tests import unittest @mock.patch.object(core.CoreListener, 'send') class BackendEventsTest(unittest.TestCase): def setUp(self): - self.backend = dummy.DummyBackend.start(audio=None).proxy() + self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() def tearDown(self): diff --git a/tests/frontends/http/events_test.py b/tests/frontends/http/events_test.py index 77438fd4..7661ac6e 100644 --- a/tests/frontends/http/events_test.py +++ b/tests/frontends/http/events_test.py @@ -24,7 +24,7 @@ from tests import unittest @mock.patch('cherrypy.engine.publish') class HttpEventsTest(unittest.TestCase): def setUp(self): - self.http = actor.HttpFrontend(core=mock.Mock()) + self.http = actor.HttpFrontend(config=None, core=mock.Mock()) def test_track_playback_paused_is_broadcasted(self, publish): publish.reset_mock() diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 3404db95..3c32cd32 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -13,7 +13,7 @@ from tests import unittest class MpdDispatcherTest(unittest.TestCase): def setUp(self): - self.backend = dummy.DummyBackend.start(audio=None).proxy() + self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.dispatcher = MpdDispatcher() diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 00594206..9d24c3fa 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -24,7 +24,7 @@ class MockConnection(mock.Mock): class BaseTestCase(unittest.TestCase): def setUp(self): - self.backend = dummy.DummyBackend.start(audio=None).proxy() + self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.connection = MockConnection() diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index d508cbf0..8868eef7 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -22,7 +22,7 @@ STOPPED = PlaybackState.STOPPED class StatusHandlerTest(unittest.TestCase): def setUp(self): - self.backend = dummy.DummyBackend.start(audio=None).proxy() + self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.dispatcher = dispatcher.MpdDispatcher(core=self.core) self.context = self.dispatcher.context diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index 78e40071..f79202c0 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -19,7 +19,7 @@ from tests import unittest class BackendEventsTest(unittest.TestCase): def setUp(self): # As a plain class, not an actor: - self.mpris_frontend = actor.MprisFrontend(core=None) + self.mpris_frontend = actor.MprisFrontend(config=None, core=None) self.mpris_object = mock.Mock(spec=objects.MprisObject) self.mpris_frontend.mpris_object = self.mpris_object diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 0c477dc8..ec4a17a9 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -26,7 +26,7 @@ STOPPED = PlaybackState.STOPPED class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() - self.backend = dummy.DummyBackend.start(audio=None).proxy() + self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.mpris = objects.MprisObject(core=self.core) diff --git a/tests/frontends/mpris/playlists_interface_test.py b/tests/frontends/mpris/playlists_interface_test.py index 2adffaf3..745a858c 100644 --- a/tests/frontends/mpris/playlists_interface_test.py +++ b/tests/frontends/mpris/playlists_interface_test.py @@ -23,7 +23,7 @@ from tests import unittest class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() - self.backend = dummy.DummyBackend.start(audio=None).proxy() + self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.mpris = objects.MprisObject(core=self.core) diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 722fd2cd..36d689a2 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -21,7 +21,7 @@ class RootInterfaceTest(unittest.TestCase): def setUp(self): objects.exit_process = mock.Mock() objects.MprisObject._connect_to_dbus = mock.Mock() - self.backend = dummy.DummyBackend.start(audio=None).proxy() + self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.mpris = objects.MprisObject(core=self.core) diff --git a/tests/help_test.py b/tests/help_test.py index fdef0f52..15c51d2a 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -22,7 +22,9 @@ class HelpTest(unittest.TestCase): self.assertIn('--quiet', output) self.assertIn('--verbose', output) self.assertIn('--save-debug-log', output) - self.assertIn('--list-settings', output) + self.assertIn('--show-config', output) + self.assertIn('--config', output) + self.assertIn('--option', output) def test_help_gst_has_gstreamer_options(self): mopidy_dir = os.path.dirname(mopidy.__file__) diff --git a/tests/utils/config_test.py b/tests/utils/config_test.py index 43bb1679..f2465b4e 100644 --- a/tests/utils/config_test.py +++ b/tests/utils/config_test.py @@ -199,10 +199,10 @@ class ListTest(unittest.TestCase): def test_deserialize_conversion_success(self): value = config.List() - expected = ['foo', 'bar', 'baz'] + expected = ('foo', 'bar', 'baz') self.assertEqual(expected, value.deserialize('foo, bar ,baz ')) - expected = ['foo,bar', 'bar', 'baz'] + expected = ('foo,bar', 'bar', 'baz') self.assertEqual(expected, value.deserialize(' foo,bar\nbar\nbaz')) def test_deserialize_enforces_required(self): @@ -212,12 +212,12 @@ class ListTest(unittest.TestCase): def test_deserialize_respects_optional(self): value = config.List(optional=True) - self.assertEqual([], value.deserialize('')) - self.assertEqual([], value.deserialize(' ')) + self.assertEqual(tuple(), value.deserialize('')) + self.assertEqual(tuple(), value.deserialize(' ')) def test_serialize(self): value = config.List() - result = value.serialize(['foo', 'bar', 'baz']) + result = value.serialize(('foo', 'bar', 'baz')) self.assertRegexpMatches(result, r'foo\n\s*bar\n\s*baz') @@ -376,10 +376,6 @@ class ExtensionConfigSchemaTest(unittest.TestCase): schema = config.ExtensionConfigSchema() self.assertIsInstance(schema['enabled'], config.Boolean) - def test_section_name_is_prefixed(self): - schema = config.ExtensionConfigSchema() - self.assertEqual('[ext.foo]', schema.format('foo', {})) - class LogLevelConfigSchemaTest(unittest.TestCase): def test_conversion(self): diff --git a/tests/utils/jsonrpc_test.py b/tests/utils/jsonrpc_test.py index 226d4614..7fb8a55e 100644 --- a/tests/utils/jsonrpc_test.py +++ b/tests/utils/jsonrpc_test.py @@ -38,7 +38,7 @@ class Calculator(object): class JsonRpcTestBase(unittest.TestCase): def setUp(self): - self.backend = dummy.DummyBackend.start(audio=None).proxy() + self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.jrw = jsonrpc.JsonRpcWrapper( diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 2c13066c..787337d2 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -59,20 +59,6 @@ class ValidateSettingsTest(unittest.TestCase): self.defaults, {'FOO': '', 'BAR': ''}) self.assertEqual(len(result), 2) - def test_masks_value_if_secret(self): - secret = setting_utils.mask_value_if_secret('SPOTIFY_PASSWORD', 'bar') - self.assertEqual('********', secret) - - def test_does_not_mask_value_if_not_secret(self): - not_secret = setting_utils.mask_value_if_secret( - 'SPOTIFY_USERNAME', 'foo') - self.assertEqual('foo', not_secret) - - def test_does_not_mask_value_if_none(self): - not_secret = setting_utils.mask_value_if_secret( - 'SPOTIFY_USERNAME', None) - self.assertEqual(None, not_secret) - class SettingsProxyTest(unittest.TestCase): def setUp(self): @@ -179,54 +165,6 @@ class SettingsProxyTest(unittest.TestCase): self.settings.validate(interactive=True) -class FormatSettingListTest(unittest.TestCase): - def setUp(self): - self.settings = setting_utils.SettingsProxy(settings) - - def test_contains_the_setting_name(self): - self.settings.TEST = 'test' - result = setting_utils.format_settings_list(self.settings) - self.assertIn('TEST:', result, result) - - def test_repr_of_a_string_value(self): - self.settings.TEST = 'test' - result = setting_utils.format_settings_list(self.settings) - self.assertIn("TEST: u'test'", result, result) - - def test_repr_of_an_int_value(self): - self.settings.TEST = 123 - result = setting_utils.format_settings_list(self.settings) - self.assertIn("TEST: 123", result, result) - - def test_repr_of_a_tuple_value(self): - self.settings.TEST = (123, 'abc') - result = setting_utils.format_settings_list(self.settings) - self.assertIn("TEST: (123, u'abc')", result, result) - - def test_passwords_are_masked(self): - self.settings.TEST_PASSWORD = 'secret' - result = setting_utils.format_settings_list(self.settings) - self.assertNotIn("TEST_PASSWORD: u'secret'", result, result) - self.assertIn("TEST_PASSWORD: u'********'", result, result) - - def test_short_values_are_not_pretty_printed(self): - self.settings.FRONTEND = ('mopidy.frontends.mpd.MpdFrontend',) - result = setting_utils.format_settings_list(self.settings) - self.assertIn( - "FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)", result) - - def test_long_values_are_pretty_printed(self): - self.settings.FRONTEND = ( - u'mopidy.frontends.mpd.MpdFrontend', - u'mopidy.frontends.lastfm.LastfmFrontend') - result = setting_utils.format_settings_list(self.settings) - self.assertIn( - "FRONTEND: \n" - " (u'mopidy.frontends.mpd.MpdFrontend',\n" - " u'mopidy.frontends.lastfm.LastfmFrontend')", - result) - - class DidYouMeanTest(unittest.TestCase): def testSuggestoins(self): defaults = {