core: Validate core API calls

This commit is contained in:
Thomas Adamcik 2015-04-14 23:20:06 +02:00
parent 8646ba4252
commit 324bec1f4a
9 changed files with 105 additions and 64 deletions

View File

@ -5,8 +5,7 @@ import logging
import operator import operator
import urlparse import urlparse
from mopidy.utils import deprecation, validation
from mopidy.utils import deprecation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -70,6 +69,9 @@ class LibraryController(object):
""" """
if uri is None: if uri is None:
return self._roots() return self._roots()
elif not uri.strip():
return []
validation.check_uri(uri)
return self._browse(uri) return self._browse(uri)
def _roots(self): def _roots(self):
@ -111,6 +113,9 @@ class LibraryController(object):
.. versionadded:: 1.0 .. versionadded:: 1.0
""" """
validation.check_choice(field, validation.DISTINCT_FIELDS)
query is None or validation.check_query(query) # TODO: normalize?
result = set() result = set()
futures = {b: b.library.get_distinct(field, query) futures = {b: b.library.get_distinct(field, query)
for b in self.backends.with_library.values()} for b in self.backends.with_library.values()}
@ -137,6 +142,8 @@ class LibraryController(object):
.. versionadded:: 1.0 .. versionadded:: 1.0
""" """
validation.check_uris(uris)
futures = { futures = {
backend: backend.library.get_images(backend_uris) backend: backend.library.get_images(backend_uris)
for (backend, backend_uris) for (backend, backend_uris)
@ -187,6 +194,11 @@ class LibraryController(object):
if none_set or both_set: if none_set or both_set:
raise ValueError("One of 'uri' or 'uris' must be set") raise ValueError("One of 'uri' or 'uris' must be set")
# TODO: validation.one_of(*args)?
uris is None or validation.check_uris(uris)
uri is None or validation.check_uri(uri)
if uri: if uri:
deprecation.warn('core.library.lookup:uri_arg') deprecation.warn('core.library.lookup:uri_arg')
@ -219,6 +231,8 @@ class LibraryController(object):
:param uri: directory or track URI :param uri: directory or track URI
:type uri: string :type uri: string
""" """
uri is None or validation.check_uri(uri)
futures = {} futures = {}
backends = {} backends = {}
uri_scheme = urlparse.urlparse(uri).scheme if uri else None uri_scheme = urlparse.urlparse(uri).scheme if uri else None
@ -285,6 +299,10 @@ class LibraryController(object):
""" """
query = _normalize_query(query or kwargs) query = _normalize_query(query or kwargs)
uris is None or validation.check_uris(uris)
query is None or validation.check_query(query)
validation.check_boolean(exact)
if kwargs: if kwargs:
deprecation.warn('core.library.search:kwargs_query') deprecation.warn('core.library.search:kwargs_query')
@ -319,6 +337,7 @@ class LibraryController(object):
def _normalize_query(query): def _normalize_query(query):
broken_client = False broken_client = False
# TODO: this breaks if query is not a dictionary like object...
for (field, values) in query.items(): for (field, values) in query.items():
if isinstance(values, basestring): if isinstance(values, basestring):
broken_client = True broken_client = True

View File

@ -2,6 +2,8 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
from mopidy.utils import validation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,6 +33,8 @@ class MixerController(object):
Returns :class:`True` if call is successful, otherwise :class:`False`. Returns :class:`True` if call is successful, otherwise :class:`False`.
""" """
validation.check_integer(volume, min=0, max=100)
if self._mixer is None: if self._mixer is None:
return False return False
else: else:
@ -52,6 +56,7 @@ class MixerController(object):
Returns :class:`True` if call is successful, otherwise :class:`False`. Returns :class:`True` if call is successful, otherwise :class:`False`.
""" """
validation.check_boolean(mute)
if self._mixer is None: if self._mixer is None:
return False return False
else: else:

View File

@ -3,9 +3,10 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
import urlparse import urlparse
from mopidy import models
from mopidy.audio import PlaybackState from mopidy.audio import PlaybackState
from mopidy.core import listener from mopidy.core import listener
from mopidy.utils import deprecation from mopidy.utils import deprecation, validation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -96,6 +97,8 @@ class PlaybackController(object):
"PAUSED" -> "PLAYING" [ label="resume" ] "PAUSED" -> "PLAYING" [ label="resume" ]
"PAUSED" -> "STOPPED" [ label="stop" ] "PAUSED" -> "STOPPED" [ label="stop" ]
""" """
validation.check_choice(new_state, validation.PLAYBACK_STATES)
(old_state, self._state) = (self.get_state(), new_state) (old_state, self._state) = (self.get_state(), new_state)
logger.debug('Changing state: %s -> %s', old_state, new_state) logger.debug('Changing state: %s -> %s', old_state, new_state)
@ -270,6 +273,7 @@ class PlaybackController(object):
:param tl_track: track to play :param tl_track: track to play
:type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
""" """
tl_track is None or validation.check_instance(tl_track, models.TlTrack)
self._play(tl_track, on_error_step=1) self._play(tl_track, on_error_step=1)
def _play(self, tl_track=None, on_error_step=1): def _play(self, tl_track=None, on_error_step=1):
@ -360,6 +364,8 @@ class PlaybackController(object):
:type time_position: int :type time_position: int
:rtype: :class:`True` if successful, else :class:`False` :rtype: :class:`True` if successful, else :class:`False`
""" """
validation.check_integer(time_position, min=0)
if not self.core.tracklist.tracks: if not self.core.tracklist.tracks:
return False return False

View File

@ -5,7 +5,7 @@ import urlparse
from mopidy.core import listener from mopidy.core import listener
from mopidy.models import Playlist from mopidy.models import Playlist
from mopidy.utils import deprecation from mopidy.utils import deprecation, validation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -62,6 +62,8 @@ class PlaylistsController(object):
.. versionadded:: 1.0 .. versionadded:: 1.0
""" """
validation.check_uri(uri)
uri_scheme = urlparse.urlparse(uri).scheme uri_scheme = urlparse.urlparse(uri).scheme
backend = self.backends.with_playlists.get(uri_scheme, None) backend = self.backends.with_playlists.get(uri_scheme, None)
if backend: if backend:
@ -139,6 +141,8 @@ class PlaylistsController(object):
:param uri: URI of the playlist to delete :param uri: URI of the playlist to delete
:type uri: string :type uri: string
""" """
validation.check_uri(uri)
uri_scheme = urlparse.urlparse(uri).scheme uri_scheme = urlparse.urlparse(uri).scheme
backend = self.backends.with_playlists.get(uri_scheme, None) backend = self.backends.with_playlists.get(uri_scheme, None)
if backend: if backend:
@ -172,6 +176,9 @@ class PlaylistsController(object):
deprecation.warn('core.playlists.filter') deprecation.warn('core.playlists.filter')
criteria = criteria or kwargs criteria = criteria or kwargs
validation.check_query(criteria, list_values=False)
# TODO: stop using self playlists
matches = self.playlists matches = self.playlists
for (key, value) in criteria.iteritems(): for (key, value) in criteria.iteritems():
matches = filter(lambda p: getattr(p, key) == value, matches) matches = filter(lambda p: getattr(p, key) == value, matches)
@ -207,6 +214,8 @@ class PlaylistsController(object):
:param uri_scheme: limit to the backend matching the URI scheme :param uri_scheme: limit to the backend matching the URI scheme
:type uri_scheme: string :type uri_scheme: string
""" """
# TODO: check: uri_scheme is None or uri_scheme?
futures = {} futures = {}
backends = {} backends = {}
playlists_loaded = False playlists_loaded = False
@ -251,8 +260,11 @@ class PlaylistsController(object):
:type playlist: :class:`mopidy.models.Playlist` :type playlist: :class:`mopidy.models.Playlist`
:rtype: :class:`mopidy.models.Playlist` or :class:`None` :rtype: :class:`mopidy.models.Playlist` or :class:`None`
""" """
validation.check_instance(playlist, Playlist)
if playlist.uri is None: if playlist.uri is None:
return return # TODO: log this problem?
uri_scheme = urlparse.urlparse(playlist.uri).scheme uri_scheme = urlparse.urlparse(playlist.uri).scheme
backend = self.backends.with_playlists.get(uri_scheme, None) backend = self.backends.with_playlists.get(uri_scheme, None)
if backend: if backend:

View File

@ -1,13 +1,11 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import collections
import logging import logging
import random import random
from mopidy import compat
from mopidy.core import listener from mopidy.core import listener
from mopidy.models import TlTrack from mopidy.models import TlTrack, Track
from mopidy.utils import deprecation from mopidy.utils import deprecation, validation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -93,6 +91,7 @@ class TracklistController(object):
:class:`False` :class:`False`
Tracks are not removed from the tracklist. Tracks are not removed from the tracklist.
""" """
validation.check_boolean(value)
if self.get_consume() != value: if self.get_consume() != value:
self._trigger_options_changed() self._trigger_options_changed()
return setattr(self, '_consume', value) return setattr(self, '_consume', value)
@ -121,7 +120,7 @@ class TracklistController(object):
:class:`False` :class:`False`
Tracks are played in the order of the tracklist. Tracks are played in the order of the tracklist.
""" """
validation.check_boolean(value)
if self.get_random() != value: if self.get_random() != value:
self._trigger_options_changed() self._trigger_options_changed()
if value: if value:
@ -157,7 +156,7 @@ class TracklistController(object):
:class:`False` :class:`False`
The tracklist is played once. The tracklist is played once.
""" """
validation.check_boolean(value)
if self.get_repeat() != value: if self.get_repeat() != value:
self._trigger_options_changed() self._trigger_options_changed()
return setattr(self, '_repeat', value) return setattr(self, '_repeat', value)
@ -188,6 +187,7 @@ class TracklistController(object):
:class:`False` :class:`False`
Playback continues after current song. Playback continues after current song.
""" """
validation.check_boolean(value)
if self.get_single() != value: if self.get_single() != value:
self._trigger_options_changed() self._trigger_options_changed()
return setattr(self, '_single', value) return setattr(self, '_single', value)
@ -205,9 +205,10 @@ class TracklistController(object):
The position of the given track in the tracklist. The position of the given track in the tracklist.
:param tl_track: the track to find the index of :param tl_track: the track to find the index of
:type tl_track: :class:`mopidy.models.TlTrack` :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:rtype: :class:`int` or :class:`None` :rtype: :class:`int` or :class:`None`
""" """
tl_track is None or validation.check_instance(tl_track, TlTrack)
try: try:
return self._tl_tracks.index(tl_track) return self._tl_tracks.index(tl_track)
except ValueError: except ValueError:
@ -223,6 +224,7 @@ class TracklistController(object):
:type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:rtype: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None`
""" """
tl_track is None or validation.check_instance(tl_track, TlTrack)
if self.get_single() and self.get_repeat(): if self.get_single() and self.get_repeat():
return tl_track return tl_track
elif self.get_single(): elif self.get_single():
@ -247,6 +249,7 @@ class TracklistController(object):
:type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:rtype: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None`
""" """
tl_track is None or validation.check_instance(tl_track, TlTrack)
if not self.get_tl_tracks(): if not self.get_tl_tracks():
return None return None
@ -288,6 +291,8 @@ class TracklistController(object):
:type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:rtype: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None`
""" """
tl_track is None or validation.check_instance(tl_track, TlTrack)
if self.get_repeat() or self.get_consume() or self.get_random(): if self.get_repeat() or self.get_consume() or self.get_random():
return tl_track return tl_track
@ -330,8 +335,13 @@ class TracklistController(object):
""" """
assert tracks is not None or uri is not None or uris is not None, \ assert tracks is not None or uri is not None or uris is not None, \
'tracks, uri or uris must be provided' 'tracks, uri or uris must be provided'
# TODO: check that only one of tracks uri and uris is set...
# TODO: can at_position be negative?
# TODO: assert that tracks are track instances tracks is None or validation.check_instances(tracks, Track)
uri is None or validation.check_uri(uri)
uris is None or validation.check_uris(uris)
validation.check_integer(at_position or 0)
if tracks: if tracks:
deprecation.warn('core.tracklist.add:tracks_arg') deprecation.warn('core.tracklist.add:tracks_arg')
@ -412,12 +422,11 @@ class TracklistController(object):
:rtype: list of :class:`mopidy.models.TlTrack` :rtype: list of :class:`mopidy.models.TlTrack`
""" """
criteria = criteria or kwargs criteria = criteria or kwargs
validation.check_query(criteria)
# TODO: deprecate kwargs
matches = self._tl_tracks matches = self._tl_tracks
for (key, values) in criteria.items(): for (key, values) in criteria.items():
if (not isinstance(values, collections.Iterable) or
isinstance(values, compat.string_types)):
# Fail hard if anyone is using the <0.17 calling style
raise ValueError('Filter values must be iterable: %r' % values)
if key == 'tlid': if key == 'tlid':
matches = [ct for ct in matches if ct.tlid in values] matches = [ct for ct in matches if ct.tlid in values]
else: else:
@ -443,6 +452,7 @@ class TracklistController(object):
tl_tracks = self._tl_tracks tl_tracks = self._tl_tracks
# TODO: use validation helpers?
assert start < end, 'start must be smaller than end' assert start < end, 'start must be smaller than end'
assert start >= 0, 'start must be at least zero' assert start >= 0, 'start must be at least zero'
assert end <= len(tl_tracks), \ assert end <= len(tl_tracks), \
@ -470,6 +480,7 @@ class TracklistController(object):
:type criteria: dict :type criteria: dict
:rtype: list of :class:`mopidy.models.TlTrack` that was removed :rtype: list of :class:`mopidy.models.TlTrack` that was removed
""" """
# TODO: deprecate kwargs
tl_tracks = self.filter(criteria, **kwargs) tl_tracks = self.filter(criteria, **kwargs)
for tl_track in tl_tracks: for tl_track in tl_tracks:
position = self._tl_tracks.index(tl_track) position = self._tl_tracks.index(tl_track)
@ -491,6 +502,7 @@ class TracklistController(object):
""" """
tl_tracks = self._tl_tracks tl_tracks = self._tl_tracks
# TOOD: use validation helpers?
if start is not None and end is not None: if start is not None and end is not None:
assert start < end, 'start must be smaller than end' assert start < end, 'start must be smaller than end'
@ -519,6 +531,7 @@ class TracklistController(object):
:type end: int :type end: int
:rtype: :class:`mopidy.models.TlTrack` :rtype: :class:`mopidy.models.TlTrack`
""" """
# TODO: validate slice?
return self._tl_tracks[start:end] return self._tl_tracks[start:end]
def _mark_playing(self, tl_track): def _mark_playing(self, tl_track):

View File

@ -74,12 +74,12 @@ def check_query(arg, list_values=True):
check_choice(key, QUERY_FIELDS, msg='Expected query field to be one ' check_choice(key, QUERY_FIELDS, msg='Expected query field to be one '
'of {choices}, not {arg!r}') 'of {choices}, not {arg!r}')
if list_values: if list_values:
msg = 'Expected "{key}" values to be list of strings, not {arg!r}' msg = 'Expected "{key}" to be list of strings, not {arg!r}'
_check_iterable(value, msg, key=key) _check_iterable(value, msg, key=key)
[_check_query_value(key, v, msg) for v in value] [_check_query_value(key, v, msg) for v in value]
else: else:
_check_query_value(key, value, 'Expected "{key}" value to be a ' _check_query_value(
'string, not {arg!r}') key, value, 'Expected "{key}" to be a string, not {arg!r}')
def _check_query_value(key, arg, msg): def _check_query_value(key, arg, msg):

View File

@ -9,7 +9,7 @@ import mock
import pykka import pykka
from mopidy import core from mopidy import core, exceptions
from mopidy.local import actor, json from mopidy.local import actor, json
from mopidy.models import Album, Artist, Image, Track from mopidy.models import Album, Artist, Image, Track
@ -137,8 +137,8 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertEqual(result[uri], self.tracks[0:1]) self.assertEqual(result[uri], self.tracks[0:1])
def test_lookup_unknown_track(self): def test_lookup_unknown_track(self):
tracks = self.library.lookup(uris=['fake uri']) tracks = self.library.lookup(uris=['fake:/uri'])
self.assertEqual(tracks, {'fake uri': []}) self.assertEqual(tracks, {'fake:/uri': []})
# test backward compatibility with local libraries returning a # test backward compatibility with local libraries returning a
# single Track # single Track
@ -343,42 +343,44 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.find_exact(any=['local:track:path1']) result = self.find_exact(any=['local:track:path1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
# TODO: This is really just a test of the query validation code now,
# as this code path never even makes it to the local backend.
def test_find_exact_wrong_type(self): def test_find_exact_wrong_type(self):
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(wrong=['test']) self.find_exact(wrong=['test'])
def test_find_exact_with_empty_query(self): def test_find_exact_with_empty_query(self):
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(artist=['']) self.find_exact(artist=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(albumartist=['']) self.find_exact(albumartist=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(track_name=['']) self.find_exact(track_name=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(composer=['']) self.find_exact(composer=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(performer=['']) self.find_exact(performer=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(album=['']) self.find_exact(album=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(track_no=['']) self.find_exact(track_no=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(genre=['']) self.find_exact(genre=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(date=['']) self.find_exact(date=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(comment=['']) self.find_exact(comment=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.find_exact(any=['']) self.find_exact(any=[''])
def test_search_no_hits(self): def test_search_no_hits(self):
@ -553,41 +555,41 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertEqual(list(result[0].tracks), self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
def test_search_wrong_type(self): def test_search_wrong_type(self):
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(wrong=['test']) self.search(wrong=['test'])
def test_search_with_empty_query(self): def test_search_with_empty_query(self):
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(artist=['']) self.search(artist=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(albumartist=['']) self.search(albumartist=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(composer=['']) self.search(composer=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(performer=['']) self.search(performer=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(track_name=['']) self.search(track_name=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(album=['']) self.search(album=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(genre=['']) self.search(genre=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(date=['']) self.search(date=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(comment=['']) self.search(comment=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(uri=['']) self.search(uri=[''])
with self.assertRaises(LookupError): with self.assertRaises(exceptions.ValidationError):
self.search(any=['']) self.search(any=[''])
def test_default_get_images_impl_no_images(self): def test_default_get_images_impl_no_images(self):

View File

@ -841,22 +841,6 @@ class LocalPlaybackProviderTest(unittest.TestCase):
self.playback.seek(self.tracklist.tracks[-1].length * 100) self.playback.seek(self.tracklist.tracks[-1].length * 100)
self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@unittest.SkipTest
@populate_tracklist
def test_seek_beyond_start_of_song(self):
# FIXME need to decide return value
self.playback.play()
result = self.playback.seek(-1000)
self.assert_(not result, 'Seek return value was %s' % result)
@populate_tracklist
def test_seek_beyond_start_of_song_update_postion(self):
self.playback.play()
self.playback.seek(-1000)
position = self.playback.time_position
self.assertGreaterEqual(position, 0)
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_tracklist @populate_tracklist
def test_stop_when_stopped(self): def test_stop_when_stopped(self):
self.playback.stop() self.playback.stop()

View File

@ -118,7 +118,7 @@ def test_check_query_invalid_values():
def test_check_values_error_message(): def test_check_values_error_message():
with raises(exceptions.ValidationError) as excinfo: with raises(exceptions.ValidationError) as excinfo:
validation.check_query({'any': 'abc'}) validation.check_query({'any': 'abc'})
assert 'Expected "any" values to be list of strings' in str(excinfo.value) assert 'Expected "any" to be list of strings, not' in str(excinfo.value)
def test_check_uri_with_valid_values(): def test_check_uri_with_valid_values():