Merge pull request #385 from adamcik/feature/config-integration
New config integration work
This commit is contained in:
commit
e50a9ed7bf
@ -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 =
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import codecs
|
||||
import ConfigParser as configparser
|
||||
import logging
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
import StringIO
|
||||
import sys
|
||||
|
||||
import gobject
|
||||
@ -35,9 +38,11 @@ sys.path.insert(
|
||||
|
||||
from mopidy import exceptions, settings
|
||||
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')
|
||||
@ -49,16 +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()
|
||||
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)
|
||||
@ -75,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(
|
||||
@ -101,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):
|
||||
@ -131,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()
|
||||
@ -145,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) '
|
||||
@ -152,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:
|
||||
@ -161,13 +224,98 @@ 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 filter_enabled_extensions(raw_config, extensions):
|
||||
boolean = config_utils.Boolean()
|
||||
enabled_extensions = []
|
||||
enabled_names = []
|
||||
disabled_names = []
|
||||
|
||||
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))
|
||||
|
||||
# Read default extension config
|
||||
for extension in extensions:
|
||||
parser.readfp(StringIO.StringIO(extension.get_default_config()))
|
||||
|
||||
# Load config from a series of config files
|
||||
for filename in files:
|
||||
try:
|
||||
filehandle = codecs.open(filename, encoding='utf-8')
|
||||
parser.readfp(filehandle)
|
||||
except IOError:
|
||||
logger.debug('Config file %s not found; skipping', filename)
|
||||
continue
|
||||
except UnicodeDecodeError:
|
||||
logger.error('Config file %s is not UTF-8 encoded', filename)
|
||||
process.exit_process()
|
||||
|
||||
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:
|
||||
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 section_name not in raw_config:
|
||||
errors[section_name] = {section_name: 'section not found'}
|
||||
try:
|
||||
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('%s %s', key, error[key])
|
||||
sys.exit(1)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def setup_settings(interactive):
|
||||
path.get_or_create_folder(path.SETTINGS_PATH)
|
||||
path.get_or_create_folder(path.DATA_PATH)
|
||||
@ -179,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():
|
||||
@ -189,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
|
||||
|
||||
@ -216,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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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__)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user