Merge branch 'develop' into feature/limit-spotify-data-pushing
This commit is contained in:
commit
610a04bf6c
@ -5,9 +5,33 @@ Changes
|
||||
This change log is used to track all major changes to Mopidy.
|
||||
|
||||
|
||||
v0.11.0 (in development)
|
||||
v0.12.0 (in development)
|
||||
========================
|
||||
|
||||
(in development)
|
||||
|
||||
**Spotify**
|
||||
|
||||
- Let GStreamer handle time position tracking and seeks. (Fixes: :issue:`191`)
|
||||
|
||||
|
||||
v0.11.0 (2012-12-24)
|
||||
====================
|
||||
|
||||
In celebration of Mopidy's three year anniversary December 23, we're releasing
|
||||
Mopidy 0.11. This release brings several improvements, most notably better
|
||||
search which now includes matching artists and albums from Spotify in the
|
||||
search results.
|
||||
|
||||
**Settings**
|
||||
|
||||
- The settings validator now complains if a setting which expects a tuple of
|
||||
values (e.g. :attr:`mopidy.settings.BACKENDS`,
|
||||
:attr:`mopidy.settings.FRONTENDS`) has a non-iterable value. This typically
|
||||
happens because the setting value contains a single value and one has
|
||||
forgotten to add a comma after the string, making the value a tuple. (Fixes:
|
||||
:issue:`278`)
|
||||
|
||||
**Spotify backend**
|
||||
|
||||
- Add :attr:`mopidy.settings.SPOTIFY_TIMEOUT` setting which allows you to
|
||||
@ -61,14 +85,29 @@ v0.11.0 (in development)
|
||||
- Make ``seek`` and ``seekid`` not restart the current track before seeking in
|
||||
it.
|
||||
|
||||
- Include fake tracks representing albums and artists in the search results.
|
||||
When these are added to the tracklist, they expand to either all tracks in
|
||||
the album or all tracks by the artist. This makes it easy to play full albums
|
||||
in proper order, which is a feature that have been frequently requested.
|
||||
(Fixes: :issue:`67`, :issue:`148`)
|
||||
|
||||
**Internal changes**
|
||||
|
||||
*Models:*
|
||||
|
||||
- Specified that :attr:`mopidy.models.Playlist.last_modified` should be in UTC.
|
||||
|
||||
- Added :class:`mopidy.models.SearchResult` model to encapsulate search results
|
||||
consisting of more than just tracks.
|
||||
|
||||
*Core API:*
|
||||
|
||||
- Change the following methods to return :class:`mopidy.models.SearchResult`
|
||||
objects which can include both track results and other results:
|
||||
|
||||
- :meth:`mopidy.core.LibraryController.find_exact`
|
||||
- :meth:`mopidy.core.LibraryController.search`
|
||||
|
||||
- Change the following methods to accept either a dict with filters or kwargs.
|
||||
Previously they only accepted kwargs, which made them impossible to use from
|
||||
the Mopidy.js through JSON-RPC, which doesn't support kwargs.
|
||||
|
||||
9
fabfile.py
vendored
9
fabfile.py
vendored
@ -1,14 +1,15 @@
|
||||
from fabric.api import local
|
||||
|
||||
|
||||
def test():
|
||||
local('nosetests tests/')
|
||||
def test(path=None):
|
||||
path = path or 'tests/'
|
||||
local('nosetests ' + path)
|
||||
|
||||
|
||||
def autotest():
|
||||
def autotest(path=None):
|
||||
while True:
|
||||
local('clear')
|
||||
test()
|
||||
test(path)
|
||||
local(
|
||||
'inotifywait -q -e create -e modify -e delete '
|
||||
'--exclude ".*\.(pyc|sw.)" -r mopidy/ tests/')
|
||||
|
||||
@ -23,7 +23,7 @@ if (isinstance(pykka.__version__, basestring)
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.10.0'
|
||||
__version__ = '0.11.0'
|
||||
|
||||
|
||||
from mopidy import settings as default_settings_module
|
||||
|
||||
@ -19,7 +19,7 @@ from __future__ import unicode_literals
|
||||
import pykka
|
||||
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.models import Playlist, SearchResult
|
||||
|
||||
|
||||
class DummyBackend(pykka.ThreadingActor, base.Backend):
|
||||
@ -37,8 +37,8 @@ class DummyLibraryProvider(base.BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self.dummy_library = []
|
||||
self.dummy_find_exact_result = []
|
||||
self.dummy_search_result = []
|
||||
self.dummy_find_exact_result = SearchResult()
|
||||
self.dummy_search_result = SearchResult()
|
||||
|
||||
def find_exact(self, **query):
|
||||
return self.dummy_find_exact_result
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import Album
|
||||
from mopidy.models import Album, SearchResult
|
||||
|
||||
from .translator import parse_mpd_tag_cache
|
||||
|
||||
@ -70,7 +70,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
result_tracks = filter(any_filter, result_tracks)
|
||||
else:
|
||||
raise LookupError('Invalid lookup field: %s' % field)
|
||||
return result_tracks
|
||||
return SearchResult(uri='file:search', tracks=result_tracks)
|
||||
|
||||
def search(self, **query):
|
||||
self._validate_query(query)
|
||||
@ -107,7 +107,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
result_tracks = filter(any_filter, result_tracks)
|
||||
else:
|
||||
raise LookupError('Invalid lookup field: %s' % field)
|
||||
return result_tracks
|
||||
return SearchResult(uri='file:search', tracks=result_tracks)
|
||||
|
||||
def _validate_query(self, query):
|
||||
for (_, values) in query.iteritems():
|
||||
|
||||
@ -2,18 +2,21 @@ from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import time
|
||||
import urllib
|
||||
|
||||
import pykka
|
||||
from spotify import Link, SpotifyError
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import Track
|
||||
from mopidy.models import Track, SearchResult
|
||||
|
||||
from . import translator
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify')
|
||||
|
||||
TRACK_AVAILABLE = 1
|
||||
|
||||
|
||||
class SpotifyTrack(Track):
|
||||
"""Proxy object for unloaded Spotify tracks."""
|
||||
@ -83,7 +86,10 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
track = Link.from_string(uri).as_track()
|
||||
self._wait_for_object_to_load(track)
|
||||
if track.is_loaded():
|
||||
return [SpotifyTrack(track=track)]
|
||||
if track.availability() == TRACK_AVAILABLE:
|
||||
return [SpotifyTrack(track=track)]
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
return [SpotifyTrack(uri=uri)]
|
||||
|
||||
@ -91,18 +97,24 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
album = Link.from_string(uri).as_album()
|
||||
album_browser = self.backend.spotify.session.browse_album(album)
|
||||
self._wait_for_object_to_load(album_browser)
|
||||
return [SpotifyTrack(track=t) for t in album_browser]
|
||||
return [
|
||||
SpotifyTrack(track=t)
|
||||
for t in album_browser if t.availability() == TRACK_AVAILABLE]
|
||||
|
||||
def _lookup_artist(self, uri):
|
||||
artist = Link.from_string(uri).as_artist()
|
||||
artist_browser = self.backend.spotify.session.browse_artist(artist)
|
||||
self._wait_for_object_to_load(artist_browser)
|
||||
return [SpotifyTrack(track=t) for t in artist_browser]
|
||||
return [
|
||||
SpotifyTrack(track=t)
|
||||
for t in artist_browser if t.availability() == TRACK_AVAILABLE]
|
||||
|
||||
def _lookup_playlist(self, uri):
|
||||
playlist = Link.from_string(uri).as_playlist()
|
||||
self._wait_for_object_to_load(playlist)
|
||||
return [SpotifyTrack(track=t) for t in playlist]
|
||||
return [
|
||||
SpotifyTrack(track=t)
|
||||
for t in playlist if t.availability() == TRACK_AVAILABLE]
|
||||
|
||||
def _wait_for_object_to_load(
|
||||
self, spotify_obj, timeout=settings.SPOTIFY_TIMEOUT):
|
||||
@ -123,12 +135,16 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
if not query:
|
||||
return self._get_all_tracks()
|
||||
|
||||
if 'uri' in query.keys():
|
||||
result = []
|
||||
for uri in query['uri']:
|
||||
tracks = self.lookup(uri)
|
||||
result += tracks
|
||||
return result
|
||||
uris = query.get('uri', [])
|
||||
if uris:
|
||||
tracks = []
|
||||
for uri in uris:
|
||||
tracks += self.lookup(uri)
|
||||
if len(uris) == 1:
|
||||
uri = uris[0]
|
||||
else:
|
||||
uri = 'spotify:search'
|
||||
return SearchResult(uri=uri, tracks=tracks)
|
||||
|
||||
spotify_query = self._translate_search_query(query)
|
||||
logger.debug('Spotify search query: %s' % spotify_query)
|
||||
@ -136,20 +152,24 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
future = pykka.ThreadingFuture()
|
||||
|
||||
def callback(results, userdata=None):
|
||||
# TODO Include results from results.albums(), etc. too
|
||||
# TODO Consider launching a second search if results.total_tracks()
|
||||
# is larger than len(results.tracks())
|
||||
tracks = [
|
||||
translator.to_mopidy_track(t) for t in results.tracks()]
|
||||
future.set(tracks)
|
||||
search_result = SearchResult(
|
||||
uri='spotify:search:%s' % (
|
||||
urllib.quote(results.query().encode('utf-8'))),
|
||||
albums=[
|
||||
translator.to_mopidy_album(a) for a in results.albums()],
|
||||
artists=[
|
||||
translator.to_mopidy_artist(a) for a in results.artists()],
|
||||
tracks=[
|
||||
translator.to_mopidy_track(t) for t in results.tracks()])
|
||||
future.set(search_result)
|
||||
|
||||
if not self.backend.spotify.connected.wait(settings.SPOTIFY_TIMEOUT):
|
||||
logger.debug('Not connected: Spotify search cancelled')
|
||||
return []
|
||||
return SearchResult(uri='spotify:search')
|
||||
|
||||
self.backend.spotify.session.search(
|
||||
spotify_query, callback,
|
||||
track_count=200, album_count=0, artist_count=0)
|
||||
album_count=200, artist_count=200, track_count=200)
|
||||
|
||||
try:
|
||||
return future.get(timeout=settings.SPOTIFY_TIMEOUT)
|
||||
@ -157,7 +177,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
logger.debug(
|
||||
'Timeout: Spotify search did not return in %ds',
|
||||
settings.SPOTIFY_TIMEOUT)
|
||||
return []
|
||||
return SearchResult(uri='spotify:search')
|
||||
|
||||
def _get_all_tracks(self):
|
||||
# Since we can't search for the entire Spotify library, we return
|
||||
@ -165,7 +185,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
|
||||
tracks = []
|
||||
for playlist in self.backend.playlists.playlists:
|
||||
tracks += playlist.tracks
|
||||
return tracks
|
||||
return SearchResult(uri='spotify:search', tracks=tracks)
|
||||
|
||||
def _translate_search_query(self, mopidy_query):
|
||||
spotify_query = []
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import urlparse
|
||||
|
||||
import pykka
|
||||
@ -37,13 +36,12 @@ class LibraryController(object):
|
||||
|
||||
:param query: one or more queries to search for
|
||||
:type query: dict
|
||||
:rtype: list of :class:`mopidy.models.Track`
|
||||
:rtype: list of :class:`mopidy.models.SearchResult`
|
||||
"""
|
||||
query = query or kwargs
|
||||
futures = [
|
||||
b.library.find_exact(**query) for b in self.backends.with_library]
|
||||
results = pykka.get_all(futures)
|
||||
return list(itertools.chain(*results))
|
||||
return pykka.get_all(futures)
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
@ -98,10 +96,9 @@ class LibraryController(object):
|
||||
|
||||
:param query: one or more queries to search for
|
||||
:type query: dict
|
||||
:rtype: list of :class:`mopidy.models.Track`
|
||||
:rtype: list of :class:`mopidy.models.SearchResult`
|
||||
"""
|
||||
query = query or kwargs
|
||||
futures = [
|
||||
b.library.search(**query) for b in self.backends.with_library]
|
||||
results = pykka.get_all(futures)
|
||||
return list(itertools.chain(*results))
|
||||
return pykka.get_all(futures)
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
|
||||
from mopidy.models import Track
|
||||
from mopidy.frontends.mpd import translator
|
||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
|
||||
@ -10,6 +14,31 @@ QUERY_RE = (
|
||||
r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$')
|
||||
|
||||
|
||||
def _get_field(field, search_results):
|
||||
return list(itertools.chain(*[getattr(r, field) for r in search_results]))
|
||||
|
||||
|
||||
_get_albums = functools.partial(_get_field, 'albums')
|
||||
_get_artists = functools.partial(_get_field, 'artists')
|
||||
_get_tracks = functools.partial(_get_field, 'tracks')
|
||||
|
||||
|
||||
def _album_as_track(album):
|
||||
return Track(
|
||||
uri=album.uri,
|
||||
name='Album: ' + album.name,
|
||||
artists=album.artists,
|
||||
album=album,
|
||||
date=album.date)
|
||||
|
||||
|
||||
def _artist_as_track(artist):
|
||||
return Track(
|
||||
uri=artist.uri,
|
||||
name='Artist: ' + artist.name,
|
||||
artists=[artist])
|
||||
|
||||
|
||||
@handle_request(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
|
||||
def count(context, tag, needle):
|
||||
"""
|
||||
@ -55,8 +84,14 @@ def find(context, mpd_query):
|
||||
query = translator.query_from_mpd_search_format(mpd_query)
|
||||
except ValueError:
|
||||
return
|
||||
result = context.core.library.find_exact(**query).get()
|
||||
return translator.tracks_to_mpd_format(result)
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
result_tracks = []
|
||||
if 'artist' not in query:
|
||||
result_tracks += [_artist_as_track(a) for a in _get_artists(results)]
|
||||
if 'album' not in query:
|
||||
result_tracks += [_album_as_track(a) for a in _get_albums(results)]
|
||||
result_tracks += _get_tracks(results)
|
||||
return translator.tracks_to_mpd_format(result_tracks)
|
||||
|
||||
|
||||
@handle_request(r'^findadd ' + QUERY_RE)
|
||||
@ -73,8 +108,8 @@ def findadd(context, mpd_query):
|
||||
query = translator.query_from_mpd_search_format(mpd_query)
|
||||
except ValueError:
|
||||
return
|
||||
result = context.core.library.find_exact(**query).get()
|
||||
context.core.tracklist.add(result)
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
context.core.tracklist.add(_get_tracks(results))
|
||||
|
||||
|
||||
@handle_request(
|
||||
@ -179,8 +214,8 @@ def list_(context, field, mpd_query=None):
|
||||
|
||||
def _list_artist(context, query):
|
||||
artists = set()
|
||||
tracks = context.core.library.find_exact(**query).get()
|
||||
for track in tracks:
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
for artist in track.artists:
|
||||
if artist.name:
|
||||
artists.add(('Artist', artist.name))
|
||||
@ -189,8 +224,8 @@ def _list_artist(context, query):
|
||||
|
||||
def _list_album(context, query):
|
||||
albums = set()
|
||||
tracks = context.core.library.find_exact(**query).get()
|
||||
for track in tracks:
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
if track.album and track.album.name:
|
||||
albums.add(('Album', track.album.name))
|
||||
return albums
|
||||
@ -198,8 +233,8 @@ def _list_album(context, query):
|
||||
|
||||
def _list_date(context, query):
|
||||
dates = set()
|
||||
tracks = context.core.library.find_exact(**query).get()
|
||||
for track in tracks:
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
if track.date:
|
||||
dates.add(('Date', track.date))
|
||||
return dates
|
||||
@ -297,8 +332,11 @@ def search(context, mpd_query):
|
||||
query = translator.query_from_mpd_search_format(mpd_query)
|
||||
except ValueError:
|
||||
return
|
||||
result = context.core.library.search(**query).get()
|
||||
return translator.tracks_to_mpd_format(result)
|
||||
results = context.core.library.search(**query).get()
|
||||
artists = [_artist_as_track(a) for a in _get_artists(results)]
|
||||
albums = [_album_as_track(a) for a in _get_albums(results)]
|
||||
tracks = _get_tracks(results)
|
||||
return translator.tracks_to_mpd_format(artists + albums + tracks)
|
||||
|
||||
|
||||
@handle_request(r'^searchadd ' + QUERY_RE)
|
||||
@ -318,8 +356,8 @@ def searchadd(context, mpd_query):
|
||||
query = translator.query_from_mpd_search_format(mpd_query)
|
||||
except ValueError:
|
||||
return
|
||||
result = context.core.library.search(**query).get()
|
||||
context.core.tracklist.add(result)
|
||||
results = context.core.library.search(**query).get()
|
||||
context.core.tracklist.add(_get_tracks(results))
|
||||
|
||||
|
||||
@handle_request(r'^searchaddpl "(?P<playlist_name>[^"]+)" ' + QUERY_RE)
|
||||
@ -341,14 +379,14 @@ def searchaddpl(context, playlist_name, mpd_query):
|
||||
query = translator.query_from_mpd_search_format(mpd_query)
|
||||
except ValueError:
|
||||
return
|
||||
result = context.core.library.search(**query).get()
|
||||
results = context.core.library.search(**query).get()
|
||||
|
||||
playlists = context.core.playlists.filter(name=playlist_name).get()
|
||||
if playlists:
|
||||
playlist = playlists[0]
|
||||
else:
|
||||
playlist = context.core.playlists.create(playlist_name).get()
|
||||
tracks = list(playlist.tracks) + result
|
||||
tracks = list(playlist.tracks) + _get_tracks(results)
|
||||
playlist = playlist.copy(tracks=tracks)
|
||||
context.core.playlists.save(playlist)
|
||||
|
||||
|
||||
@ -329,8 +329,7 @@ def seek(context, songpos, seconds):
|
||||
|
||||
- issues ``seek 1 120`` without quotes around the arguments.
|
||||
"""
|
||||
songpos = int(songpos)
|
||||
if context.core.playback.tracklist_position.get() != songpos:
|
||||
if context.core.playback.tracklist_position.get() != int(songpos):
|
||||
playpos(context, songpos)
|
||||
context.core.playback.seek(int(seconds) * 1000).get()
|
||||
|
||||
@ -344,9 +343,8 @@ def seekid(context, tlid, seconds):
|
||||
|
||||
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
|
||||
"""
|
||||
tlid = int(tlid)
|
||||
tl_track = context.core.playback.current_tl_track.get()
|
||||
if not tl_track or tl_track.tlid != tlid:
|
||||
if not tl_track or tl_track.tlid != int(tlid):
|
||||
playid(context, tlid)
|
||||
context.core.playback.seek(int(seconds) * 1000).get()
|
||||
|
||||
|
||||
@ -318,3 +318,34 @@ class Playlist(ImmutableObject):
|
||||
def length(self):
|
||||
"""The number of tracks in the playlist. Read-only."""
|
||||
return len(self.tracks)
|
||||
|
||||
|
||||
class SearchResult(ImmutableObject):
|
||||
"""
|
||||
:param uri: search result URI
|
||||
:type uri: string
|
||||
:param tracks: matching tracks
|
||||
:type tracks: list of :class:`Track` elements
|
||||
:param artists: matching artists
|
||||
:type artists: list of :class:`Artist` elements
|
||||
:param albums: matching albums
|
||||
:type albums: list of :class:`Album` elements
|
||||
"""
|
||||
|
||||
# The search result URI. Read-only.
|
||||
uri = None
|
||||
|
||||
# The tracks matching the search query. Read-only.
|
||||
tracks = tuple()
|
||||
|
||||
# The artists matching the search query. Read-only.
|
||||
artists = tuple()
|
||||
|
||||
# The albums matching the search query. Read-only.
|
||||
albums = tuple()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__['tracks'] = tuple(kwargs.pop('tracks', []))
|
||||
self.__dict__['artists'] = tuple(kwargs.pop('artists', []))
|
||||
self.__dict__['albums'] = tuple(kwargs.pop('albums', []))
|
||||
super(SearchResult, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -172,6 +172,10 @@ def validate_settings(defaults, settings):
|
||||
'bin in OUTPUT.')
|
||||
|
||||
elif setting in list_of_one_or_more:
|
||||
if not hasattr(value, '__iter__'):
|
||||
errors[setting] = (
|
||||
'Must be a tuple. '
|
||||
"Remember the comma after single values: (u'value',)")
|
||||
if not value:
|
||||
errors[setting] = 'Must contain at least one value.'
|
||||
|
||||
|
||||
@ -53,53 +53,53 @@ class LibraryControllerTest(object):
|
||||
|
||||
def test_find_exact_no_hits(self):
|
||||
result = self.library.find_exact(track=['unknown track'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
result = self.library.find_exact(artist=['unknown artist'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
result = self.library.find_exact(album=['unknown artist'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
def test_find_exact_uri(self):
|
||||
track_1_uri = 'file://' + path_to_data_dir('uri1')
|
||||
result = self.library.find_exact(uri=track_1_uri)
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
track_2_uri = 'file://' + path_to_data_dir('uri2')
|
||||
result = self.library.find_exact(uri=track_2_uri)
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_find_exact_track(self):
|
||||
result = self.library.find_exact(track=['track1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.find_exact(track=['track2'])
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_find_exact_artist(self):
|
||||
result = self.library.find_exact(artist=['artist1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.find_exact(artist=['artist2'])
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_find_exact_album(self):
|
||||
result = self.library.find_exact(album=['album1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.find_exact(album=['album2'])
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_find_exact_date(self):
|
||||
result = self.library.find_exact(date=['2001'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
result = self.library.find_exact(date=['2001-02-03'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.find_exact(date=['2002'])
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_find_exact_wrong_type(self):
|
||||
test = lambda: self.library.find_exact(wrong=['test'])
|
||||
@ -117,70 +117,70 @@ class LibraryControllerTest(object):
|
||||
|
||||
def test_search_no_hits(self):
|
||||
result = self.library.search(track=['unknown track'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
result = self.library.search(artist=['unknown artist'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
result = self.library.search(album=['unknown artist'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
result = self.library.search(uri=['unknown'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
result = self.library.search(any=['unknown'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
def test_search_uri(self):
|
||||
result = self.library.search(uri=['RI1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.search(uri=['RI2'])
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_search_track(self):
|
||||
result = self.library.search(track=['Rack1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.search(track=['Rack2'])
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_search_artist(self):
|
||||
result = self.library.search(artist=['Tist1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.search(artist=['Tist2'])
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_search_album(self):
|
||||
result = self.library.search(album=['Bum1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.search(album=['Bum2'])
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_search_date(self):
|
||||
result = self.library.search(date=['2001'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.search(date=['2001-02-03'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
result = self.library.search(date=['2001-02-04'])
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(list(result[0].tracks), [])
|
||||
|
||||
result = self.library.search(date=['2002'])
|
||||
self.assertEqual(result, self.tracks[1:2])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
|
||||
|
||||
def test_search_any(self):
|
||||
result = self.library.search(any=['Tist1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
result = self.library.search(any=['Rack1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
result = self.library.search(any=['Bum1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
result = self.library.search(any=['RI1'])
|
||||
self.assertEqual(result, self.tracks[:1])
|
||||
self.assertEqual(list(result[0].tracks), self.tracks[:1])
|
||||
|
||||
def test_search_wrong_type(self):
|
||||
test = lambda: self.library.search(wrong=['test'])
|
||||
|
||||
@ -4,7 +4,7 @@ import mock
|
||||
|
||||
from mopidy.backends import base
|
||||
from mopidy.core import Core
|
||||
from mopidy.models import Track
|
||||
from mopidy.models import SearchResult, Track
|
||||
|
||||
from tests import unittest
|
||||
|
||||
@ -75,59 +75,71 @@ class CoreLibraryTest(unittest.TestCase):
|
||||
def test_find_exact_combines_results_from_all_backends(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
track2 = Track(uri='dummy2:a')
|
||||
self.library1.find_exact().get.return_value = [track1]
|
||||
result1 = SearchResult(tracks=[track1])
|
||||
result2 = SearchResult(tracks=[track2])
|
||||
|
||||
self.library1.find_exact().get.return_value = result1
|
||||
self.library1.find_exact.reset_mock()
|
||||
self.library2.find_exact().get.return_value = [track2]
|
||||
self.library2.find_exact().get.return_value = result2
|
||||
self.library2.find_exact.reset_mock()
|
||||
|
||||
result = self.core.library.find_exact(any=['a'])
|
||||
|
||||
self.assertIn(track1, result)
|
||||
self.assertIn(track2, result)
|
||||
self.assertIn(result1, result)
|
||||
self.assertIn(result2, result)
|
||||
self.library1.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library2.find_exact.assert_called_once_with(any=['a'])
|
||||
|
||||
def test_find_accepts_query_dict_instead_of_kwargs(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
track2 = Track(uri='dummy2:a')
|
||||
self.library1.find_exact().get.return_value = [track1]
|
||||
result1 = SearchResult(tracks=[track1])
|
||||
result2 = SearchResult(tracks=[track2])
|
||||
|
||||
self.library1.find_exact().get.return_value = result1
|
||||
self.library1.find_exact.reset_mock()
|
||||
self.library2.find_exact().get.return_value = [track2]
|
||||
self.library2.find_exact().get.return_value = result2
|
||||
self.library2.find_exact.reset_mock()
|
||||
|
||||
result = self.core.library.find_exact(dict(any=['a']))
|
||||
|
||||
self.assertIn(track1, result)
|
||||
self.assertIn(track2, result)
|
||||
self.assertIn(result1, result)
|
||||
self.assertIn(result2, result)
|
||||
self.library1.find_exact.assert_called_once_with(any=['a'])
|
||||
self.library2.find_exact.assert_called_once_with(any=['a'])
|
||||
|
||||
def test_search_combines_results_from_all_backends(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
track2 = Track(uri='dummy2:a')
|
||||
self.library1.search().get.return_value = [track1]
|
||||
result1 = SearchResult(tracks=[track1])
|
||||
result2 = SearchResult(tracks=[track2])
|
||||
|
||||
self.library1.search().get.return_value = result1
|
||||
self.library1.search.reset_mock()
|
||||
self.library2.search().get.return_value = [track2]
|
||||
self.library2.search().get.return_value = result2
|
||||
self.library2.search.reset_mock()
|
||||
|
||||
result = self.core.library.search(any=['a'])
|
||||
|
||||
self.assertIn(track1, result)
|
||||
self.assertIn(track2, result)
|
||||
self.assertIn(result1, result)
|
||||
self.assertIn(result2, result)
|
||||
self.library1.search.assert_called_once_with(any=['a'])
|
||||
self.library2.search.assert_called_once_with(any=['a'])
|
||||
|
||||
def test_search_accepts_query_dict_instead_of_kwargs(self):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
track2 = Track(uri='dummy2:a')
|
||||
self.library1.search().get.return_value = [track1]
|
||||
result1 = SearchResult(tracks=[track1])
|
||||
result2 = SearchResult(tracks=[track2])
|
||||
|
||||
self.library1.search().get.return_value = result1
|
||||
self.library1.search.reset_mock()
|
||||
self.library2.search().get.return_value = [track2]
|
||||
self.library2.search().get.return_value = result2
|
||||
self.library2.search.reset_mock()
|
||||
|
||||
result = self.core.library.search(dict(any=['a']))
|
||||
|
||||
self.assertIn(track1, result)
|
||||
self.assertIn(track2, result)
|
||||
self.assertIn(result1, result)
|
||||
self.assertIn(result2, result)
|
||||
self.library1.search.assert_called_once_with(any=['a'])
|
||||
self.library2.search.assert_called_once_with(any=['a'])
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mopidy.models import Album, Artist, Track
|
||||
from mopidy.models import Album, Artist, SearchResult, Track
|
||||
|
||||
from tests.frontends.mpd import protocol
|
||||
|
||||
@ -13,9 +13,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_findadd(self):
|
||||
self.backend.library.dummy_find_exact_result = [
|
||||
Track(uri='dummy:a', name='A'),
|
||||
]
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
tracks=[Track(uri='dummy:a', name='A')])
|
||||
self.assertEqual(self.core.tracklist.length.get(), 0)
|
||||
|
||||
self.sendRequest('findadd "title" "A"')
|
||||
@ -25,9 +24,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_searchadd(self):
|
||||
self.backend.library.dummy_search_result = [
|
||||
Track(uri='dummy:a', name='A'),
|
||||
]
|
||||
self.backend.library.dummy_search_result = SearchResult(
|
||||
tracks=[Track(uri='dummy:a', name='A')])
|
||||
self.assertEqual(self.core.tracklist.length.get(), 0)
|
||||
|
||||
self.sendRequest('searchadd "title" "a"')
|
||||
@ -43,9 +41,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
|
||||
Track(uri='dummy:y', name='y'),
|
||||
])
|
||||
self.core.playlists.save(playlist)
|
||||
self.backend.library.dummy_search_result = [
|
||||
Track(uri='dummy:a', name='A'),
|
||||
]
|
||||
self.backend.library.dummy_search_result = SearchResult(
|
||||
tracks=[Track(uri='dummy:a', name='A')])
|
||||
playlists = self.core.playlists.filter(name='my favs').get()
|
||||
self.assertEqual(len(playlists), 1)
|
||||
self.assertEqual(len(playlists[0].tracks), 2)
|
||||
@ -61,9 +58,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_searchaddpl_creates_missing_playlist(self):
|
||||
self.backend.library.dummy_search_result = [
|
||||
Track(uri='dummy:a', name='A'),
|
||||
]
|
||||
self.backend.library.dummy_search_result = SearchResult(
|
||||
tracks=[Track(uri='dummy:a', name='A')])
|
||||
self.assertEqual(
|
||||
len(self.core.playlists.filter(name='my favs').get()), 0)
|
||||
|
||||
@ -119,6 +115,66 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
|
||||
|
||||
|
||||
class MusicDatabaseFindTest(protocol.BaseTestCase):
|
||||
def test_find_includes_fake_artist_and_album_tracks(self):
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
albums=[Album(uri='dummy:album:a', name='A', date='2001')],
|
||||
artists=[Artist(uri='dummy:artist:b', name='B')],
|
||||
tracks=[Track(uri='dummy:track:c', name='C')])
|
||||
|
||||
self.sendRequest('find "any" "foo"')
|
||||
|
||||
self.assertInResponse('file: dummy:artist:b')
|
||||
self.assertInResponse('Title: Artist: B')
|
||||
|
||||
self.assertInResponse('file: dummy:album:a')
|
||||
self.assertInResponse('Title: Album: A')
|
||||
self.assertInResponse('Date: 2001')
|
||||
|
||||
self.assertInResponse('file: dummy:track:c')
|
||||
self.assertInResponse('Title: C')
|
||||
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_find_artist_does_not_include_fake_artist_tracks(self):
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
albums=[Album(uri='dummy:album:a', name='A', date='2001')],
|
||||
artists=[Artist(uri='dummy:artist:b', name='B')],
|
||||
tracks=[Track(uri='dummy:track:c', name='C')])
|
||||
|
||||
self.sendRequest('find "artist" "foo"')
|
||||
|
||||
self.assertNotInResponse('file: dummy:artist:b')
|
||||
self.assertNotInResponse('Title: Artist: B')
|
||||
|
||||
self.assertInResponse('file: dummy:album:a')
|
||||
self.assertInResponse('Title: Album: A')
|
||||
self.assertInResponse('Date: 2001')
|
||||
|
||||
self.assertInResponse('file: dummy:track:c')
|
||||
self.assertInResponse('Title: C')
|
||||
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_find_artist_and_album_does_not_include_fake_tracks(self):
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
albums=[Album(uri='dummy:album:a', name='A', date='2001')],
|
||||
artists=[Artist(uri='dummy:artist:b', name='B')],
|
||||
tracks=[Track(uri='dummy:track:c', name='C')])
|
||||
|
||||
self.sendRequest('find "artist" "foo" "album" "bar"')
|
||||
|
||||
self.assertNotInResponse('file: dummy:artist:b')
|
||||
self.assertNotInResponse('Title: Artist: B')
|
||||
|
||||
self.assertNotInResponse('file: dummy:album:a')
|
||||
self.assertNotInResponse('Title: Album: A')
|
||||
self.assertNotInResponse('Date: 2001')
|
||||
|
||||
self.assertInResponse('file: dummy:track:c')
|
||||
self.assertInResponse('Title: C')
|
||||
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_find_album(self):
|
||||
self.sendRequest('find "album" "what"')
|
||||
self.assertInResponse('OK')
|
||||
@ -185,6 +241,17 @@ class MusicDatabaseFindTest(protocol.BaseTestCase):
|
||||
|
||||
|
||||
class MusicDatabaseListTest(protocol.BaseTestCase):
|
||||
def test_list(self):
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
tracks=[
|
||||
Track(uri='dummy:a', name='A', artists=[
|
||||
Artist(name='A Artist')])])
|
||||
|
||||
self.sendRequest('list "artist" "artist" "foo"')
|
||||
|
||||
self.assertInResponse('Artist: A Artist')
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_list_foo_returns_ack(self):
|
||||
self.sendRequest('list "foo"')
|
||||
self.assertEqualResponse('ACK [2@0] {list} incorrect arguments')
|
||||
@ -242,8 +309,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_list_artist_should_not_return_artists_without_names(self):
|
||||
self.backend.library.dummy_find_exact_result = [
|
||||
Track(artists=[Artist(name='')])]
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
tracks=[Track(artists=[Artist(name='')])])
|
||||
|
||||
self.sendRequest('list "artist"')
|
||||
self.assertNotInResponse('Artist: ')
|
||||
@ -301,8 +368,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_list_album_should_not_return_albums_without_names(self):
|
||||
self.backend.library.dummy_find_exact_result = [
|
||||
Track(album=Album(name=''))]
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
tracks=[Track(album=Album(name=''))])
|
||||
|
||||
self.sendRequest('list "album"')
|
||||
self.assertNotInResponse('Album: ')
|
||||
@ -356,7 +423,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_list_date_should_not_return_blank_dates(self):
|
||||
self.backend.library.dummy_find_exact_result = [Track(date='')]
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
tracks=[Track(date='')])
|
||||
|
||||
self.sendRequest('list "date"')
|
||||
self.assertNotInResponse('Date: ')
|
||||
@ -412,6 +480,23 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
|
||||
|
||||
|
||||
class MusicDatabaseSearchTest(protocol.BaseTestCase):
|
||||
def test_search(self):
|
||||
self.backend.library.dummy_search_result = SearchResult(
|
||||
albums=[Album(uri='dummy:album:a', name='A')],
|
||||
artists=[Artist(uri='dummy:artist:b', name='B')],
|
||||
tracks=[Track(uri='dummy:track:c', name='C')])
|
||||
|
||||
self.sendRequest('search "any" "foo"')
|
||||
|
||||
self.assertInResponse('file: dummy:album:a')
|
||||
self.assertInResponse('Title: Album: A')
|
||||
self.assertInResponse('file: dummy:artist:b')
|
||||
self.assertInResponse('Title: Artist: B')
|
||||
self.assertInResponse('file: dummy:track:c')
|
||||
self.assertInResponse('Title: C')
|
||||
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_search_album(self):
|
||||
self.sendRequest('search "album" "analbum"')
|
||||
self.assertInResponse('OK')
|
||||
|
||||
@ -4,7 +4,7 @@ import datetime
|
||||
import json
|
||||
|
||||
from mopidy.models import (
|
||||
Artist, Album, TlTrack, Track, Playlist,
|
||||
Artist, Album, TlTrack, Track, Playlist, SearchResult,
|
||||
ModelJSONEncoder, model_json_decoder)
|
||||
|
||||
from tests import unittest
|
||||
@ -862,10 +862,56 @@ class PlaylistTest(unittest.TestCase):
|
||||
|
||||
def test_ne(self):
|
||||
playlist1 = Playlist(
|
||||
uri='uri1', name='name2', tracks=[Track(uri='uri1')],
|
||||
uri='uri1', name='name1', tracks=[Track(uri='uri1')],
|
||||
last_modified=1)
|
||||
playlist2 = Playlist(
|
||||
uri='uri2', name='name2', tracks=[Track(uri='uri2')],
|
||||
last_modified=2)
|
||||
self.assertNotEqual(playlist1, playlist2)
|
||||
self.assertNotEqual(hash(playlist1), hash(playlist2))
|
||||
|
||||
|
||||
class SearchResultTest(unittest.TestCase):
|
||||
def test_uri(self):
|
||||
uri = 'an_uri'
|
||||
result = SearchResult(uri=uri)
|
||||
self.assertEqual(result.uri, uri)
|
||||
self.assertRaises(AttributeError, setattr, result, 'uri', None)
|
||||
|
||||
def test_tracks(self):
|
||||
tracks = [Track(), Track(), Track()]
|
||||
result = SearchResult(tracks=tracks)
|
||||
self.assertEqual(list(result.tracks), tracks)
|
||||
self.assertRaises(AttributeError, setattr, result, 'tracks', None)
|
||||
|
||||
def test_artists(self):
|
||||
artists = [Artist(), Artist(), Artist()]
|
||||
result = SearchResult(artists=artists)
|
||||
self.assertEqual(list(result.artists), artists)
|
||||
self.assertRaises(AttributeError, setattr, result, 'artists', None)
|
||||
|
||||
def test_albums(self):
|
||||
albums = [Album(), Album(), Album()]
|
||||
result = SearchResult(albums=albums)
|
||||
self.assertEqual(list(result.albums), albums)
|
||||
self.assertRaises(AttributeError, setattr, result, 'albums', None)
|
||||
|
||||
def test_invalid_kwarg(self):
|
||||
test = lambda: SearchResult(foo='baz')
|
||||
self.assertRaises(TypeError, test)
|
||||
|
||||
def test_repr_without_results(self):
|
||||
self.assertEquals(
|
||||
"SearchResult(albums=[], artists=[], tracks=[], uri=u'uri')",
|
||||
repr(SearchResult(uri='uri')))
|
||||
|
||||
def test_serialize_without_results(self):
|
||||
self.assertDictEqual(
|
||||
{'__model__': 'SearchResult', 'uri': 'uri'},
|
||||
SearchResult(uri='uri').serialize())
|
||||
|
||||
def test_to_json_and_back(self):
|
||||
result1 = SearchResult(uri='uri')
|
||||
serialized = json.dumps(result1, cls=ModelJSONEncoder)
|
||||
result2 = json.loads(serialized, object_hook=model_json_decoder)
|
||||
self.assertEqual(result1, result2)
|
||||
|
||||
@ -87,6 +87,14 @@ class ValidateSettingsTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
result['BACKENDS'], 'Must contain at least one value.')
|
||||
|
||||
def test_noniterable_multivalue_setting_returns_error(self):
|
||||
result = setting_utils.validate_settings(
|
||||
self.defaults, {'FRONTENDS': ('this is not a tuple')})
|
||||
self.assertEqual(
|
||||
result['FRONTENDS'],
|
||||
'Must be a tuple. '
|
||||
"Remember the comma after single values: (u'value',)")
|
||||
|
||||
|
||||
class SettingsProxyTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
@ -32,5 +32,6 @@ class VersionTest(unittest.TestCase):
|
||||
self.assertLess(SV('0.7.3'), SV('0.8.0'))
|
||||
self.assertLess(SV('0.8.0'), SV('0.8.1'))
|
||||
self.assertLess(SV('0.8.1'), SV('0.9.0'))
|
||||
self.assertLess(SV('0.9.0'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.10.1'))
|
||||
self.assertLess(SV('0.9.0'), SV('0.10.0'))
|
||||
self.assertLess(SV('0.10.0'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.11.1'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user