From 775b276169642f8b0e73e6a9ea1057c97d272ede Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:40:18 +0200 Subject: [PATCH 01/15] local: Use new config system --- mopidy/backends/local/actor.py | 2 ++ mopidy/backends/local/library.py | 9 ++++---- mopidy/backends/local/playlists.py | 24 ++++++++++--------- tests/backends/base/playlists.py | 17 ++------------ tests/backends/local/events_test.py | 19 ++++++++------- tests/backends/local/library_test.py | 20 ++++++---------- tests/backends/local/playback_test.py | 17 ++++++-------- tests/backends/local/playlists_test.py | 32 +++++++++++++++++--------- tests/backends/local/tracklist_test.py | 17 ++++++-------- 9 files changed, 73 insertions(+), 84 deletions(-) diff --git a/mopidy/backends/local/actor.py b/mopidy/backends/local/actor.py index abad75ca..a1655dd9 100644 --- a/mopidy/backends/local/actor.py +++ b/mopidy/backends/local/actor.py @@ -16,6 +16,8 @@ class LocalBackend(pykka.ThreadingActor, base.Backend): def __init__(self, config, audio): super(LocalBackend, self).__init__() + self.config = config + self.library = LocalLibraryProvider(backend=self) self.playback = base.BasePlaybackProvider(audio=audio, backend=self) self.playlists = LocalPlaylistsProvider(backend=self) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index f2a1a520..2b1c93f7 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import logging -from mopidy import settings from mopidy.backends import base from mopidy.models import Album, SearchResult @@ -15,15 +14,17 @@ class LocalLibraryProvider(base.BaseLibraryProvider): def __init__(self, *args, **kwargs): super(LocalLibraryProvider, self).__init__(*args, **kwargs) self._uri_mapping = {} + self._music_path = self.backend.config['local']['music_path'] + self._playlist_path = self.backend.config['local']['playlist_path'] + self._tag_cache_file = self.backend.config['local']['tag_cache_file'] self.refresh() def refresh(self, uri=None): - tracks = parse_mpd_tag_cache( - settings.LOCAL_TAG_CACHE_FILE, settings.LOCAL_MUSIC_PATH) + tracks = parse_mpd_tag_cache(self._tag_cache_file, self._music_path) logger.info( 'Loading tracks from %s using %s', - settings.LOCAL_MUSIC_PATH, settings.LOCAL_TAG_CACHE_FILE) + self._music_path, self._tag_cache_file) for track in tracks: self._uri_mapping[track.uri] = track diff --git a/mopidy/backends/local/playlists.py b/mopidy/backends/local/playlists.py index 53f7aaae..063d044d 100644 --- a/mopidy/backends/local/playlists.py +++ b/mopidy/backends/local/playlists.py @@ -5,7 +5,6 @@ import logging import os import shutil -from mopidy import settings from mopidy.backends import base, listener from mopidy.models import Playlist from mopidy.utils import formatting, path @@ -19,7 +18,8 @@ logger = logging.getLogger('mopidy.backends.local') class LocalPlaylistsProvider(base.BasePlaylistsProvider): def __init__(self, *args, **kwargs): super(LocalPlaylistsProvider, self).__init__(*args, **kwargs) - self._path = settings.LOCAL_PLAYLIST_PATH + self._music_path = self.backend.config['local']['music_path'] + self._playlist_path = self.backend.config['local']['playlist_path'] self.refresh() def create(self, name): @@ -42,16 +42,16 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): return playlist def refresh(self): - logger.info('Loading playlists from %s', self._path) + logger.info('Loading playlists from %s', self._playlist_path) playlists = [] - for m3u in glob.glob(os.path.join(self._path, '*.m3u')): + for m3u in glob.glob(os.path.join(self._playlist_path, '*.m3u')): uri = path.path_to_uri(m3u) name = os.path.splitext(os.path.basename(m3u))[0] tracks = [] - for track_uri in parse_m3u(m3u, settings.LOCAL_MUSIC_PATH): + for track_uri in parse_m3u(m3u, self._music_path): try: # TODO We must use core.library.lookup() to support tracks # from other backends @@ -86,13 +86,13 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): def _get_m3u_path(self, name): name = formatting.slugify(name) - file_path = os.path.join(self._path, name + '.m3u') - path.check_file_path_is_inside_base_dir(file_path, self._path) + file_path = os.path.join(self._playlist_path, name + '.m3u') + path.check_file_path_is_inside_base_dir(file_path, self._playlist_path) return file_path def _save_m3u(self, playlist): file_path = path.uri_to_path(playlist.uri) - path.check_file_path_is_inside_base_dir(file_path, self._path) + path.check_file_path_is_inside_base_dir(file_path, self._playlist_path) with open(file_path, 'w') as file_handle: for track in playlist.tracks: if track.uri.startswith('file://'): @@ -103,16 +103,18 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): def _delete_m3u(self, uri): file_path = path.uri_to_path(uri) - path.check_file_path_is_inside_base_dir(file_path, self._path) + path.check_file_path_is_inside_base_dir(file_path, self._playlist_path) if os.path.exists(file_path): os.remove(file_path) def _rename_m3u(self, playlist): src_file_path = path.uri_to_path(playlist.uri) - path.check_file_path_is_inside_base_dir(src_file_path, self._path) + path.check_file_path_is_inside_base_dir( + src_file_path, self._playlist_path) dst_file_path = self._get_m3u_path(playlist.name) - path.check_file_path_is_inside_base_dir(dst_file_path, self._path) + path.check_file_path_is_inside_base_dir( + dst_file_path, self._playlist_path) shutil.move(src_file_path, dst_file_path) diff --git a/tests/backends/base/playlists.py b/tests/backends/base/playlists.py index 00e32a6f..ad5648f9 100644 --- a/tests/backends/base/playlists.py +++ b/tests/backends/base/playlists.py @@ -1,25 +1,17 @@ from __future__ import unicode_literals -import os -import shutil -import tempfile - import pykka -from mopidy import audio, core, settings +from mopidy import audio, core from mopidy.models import Playlist -from tests import unittest, path_to_data_dir +from tests import unittest 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( config=self.config, audio=self.audio).proxy() @@ -28,11 +20,6 @@ class PlaylistsControllerTest(object): def tearDown(self): pykka.ActorRegistry.stop_all() - if os.path.exists(settings.LOCAL_PLAYLIST_PATH): - shutil.rmtree(settings.LOCAL_PLAYLIST_PATH) - - settings.runtime.clear() - def test_create_returns_playlist_with_name_set(self): playlist = self.core.playlists.create('test') self.assertEqual(playlist.name, 'test') diff --git a/tests/backends/local/events_test.py b/tests/backends/local/events_test.py index 5ccf0886..83d77a2f 100644 --- a/tests/backends/local/events_test.py +++ b/tests/backends/local/events_test.py @@ -1,4 +1,5 @@ -from mopidy import settings +from __future__ import unicode_literals + from mopidy.backends.local import actor from tests import unittest, path_to_data_dir @@ -7,12 +8,10 @@ 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') - super(LocalBackendEventsTest, self).setUp() - - def tearDown(self): - super(LocalBackendEventsTest, self).tearDown() - settings.runtime.clear() + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'playlist_path': '', + '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 ca90e40b..e582c788 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings from mopidy.backends.local import actor from tests import unittest, path_to_data_dir @@ -9,15 +8,10 @@ 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') - settings.LOCAL_MUSIC_PATH = path_to_data_dir('') - - super(LocalLibraryControllerTest, self).setUp() - - def tearDown(self): - settings.runtime.clear() - - super(LocalLibraryControllerTest, self).tearDown() + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'playlist_path': '', + '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 e9b3954c..4c304590 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings from mopidy.backends.local import actor from mopidy.core import PlaybackState from mopidy.models import Track @@ -13,17 +12,15 @@ from tests.backends.local import generate_song class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): backend_class = actor.LocalBackend + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'playlist_path': '', + 'tag_cache_file': path_to_data_dir('empty_tag_cache'), + } + } 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') - super(LocalPlaybackControllerTest, self).setUp() - - def tearDown(self): - super(LocalPlaybackControllerTest, self).tearDown() - settings.runtime.clear() def add_track(self, path): uri = path_to_uri(path_to_data_dir(path)) diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py index 3dbc3a2a..8528adf4 100644 --- a/tests/backends/local/playlists_test.py +++ b/tests/backends/local/playlists_test.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals import os +import shutil +import tempfile -from mopidy import settings from mopidy.backends.local import actor from mopidy.models import Track from mopidy.utils.path import path_to_uri @@ -17,25 +18,34 @@ class LocalPlaylistsControllerTest( PlaylistsControllerTest, unittest.TestCase): backend_class = actor.LocalBackend - # TODO: setup config + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'tag_cache_file': path_to_data_dir('library_tag_cache'), + } + } def setUp(self): - settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache') + self.config['local']['playlist_path'] = tempfile.mkdtemp() + self.playlist_path = self.config['local']['playlist_path'] + super(LocalPlaylistsControllerTest, self).setUp() def tearDown(self): super(LocalPlaylistsControllerTest, self).tearDown() - settings.runtime.clear() + + if os.path.exists(self.playlist_path): + shutil.rmtree(self.playlist_path) def test_created_playlist_is_persisted(self): - path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') + path = os.path.join(self.playlist_path, 'test.m3u') self.assertFalse(os.path.exists(path)) self.core.playlists.create('test') self.assertTrue(os.path.exists(path)) def test_create_slugifies_playlist_name(self): - path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u') + path = os.path.join(self.playlist_path, 'test-foo-bar.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('test FOO baR') @@ -43,7 +53,7 @@ class LocalPlaylistsControllerTest( self.assertTrue(os.path.exists(path)) def test_create_slugifies_names_which_tries_to_change_directory(self): - path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u') + path = os.path.join(self.playlist_path, 'test-foo-bar.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('../../test FOO baR') @@ -51,8 +61,8 @@ class LocalPlaylistsControllerTest( self.assertTrue(os.path.exists(path)) def test_saved_playlist_is_persisted(self): - path1 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test1.m3u') - path2 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2-foo-bar.m3u') + path1 = os.path.join(self.playlist_path, 'test1.m3u') + path2 = os.path.join(self.playlist_path, 'test2-foo-bar.m3u') playlist = self.core.playlists.create('test1') @@ -67,7 +77,7 @@ class LocalPlaylistsControllerTest( self.assertTrue(os.path.exists(path2)) def test_deleted_playlist_is_removed(self): - path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') + path = os.path.join(self.playlist_path, 'test.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('test') @@ -90,7 +100,7 @@ class LocalPlaylistsControllerTest( self.assertEqual(track_path, contents.strip()) def test_playlists_are_loaded_at_startup(self): - playlist_path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') + playlist_path = os.path.join(self.playlist_path, 'test.m3u') track = Track(uri=path_to_uri(path_to_data_dir('uri2'))) playlist = self.core.playlists.create('test') diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py index 24c400fa..3fc8a0be 100644 --- a/tests/backends/local/tracklist_test.py +++ b/tests/backends/local/tracklist_test.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings from mopidy.backends.local import actor from mopidy.models import Track @@ -11,14 +10,12 @@ from tests.backends.local import generate_song class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase): backend_class = actor.LocalBackend + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'playlist_path': '', + 'tag_cache_file': path_to_data_dir('empty_tag_cache'), + } + } 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') - super(LocalTracklistControllerTest, self).setUp() - - def tearDown(self): - super(LocalTracklistControllerTest, self).tearDown() - settings.runtime.clear() From 5894e7afe03d92bee416040f2b1586126ee87674 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:41:59 +0200 Subject: [PATCH 02/15] stream: Use new config system --- mopidy/backends/stream/__init__.py | 5 ++--- mopidy/backends/stream/actor.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/stream/__init__.py b/mopidy/backends/stream/__init__.py index 9a393bed..17b85d33 100644 --- a/mopidy/backends/stream/__init__.py +++ b/mopidy/backends/stream/__init__.py @@ -23,9 +23,8 @@ protocols = __doc__ = """A backend for playing music for streaming music. -This backend will handle streaming of URIs in -:attr:`mopidy.settings.STREAM_PROTOCOLS` assuming the right plugins are -installed. +This backend will handle streaming of URIs matching the ``stream/protocols`` +config value, assuming the needed GStreamer plugins are installed. **Issues** diff --git a/mopidy/backends/stream/actor.py b/mopidy/backends/stream/actor.py index d6eb31d3..86df447d 100644 --- a/mopidy/backends/stream/actor.py +++ b/mopidy/backends/stream/actor.py @@ -5,7 +5,7 @@ import urlparse import pykka -from mopidy import audio as audio_lib, settings +from mopidy import audio as audio_lib from mopidy.backends import base from mopidy.models import Track @@ -21,7 +21,7 @@ class StreamBackend(pykka.ThreadingActor, base.Backend): self.playlists = None self.uri_schemes = audio_lib.supported_uri_schemes( - settings.STREAM_PROTOCOLS) + config['stream']['protocols']) # TODO: Should we consider letting lookup know how to expand common playlist From 85c2b9b1ebb7a425e2edefd461b888b4f6f7b1e0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:44:37 +0200 Subject: [PATCH 03/15] lastfm: Use new config system --- mopidy/frontends/lastfm/actor.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/mopidy/frontends/lastfm/actor.py b/mopidy/frontends/lastfm/actor.py index 1e157d4f..0e5b9c6e 100644 --- a/mopidy/frontends/lastfm/actor.py +++ b/mopidy/frontends/lastfm/actor.py @@ -5,7 +5,7 @@ import time import pykka -from mopidy import exceptions, settings +from mopidy import exceptions from mopidy.core import CoreListener try: @@ -22,21 +22,17 @@ API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' class LastfmFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(LastfmFrontend, self).__init__() + self.config = config self.lastfm = None self.last_start_time = None def on_start(self): try: - username = settings.LASTFM_USERNAME - password_hash = pylast.md5(settings.LASTFM_PASSWORD) self.lastfm = pylast.LastFMNetwork( api_key=API_KEY, api_secret=API_SECRET, - username=username, password_hash=password_hash) + username=self.config['lastfm']['username'], + password_hash=pylast.md5(self.config['lastfm']['password'])) logger.info('Connected to Last.fm') - except exceptions.SettingsError as e: - logger.info('Last.fm scrobbler not started') - logger.debug('Last.fm settings error: %s', e) - self.stop() except (pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: logger.error('Error during Last.fm setup: %s', e) From bcc7c3a890499cddb7d687559a0942761b22c2e7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:48:24 +0200 Subject: [PATCH 04/15] mpris: Use new config system --- mopidy/frontends/mpris/__init__.py | 4 ++-- mopidy/frontends/mpris/actor.py | 7 ++++--- mopidy/frontends/mpris/objects.py | 7 ++++--- tests/frontends/mpris/player_interface_test.py | 2 +- .../frontends/mpris/playlists_interface_test.py | 2 +- tests/frontends/mpris/root_interface_test.py | 16 ++++++++-------- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 79806c47..4dde2e20 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -32,8 +32,8 @@ An example of an MPRIS client is the `Ubuntu Sound Menu Ubuntu Sound Menu. The package is named ``python-indicate`` in Ubuntu/Debian. -- An ``.desktop`` file for Mopidy installed at the path set in - :attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install-desktop-file` for +- An ``.desktop`` file for Mopidy installed at the path set in the + ``mpris/desktop_file`` config value. See :ref:`install-desktop-file` for details. **Default config** diff --git a/mopidy/frontends/mpris/actor.py b/mopidy/frontends/mpris/actor.py index 11f87922..92805bd3 100644 --- a/mopidy/frontends/mpris/actor.py +++ b/mopidy/frontends/mpris/actor.py @@ -4,7 +4,6 @@ import logging import pykka -from mopidy import settings from mopidy.core import CoreListener from mopidy.frontends.mpris import objects @@ -20,13 +19,14 @@ except ImportError as import_error: class MprisFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(MprisFrontend, self).__init__() + self.config = config self.core = core self.indicate_server = None self.mpris_object = None def on_start(self): try: - self.mpris_object = objects.MprisObject(self.core) + self.mpris_object = objects.MprisObject(self.config, self.core) self._send_startup_notification() except Exception as e: logger.error('MPRIS frontend setup failed (%s)', e) @@ -53,7 +53,8 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener): logger.debug('Sending startup notification...') self.indicate_server = indicate.Server() self.indicate_server.set_type('music.mopidy') - self.indicate_server.set_desktop_file(settings.DESKTOP_FILE) + self.indicate_server.set_desktop_file( + self.config['mpris']['desktop_file']) self.indicate_server.show() logger.debug('Startup notification sent') diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 04a72676..696e39bd 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -13,7 +13,6 @@ except ImportError as import_error: from mopidy.exceptions import OptionalDependencyError raise OptionalDependencyError(import_error) -from mopidy import settings from mopidy.core import PlaybackState from mopidy.utils.process import exit_process @@ -36,7 +35,8 @@ class MprisObject(dbus.service.Object): properties = None - def __init__(self, core): + def __init__(self, config, core): + self.config = config self.core = core self.properties = { ROOT_IFACE: self._get_root_iface_properties(), @@ -175,7 +175,8 @@ class MprisObject(dbus.service.Object): ### Root interface properties def get_DesktopEntry(self): - return os.path.splitext(os.path.basename(settings.DESKTOP_FILE))[0] + return os.path.splitext(os.path.basename( + self.config['mpris']['desktop_file']))[0] def get_SupportedUriSchemes(self): return dbus.Array(self.core.uri_schemes.get(), signature='s') diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index ec4a17a9..e1e13084 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -28,7 +28,7 @@ class PlayerInterfaceTest(unittest.TestCase): objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() - self.mpris = objects.MprisObject(core=self.core) + self.mpris = objects.MprisObject(config={}, core=self.core) def tearDown(self): pykka.ActorRegistry.stop_all() diff --git a/tests/frontends/mpris/playlists_interface_test.py b/tests/frontends/mpris/playlists_interface_test.py index 745a858c..67f9e9be 100644 --- a/tests/frontends/mpris/playlists_interface_test.py +++ b/tests/frontends/mpris/playlists_interface_test.py @@ -25,7 +25,7 @@ class PlayerInterfaceTest(unittest.TestCase): objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() - self.mpris = objects.MprisObject(core=self.core) + self.mpris = objects.MprisObject(config={}, core=self.core) foo = self.core.playlists.create('foo').get() foo = foo.copy(last_modified=datetime.datetime(2012, 3, 1, 6, 0, 0)) diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 36d689a2..806b2162 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -5,7 +5,7 @@ import sys import mock import pykka -from mopidy import core, exceptions, settings +from mopidy import core, exceptions from mopidy.backends import dummy try: @@ -19,11 +19,17 @@ from tests import unittest @unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') class RootInterfaceTest(unittest.TestCase): def setUp(self): + config = { + 'mpris': { + 'desktop_file': '/tmp/foo.desktop', + } + } + objects.exit_process = mock.Mock() objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() - self.mpris = objects.MprisObject(core=self.core) + self.mpris = objects.MprisObject(config=config, core=self.core) def tearDown(self): pykka.ActorRegistry.stop_all() @@ -66,15 +72,9 @@ class RootInterfaceTest(unittest.TestCase): result = self.mpris.Get(objects.ROOT_IFACE, 'Identity') self.assertEquals(result, 'Mopidy') - def test_desktop_entry_is_mopidy(self): - result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry') - self.assertEquals(result, 'mopidy') - def test_desktop_entry_is_based_on_DESKTOP_FILE_setting(self): - settings.runtime['DESKTOP_FILE'] = '/tmp/foo.desktop' result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry') self.assertEquals(result, 'foo') - settings.runtime.clear() def test_supported_uri_schemes_includes_backend_uri_schemes(self): result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes') From 8a8a78e025cf30bba4460d714668a3148d6e9940 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:52:38 +0200 Subject: [PATCH 05/15] http: Use new config system --- mopidy/frontends/http/__init__.py | 28 ++++++++++++++-------------- mopidy/frontends/http/actor.py | 11 ++++++----- tests/frontends/http/events_test.py | 11 ++++++++++- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index d588a376..1b93fb8a 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -61,19 +61,19 @@ Setup The frontend is enabled by default if all dependencies are available. -When it is enabled it starts a web server at the port specified by -:attr:`mopidy.settings.HTTP_SERVER_PORT`. +When it is enabled it starts a web server at the port specified by the +``http/port`` config value. .. warning:: Security As a simple security measure, the web server is by default only available - from localhost. To make it available from other computers, change - :attr:`mopidy.settings.HTTP_SERVER_HOSTNAME`. Before you do so, note that - the HTTP frontend does not feature any form of user authentication or - authorization. Anyone able to access the web server can use the full core - API of Mopidy. Thus, you probably only want to make the web server - available from your local network or place it behind a web proxy which - takes care or user authentication. You have been warned. + from localhost. To make it available from other computers, change the + ``http/hostname`` config value. Before you do so, note that the HTTP + frontend does not feature any form of user authentication or authorization. + Anyone able to access the web server can use the full core API of Mopidy. + Thus, you probably only want to make the web server available from your + local network or place it behind a web proxy which takes care or user + authentication. You have been warned. Using a web based Mopidy client @@ -81,10 +81,11 @@ Using a web based Mopidy client The web server can also host any static files, for example the HTML, CSS, JavaScript, and images needed for a web based Mopidy client. To host static -files, change :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point to the -root directory of your web client, e.g.:: +files, change the ``http/static_dir`` to point to the root directory of your +web client, e.g.:: - HTTP_SERVER_STATIC_DIR = u'/home/alice/dev/the-client' + [http] + static_dir = /home/alice/dev/the-client If the directory includes a file named ``index.html``, it will be served on the root of Mopidy's web server. @@ -405,8 +406,7 @@ Example to get started with 2. Create an empty directory for your web client. -3. Change the setting :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point - to your new directory. +3. Change the ``http/static_dir`` config value to point to your new directory. 4. Start/restart Mopidy. diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 54085471..149cbc7f 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -6,7 +6,7 @@ import os import pykka -from mopidy import exceptions, models, settings +from mopidy import exceptions, models from mopidy.core import CoreListener try: @@ -25,6 +25,7 @@ logger = logging.getLogger('mopidy.frontends.http') class HttpFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(HttpFrontend, self).__init__() + self.config = config self.core = core self._setup_server() self._setup_websocket_plugin() @@ -35,8 +36,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): cherrypy.config.update({ 'engine.autoreload_on': False, 'server.socket_host': ( - settings.HTTP_SERVER_HOSTNAME.encode('utf-8')), - 'server.socket_port': settings.HTTP_SERVER_PORT, + self.config['http']['hostname'].encode('utf-8')), + 'server.socket_port': self.config['http']['port'], }) def _setup_websocket_plugin(self): @@ -48,8 +49,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): root.mopidy = MopidyResource() root.mopidy.ws = ws.WebSocketResource(self.core) - if settings.HTTP_SERVER_STATIC_DIR: - static_dir = settings.HTTP_SERVER_STATIC_DIR + if self.config['http']['static_dir']: + static_dir = self.config['http']['static_dir'] else: static_dir = os.path.join(os.path.dirname(__file__), 'data') logger.debug('HTTP server will serve "%s" at /', static_dir) diff --git a/tests/frontends/http/events_test.py b/tests/frontends/http/events_test.py index 7661ac6e..c334eefa 100644 --- a/tests/frontends/http/events_test.py +++ b/tests/frontends/http/events_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import json try: @@ -24,7 +26,14 @@ from tests import unittest @mock.patch('cherrypy.engine.publish') class HttpEventsTest(unittest.TestCase): def setUp(self): - self.http = actor.HttpFrontend(config=None, core=mock.Mock()) + config = { + 'http': { + 'hostname': '127.0.0.1', + 'port': 6680, + 'static_dir': None, + } + } + self.http = actor.HttpFrontend(config=config, core=mock.Mock()) def test_track_playback_paused_is_broadcasted(self, publish): publish.reset_mock() From c452f0115a386db2e8aa89d26f0a6b877241ba93 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 16:07:00 +0200 Subject: [PATCH 06/15] mpd: Use new config system --- mopidy/frontends/mpd/actor.py | 15 +++--- mopidy/frontends/mpd/dispatcher.py | 15 ++++-- mopidy/frontends/mpd/protocol/connection.py | 3 +- mopidy/frontends/mpd/session.py | 5 +- mopidy/frontends/mpd/translator.py | 21 ++++---- mopidy/scanner.py | 4 +- tests/frontends/mpd/dispatcher_test.py | 7 ++- tests/frontends/mpd/protocol/__init__.py | 10 +++- .../mpd/protocol/authentication_test.py | 41 ++++++-------- .../frontends/mpd/protocol/connection_test.py | 17 ------ .../frontends/mpd/protocol/reflection_test.py | 53 ++++++++++--------- tests/frontends/mpd/translator_test.py | 46 +++++++--------- 12 files changed, 117 insertions(+), 120 deletions(-) diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index e288c24e..45ed753e 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -5,7 +5,6 @@ import sys import pykka -from mopidy import settings from mopidy.core import CoreListener from mopidy.frontends.mpd import session from mopidy.utils import encoding, network, process @@ -16,17 +15,21 @@ logger = logging.getLogger('mopidy.frontends.mpd') class MpdFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(MpdFrontend, self).__init__() - hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) - port = settings.MPD_SERVER_PORT + hostname = network.format_hostname(config['mpd']['hostname']) + port = config['mpd']['port'] # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 # See https://github.com/mopidy/mopidy/issues/302 for details. try: network.Server( hostname, port, - protocol=session.MpdSession, protocol_kwargs={b'core': core}, - max_connections=settings.MPD_SERVER_MAX_CONNECTIONS, - timeout=settings.MPD_SERVER_CONNECTION_TIMEOUT) + protocol=session.MpdSession, + protocol_kwargs={ + b'config': config, + b'core': core, + }, + max_connections=config['mpd']['max_connections'], + timeout=config['mpd']['connection_timeout']) except IOError as error: logger.error( 'MPD server startup failed: %s', diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 4f0001ac..dc665abc 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -5,7 +5,6 @@ import re import pykka -from mopidy import settings from mopidy.frontends.mpd import exceptions, protocol logger = logging.getLogger('mopidy.frontends.mpd.dispatcher') @@ -22,13 +21,15 @@ class MpdDispatcher(object): _noidle = re.compile(r'^noidle$') - def __init__(self, session=None, core=None): + def __init__(self, session=None, config=None, core=None): + self.config = config self.authenticated = False self.command_list_receiving = False self.command_list_ok = False self.command_list = [] self.command_list_index = None - self.context = MpdContext(self, session=session, core=core) + self.context = MpdContext( + self, session=session, config=config, core=core) def handle_request(self, request, current_command_list_index=None): """Dispatch incoming requests to the correct handler.""" @@ -82,7 +83,7 @@ class MpdDispatcher(object): def _authenticate_filter(self, request, response, filter_chain): if self.authenticated: return self._call_next_filter(request, response, filter_chain) - elif settings.MPD_SERVER_PASSWORD is None: + elif self.config['mpd']['password'] is None: self.authenticated = True return self._call_next_filter(request, response, filter_chain) else: @@ -223,6 +224,9 @@ class MpdContext(object): #: The current :class:`mopidy.frontends.mpd.MpdSession`. session = None + #: The Mopidy configuration. + config = None + #: The Mopidy core API. An instance of :class:`mopidy.core.Core`. core = None @@ -232,9 +236,10 @@ class MpdContext(object): #: The subsytems that we want to be notified about in idle mode. subscriptions = None - def __init__(self, dispatcher, session=None, core=None): + def __init__(self, dispatcher, session=None, config=None, core=None): self.dispatcher = dispatcher self.session = session + self.config = config self.core = core self.events = set() self.subscriptions = set() diff --git a/mopidy/frontends/mpd/protocol/connection.py b/mopidy/frontends/mpd/protocol/connection.py index f7898d21..44696705 100644 --- a/mopidy/frontends/mpd/protocol/connection.py +++ b/mopidy/frontends/mpd/protocol/connection.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import ( MpdPasswordError, MpdPermissionError) @@ -40,7 +39,7 @@ def password_(context, password): This is used for authentication with the server. ``PASSWORD`` is simply the plaintext password. """ - if password == settings.MPD_SERVER_PASSWORD: + if password == context.config['mpd']['password']: context.dispatcher.authenticated = True else: raise MpdPasswordError('incorrect password', command='password') diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index 8a5deecd..14173308 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -18,9 +18,10 @@ class MpdSession(network.LineProtocol): encoding = protocol.ENCODING delimiter = r'\r?\n' - def __init__(self, connection, core=None): + def __init__(self, connection, config=None, core=None): super(MpdSession, self).__init__(connection) - self.dispatcher = dispatcher.MpdDispatcher(session=self, core=core) + self.dispatcher = dispatcher.MpdDispatcher( + session=self, config=config, core=core) def on_start(self): logger.info('New MPD connection from [%s]:%s', self.host, self.port) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 15ca181d..d820b0e0 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -5,7 +5,6 @@ import re import shlex import urllib -from mopidy import settings from mopidy.frontends.mpd import protocol from mopidy.frontends.mpd.exceptions import MpdArgError from mopidy.models import TlTrack @@ -216,12 +215,14 @@ def query_from_mpd_search_format(mpd_query): return query -def tracks_to_tag_cache_format(tracks): +def tracks_to_tag_cache_format(tracks, music_path): """ Format list of tracks for output to MPD tag cache :param tracks: the tracks :type tracks: list of :class:`mopidy.models.Track` + :param music_path: the path to the music dir + :type music_path: string :rtype: list of lists of two-tuples """ result = [ @@ -231,14 +232,15 @@ def tracks_to_tag_cache_format(tracks): ('info_end',) ] tracks.sort(key=lambda t: t.uri) - _add_to_tag_cache(result, *tracks_to_directory_tree(tracks)) + folders, files = tracks_to_directory_tree(tracks, music_path) + _add_to_tag_cache(result, folders, files, music_path) return result -def _add_to_tag_cache(result, folders, files): - base_path = settings.LOCAL_MUSIC_PATH.encode('utf-8') +def _add_to_tag_cache(result, folders, files, music_path): + base_path = music_path.encode('utf-8') - for path, entry in folders.items(): + for path, (entry_folders, entry_files) in folders.items(): try: text_path = path.decode('utf-8') except UnicodeDecodeError: @@ -247,7 +249,7 @@ def _add_to_tag_cache(result, folders, files): result.append(('directory', text_path)) result.append(('mtime', get_mtime(os.path.join(base_path, path)))) result.append(('begin', name)) - _add_to_tag_cache(result, *entry) + _add_to_tag_cache(result, entry_folders, entry_files, music_path) result.append(('end', name)) result.append(('songList begin',)) @@ -273,7 +275,7 @@ def _add_to_tag_cache(result, folders, files): result.append(('songList end',)) -def tracks_to_directory_tree(tracks): +def tracks_to_directory_tree(tracks, music_path): directories = ({}, []) for track in tracks: @@ -282,8 +284,7 @@ def tracks_to_directory_tree(tracks): absolute_track_dir_path = os.path.dirname(uri_to_path(track.uri)) relative_track_dir_path = re.sub( - '^' + re.escape(settings.LOCAL_MUSIC_PATH), b'', - absolute_track_dir_path) + '^' + re.escape(music_path), b'', absolute_track_dir_path) for part in split_path(relative_track_dir_path): path = os.path.join(path, part) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 4b057774..8bc57349 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -42,6 +42,7 @@ from mopidy.utils import log, path, versioning def main(): options = parse_options() + config = {} # TODO Read config from new config system log.setup_root_logger() log.setup_console_logging(options.verbosity_level) @@ -67,7 +68,8 @@ def main(): logging.info('Done scanning; writing tag cache...') - for row in mpd_translator.tracks_to_tag_cache_format(tracks): + for row in mpd_translator.tracks_to_tag_cache_format( + tracks, config['mpd']['music_path']): if len(row) == 1: print ('%s' % row).encode('utf-8') else: diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 3c32cd32..35e18c3b 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -13,9 +13,14 @@ from tests import unittest class MpdDispatcherTest(unittest.TestCase): def setUp(self): + config = { + 'mpd': { + 'password': None, + } + } self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() - self.dispatcher = MpdDispatcher() + self.dispatcher = MpdDispatcher(config=config) def tearDown(self): pykka.ActorRegistry.stop_all() diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 9d24c3fa..e62141ee 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -23,12 +23,20 @@ class MockConnection(mock.Mock): class BaseTestCase(unittest.TestCase): + def get_config(self): + return { + 'mpd': { + 'password': None, + } + } + def setUp(self): self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.connection = MockConnection() - self.session = session.MpdSession(self.connection, core=self.core) + self.session = session.MpdSession( + self.connection, config=self.get_config(), core=self.core) self.dispatcher = self.session.dispatcher self.context = self.dispatcher.context diff --git a/tests/frontends/mpd/protocol/authentication_test.py b/tests/frontends/mpd/protocol/authentication_test.py index 26b03f45..2597ddef 100644 --- a/tests/frontends/mpd/protocol/authentication_test.py +++ b/tests/frontends/mpd/protocol/authentication_test.py @@ -1,63 +1,56 @@ from __future__ import unicode_literals -from mopidy import settings - from tests.frontends.mpd import protocol -class AuthenticationTest(protocol.BaseTestCase): - def test_authentication_with_valid_password_is_accepted(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' +class AuthenticationActiveTest(protocol.BaseTestCase): + def get_config(self): + config = super(AuthenticationActiveTest, self).get_config() + config['mpd']['password'] = 'topsecret' + return config + def test_authentication_with_valid_password_is_accepted(self): self.sendRequest('password "topsecret"') self.assertTrue(self.dispatcher.authenticated) self.assertInResponse('OK') def test_authentication_with_invalid_password_is_not_accepted(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('password "secret"') self.assertFalse(self.dispatcher.authenticated) self.assertEqualResponse('ACK [3@0] {password} incorrect password') - def test_authentication_with_anything_when_password_check_turned_off(self): - settings.MPD_SERVER_PASSWORD = None - - self.sendRequest('any request at all') - self.assertTrue(self.dispatcher.authenticated) - self.assertEqualResponse('ACK [5@0] {} unknown command "any"') - def test_anything_when_not_authenticated_should_fail(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('any request at all') self.assertFalse(self.dispatcher.authenticated) self.assertEqualResponse( u'ACK [4@0] {any} you don\'t have permission for "any"') def test_close_is_allowed_without_authentication(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('close') self.assertFalse(self.dispatcher.authenticated) def test_commands_is_allowed_without_authentication(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('commands') self.assertFalse(self.dispatcher.authenticated) self.assertInResponse('OK') def test_notcommands_is_allowed_without_authentication(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('notcommands') self.assertFalse(self.dispatcher.authenticated) self.assertInResponse('OK') def test_ping_is_allowed_without_authentication(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('ping') self.assertFalse(self.dispatcher.authenticated) self.assertInResponse('OK') + + +class AuthenticationInactiveTest(protocol.BaseTestCase): + def test_authentication_with_anything_when_password_check_turned_off(self): + self.sendRequest('any request at all') + self.assertTrue(self.dispatcher.authenticated) + self.assertEqualResponse('ACK [5@0] {} unknown command "any"') + + def test_any_password_is_not_accepted_when_password_check_turned_off(self): + self.sendRequest('password "secret"') + self.assertEqualResponse('ACK [3@0] {password} incorrect password') diff --git a/tests/frontends/mpd/protocol/connection_test.py b/tests/frontends/mpd/protocol/connection_test.py index 840ce48f..01deb7a7 100644 --- a/tests/frontends/mpd/protocol/connection_test.py +++ b/tests/frontends/mpd/protocol/connection_test.py @@ -2,8 +2,6 @@ from __future__ import unicode_literals from mock import patch -from mopidy import settings - from tests.frontends.mpd import protocol @@ -26,21 +24,6 @@ class ConnectionHandlerTest(protocol.BaseTestCase): self.assertEqualResponse( 'ACK [4@0] {kill} you don\'t have permission for "kill"') - def test_valid_password_is_accepted(self): - settings.MPD_SERVER_PASSWORD = 'topsecret' - self.sendRequest('password "topsecret"') - self.assertEqualResponse('OK') - - def test_invalid_password_is_not_accepted(self): - settings.MPD_SERVER_PASSWORD = 'topsecret' - self.sendRequest('password "secret"') - self.assertEqualResponse('ACK [3@0] {password} incorrect password') - - def test_any_password_is_not_accepted_when_password_check_turned_off(self): - settings.MPD_SERVER_PASSWORD = None - self.sendRequest('password "secret"') - self.assertEqualResponse('ACK [3@0] {password} incorrect password') - def test_ping(self): self.sendRequest('ping') self.assertEqualResponse('OK') diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py index f2720473..16f4579f 100644 --- a/tests/frontends/mpd/protocol/reflection_test.py +++ b/tests/frontends/mpd/protocol/reflection_test.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings - from tests.frontends.mpd import protocol @@ -29,19 +27,6 @@ class ReflectionHandlerTest(protocol.BaseTestCase): self.assertNotInResponse('command: sticker') self.assertInResponse('OK') - def test_commands_show_less_if_auth_required_and_not_authed(self): - settings.MPD_SERVER_PASSWORD = u'secret' - self.sendRequest('commands') - # Not requiring auth - self.assertInResponse('command: close') - self.assertInResponse('command: commands') - self.assertInResponse('command: notcommands') - self.assertInResponse('command: password') - self.assertInResponse('command: ping') - # Requiring auth - self.assertNotInResponse('command: play') - self.assertNotInResponse('command: status') - def test_decoders(self): self.sendRequest('decoders') self.assertInResponse('OK') @@ -53,8 +38,35 @@ class ReflectionHandlerTest(protocol.BaseTestCase): self.assertInResponse('command: kill') self.assertInResponse('OK') + def test_tagtypes(self): + self.sendRequest('tagtypes') + self.assertInResponse('OK') + + def test_urlhandlers(self): + self.sendRequest('urlhandlers') + self.assertInResponse('OK') + self.assertInResponse('handler: dummy') + + +class ReflectionWhenNotAuthedTest(protocol.BaseTestCase): + def get_config(self): + config = super(ReflectionWhenNotAuthedTest, self).get_config() + config['mpd']['password'] = 'topsecret' + return config + + def test_commands_show_less_if_auth_required_and_not_authed(self): + self.sendRequest('commands') + # Not requiring auth + self.assertInResponse('command: close') + self.assertInResponse('command: commands') + self.assertInResponse('command: notcommands') + self.assertInResponse('command: password') + self.assertInResponse('command: ping') + # Requiring auth + self.assertNotInResponse('command: play') + self.assertNotInResponse('command: status') + def test_notcommands_returns_more_if_auth_required_and_not_authed(self): - settings.MPD_SERVER_PASSWORD = u'secret' self.sendRequest('notcommands') # Not requiring auth self.assertNotInResponse('command: close') @@ -65,12 +77,3 @@ class ReflectionHandlerTest(protocol.BaseTestCase): # Requiring auth self.assertInResponse('command: play') self.assertInResponse('command: status') - - def test_tagtypes(self): - self.sendRequest('tagtypes') - self.assertInResponse('OK') - - def test_urlhandlers(self): - self.sendRequest('urlhandlers') - self.assertInResponse('OK') - self.assertInResponse('handler: dummy') diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py index 088ae137..5df65c5c 100644 --- a/tests/frontends/mpd/translator_test.py +++ b/tests/frontends/mpd/translator_test.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import datetime import os -from mopidy import settings from mopidy.utils.path import mtime, uri_to_path from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, TlTrack, Playlist, Track @@ -24,11 +23,10 @@ class TrackMpdFormatTest(unittest.TestCase): ) def setUp(self): - settings.LOCAL_MUSIC_PATH = '/dir/subdir' + self.music_path = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): - settings.runtime.clear() mtime.undo_fake() def test_track_to_mpd_format_for_empty_track(self): @@ -137,15 +135,14 @@ class QueryFromMpdListFormatTest(unittest.TestCase): class TracksToTagCacheFormatTest(unittest.TestCase): def setUp(self): - settings.LOCAL_MUSIC_PATH = '/dir/subdir' + self.music_path = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): - settings.runtime.clear() mtime.undo_fake() def translate(self, track): - base_path = settings.LOCAL_MUSIC_PATH.encode('utf-8') + base_path = self.music_path.encode('utf-8') result = dict(translator.track_to_mpd_format(track)) result['file'] = uri_to_path(result['file'])[len(base_path) + 1:] result['key'] = os.path.basename(result['file']) @@ -177,11 +174,11 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.fail("Couldn't find end %s in result" % directory) def test_empty_tag_cache_has_header(self): - result = translator.tracks_to_tag_cache_format([]) + result = translator.tracks_to_tag_cache_format([], self.music_path) result = self.consume_headers(result) def test_empty_tag_cache_has_song_list(self): - result = translator.tracks_to_tag_cache_format([]) + result = translator.tracks_to_tag_cache_format([], self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -190,12 +187,12 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_header(self): track = Track(uri='file:///dir/subdir/song.mp3') - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) def test_tag_cache_has_song_list(self): track = Track(uri='file:///dir/subdir/song.mp3') - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -205,7 +202,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -216,7 +213,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track_with_key_and_mtime(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -227,7 +224,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_directories(self): track = Track(uri='file:///dir/subdir/folder/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) folder, result = self.consume_directory(result) @@ -241,7 +238,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_diretory_header_is_right(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) folder, result = self.consume_directory(result) @@ -253,7 +250,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) @@ -281,7 +278,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): formated.extend(self.translate(tracks[0])) formated.extend(self.translate(tracks[1])) - result = translator.tracks_to_tag_cache_format(tracks) + result = translator.tracks_to_tag_cache_format(tracks, self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -299,7 +296,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): formated.append(self.translate(tracks[0])) formated.append(self.translate(tracks[1])) - result = translator.tracks_to_tag_cache_format(tracks) + result = translator.tracks_to_tag_cache_format(tracks, self.music_path) result = self.consume_headers(result) folder, result = self.consume_directory(result) @@ -315,13 +312,10 @@ class TracksToTagCacheFormatTest(unittest.TestCase): class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): - settings.LOCAL_MUSIC_PATH = '/root/' - - def tearDown(self): - settings.runtime.clear() + self.music_path = '/root/' def test_no_tracks_gives_emtpy_tree(self): - tree = translator.tracks_to_directory_tree([]) + tree = translator.tracks_to_directory_tree([], self.music_path) self.assertEqual(tree, ({}, [])) def test_top_level_files(self): @@ -330,18 +324,18 @@ class TracksToDirectoryTreeTest(unittest.TestCase): Track(uri='file:///root/file2.mp3'), Track(uri='file:///root/file3.mp3'), ] - tree = translator.tracks_to_directory_tree(tracks) + tree = translator.tracks_to_directory_tree(tracks, self.music_path) self.assertEqual(tree, ({}, tracks)) def test_single_file_in_subdir(self): tracks = [Track(uri='file:///root/dir/file1.mp3')] - tree = translator.tracks_to_directory_tree(tracks) + tree = translator.tracks_to_directory_tree(tracks, self.music_path) expected = ({'dir': ({}, tracks)}, []) self.assertEqual(tree, expected) def test_single_file_in_sub_subdir(self): tracks = [Track(uri='file:///root/dir1/dir2/file1.mp3')] - tree = translator.tracks_to_directory_tree(tracks) + tree = translator.tracks_to_directory_tree(tracks, self.music_path) expected = ({'dir1': ({'dir1/dir2': ({}, tracks)}, [])}, []) self.assertEqual(tree, expected) @@ -353,7 +347,7 @@ class TracksToDirectoryTreeTest(unittest.TestCase): Track(uri='file:///root/dir2/file4.mp3'), Track(uri='file:///root/dir2/sub/file5.mp3'), ] - tree = translator.tracks_to_directory_tree(tracks) + tree = translator.tracks_to_directory_tree(tracks, self.music_path) expected = ( { 'dir1': ({}, [tracks[1], tracks[2]]), From 1ca6ffc6fb9ac0649cea18464c3cc295a82b7aeb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Apr 2013 23:47:03 +0200 Subject: [PATCH 07/15] mpd: Fix two test failures --- tests/frontends/mpd/translator_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py index 5df65c5c..828acf1a 100644 --- a/tests/frontends/mpd/translator_test.py +++ b/tests/frontends/mpd/translator_test.py @@ -312,7 +312,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): - self.music_path = '/root/' + self.music_path = '/root' def test_no_tracks_gives_emtpy_tree(self): tree = translator.tracks_to_directory_tree([], self.music_path) From 42707f50df0bec18a1458a0fbf1020c522275b92 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:04:35 +0200 Subject: [PATCH 08/15] audio: Use new config system --- mopidy/audio/actor.py | 43 ++++++++++++++++++++++--------------- mopidy/audio/mixers/auto.py | 4 ++-- mopidy/audio/mixers/fake.py | 4 ++-- mopidy/audio/mixers/nad.py | 16 ++++++-------- tests/audio/actor_test.py | 24 +++++++++++++++------ 5 files changed, 54 insertions(+), 37 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 42dee084..5d92f3c4 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -9,7 +9,6 @@ import logging import pykka -from mopidy import settings from mopidy.utils import process from . import mixers, utils @@ -28,11 +27,14 @@ class Audio(pykka.ThreadingActor): """ Audio output through `GStreamer `_. - **Settings:** + **Default config:** - - :attr:`mopidy.settings.OUTPUT` - - :attr:`mopidy.settings.MIXER` - - :attr:`mopidy.settings.MIXER_TRACK` + .. code-block:: ini + + [audio] + mixer = autoaudiomixer + mixer_track = + output = autoaudiosink """ #: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState` @@ -41,6 +43,8 @@ class Audio(pykka.ThreadingActor): def __init__(self, config): super(Audio, self).__init__() + self._config = config + self._playbin = None self._signal_ids = {} # {(element, event): signal_id} @@ -143,47 +147,51 @@ class Audio(pykka.ThreadingActor): self._playbin.set_state(gst.STATE_NULL) def _setup_output(self): + output_desc = self._config['audio']['output'] try: output = gst.parse_bin_from_description( - settings.OUTPUT, ghost_unconnected_pads=True) + output_desc, ghost_unconnected_pads=True) self._playbin.set_property('audio-sink', output) - logger.info('Audio output set to "%s"', settings.OUTPUT) + logger.info('Audio output set to "%s"', output_desc) except gobject.GError as ex: logger.error( - 'Failed to create audio output "%s": %s', settings.OUTPUT, ex) + 'Failed to create audio output "%s": %s', output_desc, ex) process.exit_process() def _setup_mixer(self): - if not settings.MIXER: + mixer_desc = self._config['audio']['mixer'] + track_desc = self._config['audio']['mixer_track'] + + if mixer_desc is None: logger.info('Not setting up audio mixer') return - if settings.MIXER == 'software': + if mixer_desc == 'software': self._software_mixing = True logger.info('Audio mixer is using software mixing') return try: mixerbin = gst.parse_bin_from_description( - settings.MIXER, ghost_unconnected_pads=False) + mixer_desc, ghost_unconnected_pads=False) except gobject.GError as ex: logger.warning( - 'Failed to create audio mixer "%s": %s', settings.MIXER, ex) + 'Failed to create audio mixer "%s": %s', mixer_desc, ex) return # We assume that the bin will contain a single mixer. mixer = mixerbin.get_by_interface(b'GstMixer') if not mixer: logger.warning( - 'Did not find any audio mixers in "%s"', settings.MIXER) + 'Did not find any audio mixers in "%s"', mixer_desc) return if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: logger.warning( - 'Setting audio mixer "%s" to READY failed', settings.MIXER) + 'Setting audio mixer "%s" to READY failed', mixer_desc) return - track = self._select_mixer_track(mixer, settings.MIXER_TRACK) + track = self._select_mixer_track(mixer, track_desc) if not track: logger.warning('Could not find usable audio mixer track') return @@ -198,8 +206,9 @@ class Audio(pykka.ThreadingActor): def _select_mixer_track(self, mixer, track_label): # Ignore tracks without volumes, then look for track with - # label == settings.MIXER_TRACK, otherwise fallback to first usable - # track hoping the mixer gave them to us in a sensible order. + # label equal to the audio/mixer_track config value, otherwise fallback + # to first usable track hoping the mixer gave them to us in a sensible + # order. usable_tracks = [] for track in mixer.list_tracks(): diff --git a/mopidy/audio/mixers/auto.py b/mopidy/audio/mixers/auto.py index 96359da1..b24bcf4c 100644 --- a/mopidy/audio/mixers/auto.py +++ b/mopidy/audio/mixers/auto.py @@ -6,9 +6,9 @@ This is Mopidy's default mixer. None -**Settings** +**Configuration** -If this wasn't the default, you would set :attr:`mopidy.settings.MIXER` to +If this wasn't the default, you would set the ``audio/mixer`` config value to ``autoaudiomixer`` to use this mixer. """ diff --git a/mopidy/audio/mixers/fake.py b/mopidy/audio/mixers/fake.py index 738491b5..05e86923 100644 --- a/mopidy/audio/mixers/fake.py +++ b/mopidy/audio/mixers/fake.py @@ -4,9 +4,9 @@ None -**Settings** +**Configuration** -Set :attr:`mopidy.settings.MIXER` to ``fakemixer`` to use this mixer. +Set the ``audio/mixer`` config value to ``fakemixer`` to use this mixer. """ from __future__ import unicode_literals diff --git a/mopidy/audio/mixers/nad.py b/mopidy/audio/mixers/nad.py index 8481de55..058333d1 100644 --- a/mopidy/audio/mixers/nad.py +++ b/mopidy/audio/mixers/nad.py @@ -7,10 +7,10 @@ serial cable. .. literalinclude:: ../../../../requirements/external_mixers.txt -**Settings** +**Configuration** -Set :attr:`mopidy.settings.MIXER` to ``nadmixer`` to use it. You probably also -needs to add some properties to the ``MIXER`` setting. +Set the ``audio/mixer`` config value to ``nadmixer`` to use it. You probably +also needs to add some properties to the ``audio/mixer`` config value. Supported properties includes: @@ -34,15 +34,13 @@ Supported properties includes: Configuration examples:: # Minimum configuration, if the amplifier is available at /dev/ttyUSB0 - MIXER = u'nadmixer' + mixer = nadmixer # Minimum configuration, if the amplifier is available elsewhere - MIXER = u'nadmixer port=/dev/ttyUSB3' + mixer = nadmixer port=/dev/ttyUSB3 # Full configuration - MIXER = ( - u'nadmixer port=/dev/ttyUSB0 ' - u'source=aux speakers-a=on speakers-b=off') + mixer = nadmixer port=/dev/ttyUSB0 source=aux speakers-a=on speakers-b=off """ from __future__ import unicode_literals @@ -132,7 +130,7 @@ class NadTalker(pykka.ThreadingActor): calibrating the NAD amplifier's volume. """ - # Serial link settings + # Serial link config BAUDRATE = 115200 BYTESIZE = 8 PARITY = 'N' diff --git a/tests/audio/actor_test.py b/tests/audio/actor_test.py index 51786adb..a40b0572 100644 --- a/tests/audio/actor_test.py +++ b/tests/audio/actor_test.py @@ -6,7 +6,7 @@ import gst import pykka -from mopidy import audio, settings +from mopidy import audio from mopidy.utils.path import path_to_uri from tests import unittest, path_to_data_dir @@ -14,14 +14,18 @@ from tests import unittest, path_to_data_dir class AudioTest(unittest.TestCase): def setUp(self): - settings.MIXER = 'fakemixer track_max_volume=65536' - settings.OUTPUT = 'fakesink' + config = { + 'audio': { + 'mixer': 'fakemixer track_max_volume=65536', + 'mixer_track': None, + 'output': 'fakesink', + } + } self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) - self.audio = audio.Audio.start(config=None).proxy() + self.audio = audio.Audio.start(config=config).proxy() def tearDown(self): pykka.ActorRegistry.stop_all() - settings.runtime.clear() def prepare_uri(self, uri): self.audio.prepare_change() @@ -59,8 +63,14 @@ class AudioTest(unittest.TestCase): self.assertEqual(value, self.audio.get_volume().get()) def test_set_volume_with_mixer_max_below_100(self): - settings.MIXER = 'fakemixer track_max_volume=40' - self.audio = audio.Audio.start(config=None).proxy() + config = { + 'audio': { + 'mixer': 'fakemixer track_max_volume=40', + 'mixer_track': None, + 'output': 'fakesink', + } + } + self.audio = audio.Audio.start(config=config).proxy() for value in range(0, 101): self.assertTrue(self.audio.set_volume(value).get()) From 2276130758519b4240fb6004d74a1b98dab0c26f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:33:34 +0200 Subject: [PATCH 09/15] main: Use new config system --- mopidy/__main__.py | 45 +++++++++++++++++++++----------------------- mopidy/utils/path.py | 2 ++ 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 3783d150..94e245d0 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -28,7 +28,7 @@ sys.path.insert( 0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) -from mopidy import exceptions, settings +from mopidy import exceptions from mopidy.audio import Audio from mopidy.config import default_config, config_schemas from mopidy.core import Core @@ -48,8 +48,10 @@ def main(): config_files = options.config.split(':') config_overrides = options.overrides + extensions = [] # Make sure it is defined before the finally block + try: - extensions = [] # Make sure it is defined before the finally block + create_file_structures() logging_config = load_config(config_files, config_overrides) log.setup_logging( logging_config, options.verbosity_level, options.save_debug_log) @@ -58,8 +60,7 @@ def main(): extensions = filter_enabled_extensions(raw_config, extensions) config = validate_config(raw_config, config_schemas, extensions) log.setup_log_levels(config) - check_old_folders() - setup_settings() + check_old_locations() # Anything that wants to exit after this point must use # mopidy.utils.process.exit_process as actors have been started. @@ -68,8 +69,6 @@ def main(): core = setup_core(audio, backends) setup_frontends(config, extensions, core) loop.run() - except exceptions.SettingsError as ex: - logger.error(ex.message) except KeyboardInterrupt: logger.info('Interrupted. Exiting...') except Exception as ex: @@ -167,17 +166,20 @@ def show_config_callback(option, opt, value, parser): sys.exit(0) -def check_old_folders(): - # TODO: add old settings and pre extension storage locations? - old_settings_folder = os.path.expanduser('~/.mopidy') +def check_old_locations(): + dot_mopidy_dir = path.expand_path('~/.mopidy') + if os.path.isdir(dot_mopidy_dir): + logger.warning( + 'Old Mopidy dot dir found at %s. Please migrate your config to ' + 'the ini-file based config format. See release notes for further ' + 'instructions.', dot_mopidy_dir) - if not os.path.isdir(old_settings_folder): - return - - logger.warning( - 'Old settings folder found at %s, settings.py should be moved ' - 'to %s, any cache data should be deleted. See release notes for ' - 'further instructions.', old_settings_folder, path.SETTINGS_PATH) + old_settings_file = path.expand_path('$XDG_CONFIG_DIR/mopidy/settings.py') + if os.path.isfile(old_settings_file): + logger.warning( + 'Old Mopidy settings file found at %s. Please migrate your ' + 'config to the ini-file based config format. See release notes ' + 'for further instructions.', old_settings_file) def load_extensions(): @@ -306,15 +308,10 @@ def validate_config(raw_config, schemas, extensions=None): return config -def setup_settings(): - path.get_or_create_folder(path.SETTINGS_PATH) +def create_file_structures(): path.get_or_create_folder(path.DATA_PATH) - path.get_or_create_file(path.SETTINGS_FILE) - try: - settings.validate() - except exceptions.SettingsError as ex: - logger.error(ex.message) - sys.exit(1) + path.get_or_create_folder(path.CONFIG_PATH) + path.get_or_create_file(path.CONFIG_FILE) def setup_audio(config): diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 4e5a66cd..88b8be3e 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -26,6 +26,8 @@ XDG_DIRS = { 'XDG_MUSIC_DIR': XDG_MUSIC_DIR, } DATA_PATH = os.path.join(unicode(XDG_DATA_DIR), 'mopidy') +CONFIG_PATH = os.path.join(unicode(XDG_CONFIG_DIR), 'mopidy') +CONFIG_FILE = os.path.join(unicode(CONFIG_PATH), 'mopidy.conf') SETTINGS_PATH = os.path.join(unicode(XDG_CONFIG_DIR), 'mopidy') SETTINGS_FILE = os.path.join(unicode(SETTINGS_PATH), 'settings.py') From e92a7628f25a151f3f83fd0b90da7d590cdc757e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:36:41 +0200 Subject: [PATCH 10/15] settings: Remove SettingsProxy at mopidy.settings --- mopidy/__init__.py | 5 ----- tests/__init__.py | 5 ----- tests/frontends/mpd/protocol/__init__.py | 3 +-- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 416f4fbf..fc2b611b 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -24,8 +24,3 @@ warnings.filterwarnings('ignore', 'could not open display') __version__ = '0.13.0' - - -from mopidy import settings as default_settings_module -from mopidy.utils.settings import SettingsProxy -settings = SettingsProxy(default_settings_module) diff --git a/tests/__init__.py b/tests/__init__.py index 7f7a9c36..b4e1d283 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,11 +8,6 @@ if sys.version_info < (2, 7): else: import unittest # noqa -from mopidy import settings - -# Nuke any local settings to ensure same test env all over -settings.local.clear() - def path_to_data_dir(name): path = os.path.dirname(__file__) diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index e62141ee..21ba1c25 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import mock import pykka -from mopidy import core, settings +from mopidy import core from mopidy.backends import dummy from mopidy.frontends.mpd import session @@ -42,7 +42,6 @@ class BaseTestCase(unittest.TestCase): def tearDown(self): pykka.ActorRegistry.stop_all() - settings.runtime.clear() def sendRequest(self, request): self.connection.response = [] From 777993583f4d7faef3b93c9094fbf5fd86311670 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:39:26 +0200 Subject: [PATCH 11/15] settings: Remove settings module --- docs/settings.rst | 4 +- mopidy/settings.py | 285 --------------------------------------------- 2 files changed, 1 insertion(+), 288 deletions(-) delete mode 100644 mopidy/settings.py diff --git a/docs/settings.rst b/docs/settings.rst index 4c9acd96..e16cfbfd 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -218,6 +218,4 @@ used at the same time without any danger of naming collisions. Available settings ================== -.. automodule:: mopidy.settings - :synopsis: Available settings and their default values - :members: +.. note:: TODO: Document config values of the new config system diff --git a/mopidy/settings.py b/mopidy/settings.py deleted file mode 100644 index cde6430a..00000000 --- a/mopidy/settings.py +++ /dev/null @@ -1,285 +0,0 @@ -""" -All available settings and their default values. - -.. warning:: - - Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a - file called ``~/.config/mopidy/settings.py`` and redefine settings there. -""" - -from __future__ import unicode_literals - -#: The log format used for informational logging. -#: -#: See http://docs.python.org/2/library/logging.html#formatter-objects for -#: details on the format. -CONSOLE_LOG_FORMAT = '%(levelname)-8s %(message)s' - -#: The log format used for debug logging. -#: -#: See http://docs.python.org/library/logging.html#formatter-objects for -#: details on the format. -DEBUG_LOG_FORMAT = '%(levelname)-8s %(asctime)s' + \ - ' [%(process)d:%(threadName)s] %(name)s\n %(message)s' - -#: The file to dump debug log data to when Mopidy is run with the -#: :option:`--save-debug-log` option. -#: -#: Default:: -#: -#: DEBUG_LOG_FILENAME = u'mopidy.log' -DEBUG_LOG_FILENAME = 'mopidy.log' - -#: Location of the Mopidy .desktop file. -#: -#: Used by :mod:`mopidy.frontends.mpris`. -#: -#: Default:: -#: -#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop' -DESKTOP_FILE = '/usr/share/applications/mopidy.desktop' - -#: Which address Mopidy's HTTP server should bind to. -#: -#: Used by :mod:`mopidy.frontends.http`. -#: -#: Examples: -#: -#: ``127.0.0.1`` -#: Listens only on the IPv4 loopback interface. Default. -#: ``::1`` -#: Listens only on the IPv6 loopback interface. -#: ``0.0.0.0`` -#: Listens on all IPv4 interfaces. -#: ``::`` -#: Listens on all interfaces, both IPv4 and IPv6. -HTTP_SERVER_HOSTNAME = u'127.0.0.1' - -#: Which TCP port Mopidy's HTTP server should listen to. -#: -#: Used by :mod:`mopidy.frontends.http`. -#: -#: Default: 6680 -HTTP_SERVER_PORT = 6680 - -#: Which directory Mopidy's HTTP server should serve at ``/``. -#: -#: Change this to have Mopidy serve e.g. files for your JavaScript client. -#: ``/mopidy`` will continue to work as usual even if you change this setting. -#: -#: Used by :mod:`mopidy.frontends.http`. -#: -#: Default: None -HTTP_SERVER_STATIC_DIR = None - -#: Your `Last.fm `_ username. -#: -#: Used by :mod:`mopidy.frontends.lastfm`. -LASTFM_USERNAME = '' - -#: Your `Last.fm `_ password. -#: -#: Used by :mod:`mopidy.frontends.lastfm`. -LASTFM_PASSWORD = '' - -#: Path to folder with local music. -#: -#: Used by :mod:`mopidy.backends.local`. -#: -#: Default:: -#: -#: LOCAL_MUSIC_PATH = u'$XDG_MUSIC_DIR' -LOCAL_MUSIC_PATH = '$XDG_MUSIC_DIR' - -#: Path to playlist folder with m3u files for local music. -#: -#: Used by :mod:`mopidy.backends.local`. -#: -#: Default:: -#: -#: LOCAL_PLAYLIST_PATH = u'$XDG_DATA_DIR/mopidy/playlists' -LOCAL_PLAYLIST_PATH = '$XDG_DATA_DIR/mopidy/playlists' - -#: Path to tag cache for local music. -#: -#: Used by :mod:`mopidy.backends.local`. -#: -#: Default:: -#: -#: LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache' -LOCAL_TAG_CACHE_FILE = '$XDG_DATA_DIR/mopidy/tag_cache' - -#: Audio mixer to use. -#: -#: Expects a GStreamer mixer to use, typical values are: -#: ``alsamixer``, ``pulsemixer``, ``ossmixer``, and ``oss4mixer``. -#: -#: Setting this to :class:`None` turns off volume control. ``software`` -#: can be used to force software mixing in the application. -#: -#: Default:: -#: -#: MIXER = u'autoaudiomixer' -MIXER = 'autoaudiomixer' - -#: Audio mixer track to use. -#: -#: Name of the mixer track to use. If this is not set we will try to find the -#: master output track. As an example, using ``alsamixer`` you would -#: typically set this to ``Master`` or ``PCM``. -#: -#: Default:: -#: -#: MIXER_TRACK = None -MIXER_TRACK = None - -#: Number of seconds an MPD client can stay inactive before the connection is -#: closed by the server. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Default:: -#: -#: MPD_SERVER_CONNECTION_TIMEOUT = 60 -MPD_SERVER_CONNECTION_TIMEOUT = 60 - -#: Which address Mopidy's MPD server should bind to. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Examples: -#: -#: ``127.0.0.1`` -#: Listens only on the IPv4 loopback interface. Default. -#: ``::1`` -#: Listens only on the IPv6 loopback interface. -#: ``0.0.0.0`` -#: Listens on all IPv4 interfaces. -#: ``::`` -#: Listens on all interfaces, both IPv4 and IPv6. -MPD_SERVER_HOSTNAME = '127.0.0.1' - -#: Which TCP port Mopidy's MPD server should listen to. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Default: 6600 -MPD_SERVER_PORT = 6600 - -#: The password required for connecting to the MPD server. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Default: :class:`None`, which means no password required. -MPD_SERVER_PASSWORD = None - -#: The maximum number of concurrent connections the MPD server will accept. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Default: 20 -MPD_SERVER_MAX_CONNECTIONS = 20 - -#: Audio output to use. -#: -#: Expects a GStreamer sink. Typical values are ``autoaudiosink``, -#: ``alsasink``, ``osssink``, ``oss4sink``, ``pulsesink``, and ``shout2send``, -#: and additional arguments specific to each sink. -#: -#: Default:: -#: -#: OUTPUT = u'autoaudiosink' -OUTPUT = 'autoaudiosink' - -#: Path to the Spotify cache. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_CACHE_PATH = u'$XDG_CACHE_DIR/mopidy/spotify' -SPOTIFY_CACHE_PATH = '$XDG_CACHE_DIR/mopidy/spotify' - -#: Your Spotify Premium username. -#: -#: Used by :mod:`mopidy.backends.spotify`. -SPOTIFY_USERNAME = '' - -#: Your Spotify Premium password. -#: -#: Used by :mod:`mopidy.backends.spotify`. -SPOTIFY_PASSWORD = '' - -#: Spotify preferred bitrate. -#: -#: Available values are 96, 160, and 320. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_BITRATE = 160 -SPOTIFY_BITRATE = 160 - -#: Spotify proxy host. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Example:: -#: -#: SPOTIFY_PROXY_HOST = u'protocol://host:port' -#: -#: Default:: -#: -#: SPOTIFY_PROXY_HOST = None -SPOTIFY_PROXY_HOST = None - -#: Spotify proxy username. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_PROXY_USERNAME = None -SPOTIFY_PROXY_USERNAME = None - -#: Spotify proxy password. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_PROXY_PASSWORD = None -SPOTIFY_PROXY_PASSWORD = None - -#: Max number of seconds to wait for Spotify operations to complete. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_TIMEOUT = 10 -SPOTIFY_TIMEOUT = 10 - -#: Whitelist of URIs to support streaming from. -#: -#: Used by :mod:`mopidy.backends.stream`. -#: -#: Default:: -#: -#: STREAM_PROTOCOLS = ( -#: u'http', -#: u'https', -#: u'mms', -#: u'rtmp', -#: u'rtmps', -#: u'rtsp', -#: ) -STREAM_PROTOCOLS = ( - 'http', - 'https', - 'mms', - 'rtmp', - 'rtmps', - 'rtsp', -) From 63b7260c01e0d3ed68bad49932112c6bbb0f4103 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:41:09 +0200 Subject: [PATCH 12/15] settings: Remove settings utils --- mopidy/utils/settings.py | 173 ----------------------------------- tests/utils/settings_test.py | 150 ------------------------------ 2 files changed, 323 deletions(-) delete mode 100644 mopidy/utils/settings.py delete mode 100644 tests/utils/settings_test.py diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py deleted file mode 100644 index f903a70d..00000000 --- a/mopidy/utils/settings.py +++ /dev/null @@ -1,173 +0,0 @@ -# Absolute import needed to import ~/.config/mopidy/settings.py and not -# ourselves -from __future__ import absolute_import, unicode_literals - -import copy -import getpass -import logging -import os -import pprint -import sys - -from mopidy import exceptions -from mopidy.utils import formatting, path - -logger = logging.getLogger('mopidy.utils.settings') - - -class SettingsProxy(object): - def __init__(self, default_settings_module): - self.default = self._get_settings_dict_from_module( - default_settings_module) - self.local = self._get_local_settings() - self.runtime = {} - - def _get_local_settings(self): - if not os.path.isfile(path.SETTINGS_FILE): - return {} - sys.path.insert(0, path.SETTINGS_PATH) - # pylint: disable = F0401 - import settings as local_settings_module - # pylint: enable = F0401 - return self._get_settings_dict_from_module(local_settings_module) - - def _get_settings_dict_from_module(self, module): - settings = filter( - lambda (key, value): self._is_setting(key), - module.__dict__.iteritems()) - return dict(settings) - - def _is_setting(self, name): - return name.isupper() - - @property - def current(self): - current = copy.copy(self.default) - current.update(self.local) - current.update(self.runtime) - return current - - def __getattr__(self, attr): - if not self._is_setting(attr): - return - - current = self.current # bind locally to avoid copying+updates - if attr not in current: - raise exceptions.SettingsError('Setting "%s" is not set.' % attr) - - value = current[attr] - if isinstance(value, basestring) and len(value) == 0: - raise exceptions.SettingsError('Setting "%s" is empty.' % attr) - if not value: - return value - if attr.endswith('_PATH') or attr.endswith('_FILE'): - value = path.expand_path(value) - return value - - def __setattr__(self, attr, value): - if self._is_setting(attr): - self.runtime[attr] = value - else: - super(SettingsProxy, self).__setattr__(attr, value) - - def validate(self): - if self.get_errors(): - logger.error( - 'Settings validation errors: %s', - formatting.indent(self.get_errors_as_string())) - raise exceptions.SettingsError('Settings validation failed.') - - def _read_from_stdin(self, prompt): - if '_PASSWORD' in prompt: - return ( - getpass.getpass(prompt) - .decode(sys.stdin.encoding, 'ignore')) - else: - sys.stdout.write(prompt) - return ( - sys.stdin.readline().strip() - .decode(sys.stdin.encoding, 'ignore')) - - def get_errors(self): - return validate_settings(self.default, self.local) - - def get_errors_as_string(self): - lines = [] - for (setting, error) in self.get_errors().iteritems(): - lines.append('%s: %s' % (setting, error)) - return '\n'.join(lines) - - -def validate_settings(defaults, settings): - """ - Checks the settings for both errors like misspellings and against a set of - rules for renamed settings, etc. - - Returns mapping from setting names to associated errors. - - :param defaults: Mopidy's default settings - :type defaults: dict - :param settings: the user's local settings - :type settings: dict - :rtype: dict - """ - errors = {} - - changed = { - 'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME', - 'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT', - 'GSTREAMER_AUDIO_SINK': 'OUTPUT', - 'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH', - 'LOCAL_OUTPUT_OVERRIDE': 'OUTPUT', - 'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH', - 'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE', - 'MIXER_ALSA_CONTROL': None, - 'MIXER_EXT_PORT': None, - 'MIXER_EXT_SPEAKERS_A': None, - 'MIXER_EXT_SPEAKERS_B': None, - 'MIXER_MAX_VOLUME': None, - 'SERVER': None, - 'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME', - 'SERVER_PORT': 'MPD_SERVER_PORT', - 'SPOTIFY_HIGH_BITRATE': 'SPOTIFY_BITRATE', - 'SPOTIFY_LIB_APPKEY': None, - 'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH', - } - - must_be_iterable = [ - 'STREAM_PROTOCOLS', - ] - - for setting, value in settings.iteritems(): - if setting in changed: - if changed[setting] is None: - errors[setting] = 'Deprecated setting. It may be removed.' - else: - errors[setting] = 'Deprecated setting. Use %s.' % ( - changed[setting],) - - elif setting == 'OUTPUTS': - errors[setting] = ( - 'Deprecated setting, please change to OUTPUT. OUTPUT expects ' - 'a GStreamer bin description string for your desired output.') - - elif setting == 'SPOTIFY_BITRATE': - if value not in (96, 160, 320): - errors[setting] = ( - 'Unavailable Spotify bitrate. Available bitrates are 96, ' - '160, and 320.') - - elif setting.startswith('SHOUTCAST_OUTPUT_'): - errors[setting] = ( - 'Deprecated setting, please set the value via the GStreamer ' - 'bin in OUTPUT.') - - elif setting in must_be_iterable and not hasattr(value, '__iter__'): - errors[setting] = ( - 'Must be a tuple. ' - "Remember the comma after single values: (u'value',)") - - elif setting not in defaults and not setting.startswith('CUSTOM_'): - errors[setting] = 'Unknown setting.' - - return errors diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py deleted file mode 100644 index ce763486..00000000 --- a/tests/utils/settings_test.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import unicode_literals - -import os - -from mopidy import exceptions, settings -from mopidy.utils import settings as setting_utils - -from tests import unittest - - -class ValidateSettingsTest(unittest.TestCase): - def setUp(self): - self.defaults = { - 'MPD_SERVER_HOSTNAME': '::', - 'MPD_SERVER_PORT': 6600, - 'SPOTIFY_BITRATE': 160, - } - - def test_no_errors_yields_empty_dict(self): - result = setting_utils.validate_settings(self.defaults, {}) - self.assertEqual(result, {}) - - def test_unknown_setting_returns_error(self): - result = setting_utils.validate_settings( - self.defaults, {'MPD_SERVER_HOSTNMAE': '127.0.0.1'}) - self.assertEqual( - result['MPD_SERVER_HOSTNMAE'], 'Unknown setting.') - - def test_custom_settings_does_not_return_errors(self): - result = setting_utils.validate_settings( - self.defaults, {'CUSTOM_MYAPP_SETTING': 'foobar'}) - self.assertNotIn('CUSTOM_MYAPP_SETTING', result) - - def test_not_renamed_setting_returns_error(self): - result = setting_utils.validate_settings( - self.defaults, {'SERVER_HOSTNAME': '127.0.0.1'}) - self.assertEqual( - result['SERVER_HOSTNAME'], - 'Deprecated setting. Use MPD_SERVER_HOSTNAME.') - - def test_unneeded_settings_returns_error(self): - result = setting_utils.validate_settings( - self.defaults, {'SPOTIFY_LIB_APPKEY': '/tmp/foo'}) - self.assertEqual( - result['SPOTIFY_LIB_APPKEY'], - 'Deprecated setting. It may be removed.') - - def test_unavailable_bitrate_setting_returns_error(self): - result = setting_utils.validate_settings( - self.defaults, {'SPOTIFY_BITRATE': 50}) - self.assertEqual( - result['SPOTIFY_BITRATE'], - 'Unavailable Spotify bitrate. ' - 'Available bitrates are 96, 160, and 320.') - - def test_two_errors_are_both_reported(self): - result = setting_utils.validate_settings( - self.defaults, {'FOO': '', 'BAR': ''}) - self.assertEqual(len(result), 2) - - -class SettingsProxyTest(unittest.TestCase): - def setUp(self): - self.settings = setting_utils.SettingsProxy(settings) - self.settings.local.clear() - - def test_set_and_get_attr(self): - self.settings.TEST = 'test' - self.assertEqual(self.settings.TEST, 'test') - - def test_getattr_raises_error_on_missing_setting(self): - try: - self.settings.TEST - self.fail('Should raise exception') - except exceptions.SettingsError as e: - self.assertEqual('Setting "TEST" is not set.', e.message) - - def test_getattr_raises_error_on_empty_setting(self): - self.settings.TEST = '' - try: - self.settings.TEST - self.fail('Should raise exception') - except exceptions.SettingsError as e: - self.assertEqual('Setting "TEST" is empty.', e.message) - - def test_getattr_does_not_raise_error_if_setting_is_false(self): - self.settings.TEST = False - self.assertEqual(False, self.settings.TEST) - - def test_getattr_does_not_raise_error_if_setting_is_none(self): - self.settings.TEST = None - self.assertEqual(None, self.settings.TEST) - - def test_getattr_does_not_raise_error_if_setting_is_zero(self): - self.settings.TEST = 0 - self.assertEqual(0, self.settings.TEST) - - def test_setattr_updates_runtime_settings(self): - self.settings.TEST = 'test' - self.assertIn('TEST', self.settings.runtime) - - def test_setattr_updates_runtime_with_value(self): - self.settings.TEST = 'test' - self.assertEqual(self.settings.runtime['TEST'], 'test') - - def test_runtime_value_included_in_current(self): - self.settings.TEST = 'test' - self.assertEqual(self.settings.current['TEST'], 'test') - - def test_value_ending_in_path_is_expanded(self): - self.settings.TEST_PATH = '~/test' - actual = self.settings.TEST_PATH - expected = os.path.expanduser('~/test') - self.assertEqual(actual, expected) - - def test_value_ending_in_path_is_absolute(self): - self.settings.TEST_PATH = './test' - actual = self.settings.TEST_PATH - expected = os.path.abspath('./test') - self.assertEqual(actual, expected) - - def test_value_ending_in_file_is_expanded(self): - self.settings.TEST_FILE = '~/test' - actual = self.settings.TEST_FILE - expected = os.path.expanduser('~/test') - self.assertEqual(actual, expected) - - def test_value_ending_in_file_is_absolute(self): - self.settings.TEST_FILE = './test' - actual = self.settings.TEST_FILE - expected = os.path.abspath('./test') - self.assertEqual(actual, expected) - - def test_value_not_ending_in_path_or_file_is_not_expanded(self): - self.settings.TEST = '~/test' - actual = self.settings.TEST - self.assertEqual(actual, '~/test') - - def test_value_not_ending_in_path_or_file_is_not_absolute(self): - self.settings.TEST = './test' - actual = self.settings.TEST - self.assertEqual(actual, './test') - - def test_value_ending_in_file_can_be_none(self): - self.settings.TEST_FILE = None - self.assertEqual(self.settings.TEST_FILE, None) - - def test_value_ending_in_path_can_be_none(self): - self.settings.TEST_PATH = None - self.assertEqual(self.settings.TEST_PATH, None) From a5d81b86c0c5ba768a8434491b2bb1b67439119a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 20:11:12 +0200 Subject: [PATCH 13/15] scanner: Use new config --- mopidy/scanner.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 8bc57349..364828f4 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -34,7 +34,6 @@ import pygst pygst.require('0.10') import gst -from mopidy import settings from mopidy.frontends.mpd import translator as mpd_translator from mopidy.models import Track, Artist, Album from mopidy.utils import log, path, versioning @@ -58,9 +57,9 @@ def main(): logging.warning('Failed %s: %s', uri, error) logging.debug('Debug info for %s: %s', uri, debug) - logging.info('Scanning %s', settings.LOCAL_MUSIC_PATH) + logging.info('Scanning %s', config['local']['music_path']) - scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug) + scanner = Scanner(config['local']['music_path'], store, debug) try: scanner.start() except KeyboardInterrupt: From d83d33aece9559f0c82b76e936e4ff478ee067e2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 10:01:24 +0200 Subject: [PATCH 14/15] scrobbler: Update to use 'scrobbler' config values --- mopidy/frontends/scrobbler/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/scrobbler/actor.py b/mopidy/frontends/scrobbler/actor.py index 469df17a..1809661a 100644 --- a/mopidy/frontends/scrobbler/actor.py +++ b/mopidy/frontends/scrobbler/actor.py @@ -30,8 +30,8 @@ class ScrobblerFrontend(pykka.ThreadingActor, CoreListener): try: self.lastfm = pylast.LastFMNetwork( api_key=API_KEY, api_secret=API_SECRET, - username=self.config['lastfm']['username'], - password_hash=pylast.md5(self.config['lastfm']['password'])) + username=self.config['scrobbler']['username'], + password_hash=pylast.md5(self.config['scrobbler']['password'])) logger.info('Connected to Last.fm') except (pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: From 05c74abbe321681909312e91254c1c45a3121545 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 10:01:36 +0200 Subject: [PATCH 15/15] http: Revert to use 'static_dir' --- mopidy/frontends/http/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index 1b7e7c7d..4297378c 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -30,8 +30,7 @@ port = 6680 # # Change this to have Mopidy serve e.g. files for your JavaScript client. # "/mopidy" will continue to work as usual even if you change this setting. -# -static_path = +static_dir = [logging.levels] cherrypy = warning @@ -533,7 +532,7 @@ class Extension(ext.Extension): schema = config.ExtensionConfigSchema() schema['hostname'] = config.Hostname() schema['port'] = config.Port() - schema['static_path'] = config.Path(optional=True) + schema['static_dir'] = config.Path(optional=True) return schema def validate_environment(self):