core: Add exact to search() and deprecate find_exact()

Backends that still implement find_exact will be called without exact as an
argument to search, and we will continue to use find_exact. Please remove
find_exact from such backends and switch to the new search API.
This commit is contained in:
Thomas Adamcik 2015-03-24 09:26:11 +01:00
parent 3e361d4870
commit 141c14ad45
3 changed files with 58 additions and 69 deletions

View File

@ -103,6 +103,11 @@ v1.0.0 (UNRELEASED)
- **Deprecated:** :meth:`mopidy.core.PlaylistsController.filter`. Use - **Deprecated:** :meth:`mopidy.core.PlaylistsController.filter`. Use
:meth:`~mopidy.core.PlaylistsController.as_list` and filter yourself. :meth:`~mopidy.core.PlaylistsController.as_list` and filter yourself.
- Add ``exact`` to :meth:`mopidy.core.LibraryController.search`.
- **Deprecated:** :meth:`mopidy.core.LibraryController.find_exact`. Use
:meth:`mopidy.core.LibraryController.search` with ``exact`` set.
**Backend API** **Backend API**
- Remove default implementation of - Remove default implementation of

View File

@ -127,62 +127,12 @@ class LibraryController(object):
return results return results
def find_exact(self, query=None, uris=None, **kwargs): def find_exact(self, query=None, uris=None, **kwargs):
""" """Search the library for tracks where ``field`` is ``values``.
Search the library for tracks where ``field`` is ``values``.
.. deprecated:: 1.0 .. deprecated:: 1.0
Previously, if the query was empty, and the backend could support Use :meth:`search` with ``exact`` set.
it, all available tracks were returned. This has not changed, but
it is strongly discouraged. No new code should rely on this
If ``uris`` is given, the search is limited to results from within the
URI roots. For example passing ``uris=['file:']`` will limit the search
to the local backend.
Examples::
# Returns results matching 'a' from any backend
find_exact({'any': ['a']})
find_exact(any=['a'])
# Returns results matching artist 'xyz' from any backend
find_exact({'artist': ['xyz']})
find_exact(artist=['xyz'])
# Returns results matching 'a' and 'b' and artist 'xyz' from any
# backend
find_exact({'any': ['a', 'b'], 'artist': ['xyz']})
find_exact(any=['a', 'b'], artist=['xyz'])
# Returns results matching 'a' if within the given URI roots
# "file:///media/music" and "spotify:"
find_exact(
{'any': ['a']}, uris=['file:///media/music', 'spotify:'])
find_exact(any=['a'], uris=['file:///media/music', 'spotify:'])
.. versionchanged:: 1.0
This method now calls
:meth:`~mopidy.backend.LibraryProvider.search` on the backends
instead of the deprecated ``find_exact``. If the backend still
implements ``find_exact`` we will continue to use it for now.
:param query: one or more queries to search for
:type query: dict
:param uris: zero or more URI roots to limit the search to
:type uris: list of strings or :class:`None`
:rtype: list of :class:`mopidy.models.SearchResult`
""" """
query = _normalize_query(query or kwargs) return self.search(query=query, uris=uris, exact=True, **kwargs)
futures = []
for backend, backend_uris in self._get_backends_to_uris(uris).items():
if hasattr(backend.library, 'find_exact'):
futures.append(backend.library.find_exact(
query=query, uris=backend_uris))
else:
futures.append(backend.library.search(
query=query, uris=backend_uris, exact=True))
return [result for result in pykka.get_all(futures) if result]
def lookup(self, uri=None, uris=None): def lookup(self, uri=None, uris=None):
""" """
@ -248,7 +198,7 @@ class LibraryController(object):
for b in self.backends.with_library.values()] for b in self.backends.with_library.values()]
pykka.get_all(futures) pykka.get_all(futures)
def search(self, query=None, uris=None, **kwargs): def search(self, query=None, uris=None, exact=False, **kwargs):
""" """
Search the library for tracks where ``field`` contains ``values``. Search the library for tracks where ``field`` contains ``values``.
@ -287,12 +237,29 @@ class LibraryController(object):
:param uris: zero or more URI roots to limit the search to :param uris: zero or more URI roots to limit the search to
:type uris: list of strings or :class:`None` :type uris: list of strings or :class:`None`
:rtype: list of :class:`mopidy.models.SearchResult` :rtype: list of :class:`mopidy.models.SearchResult`
.. versionadded:: 1.0
The ``exact`` keyword argument, which replaces :meth:`find_exact`.
""" """
query = _normalize_query(query or kwargs) query = _normalize_query(query or kwargs)
futures = [ futures = []
backend.library.search(query=query, uris=backend_uris) for backend, backend_uris in self._get_backends_to_uris(uris).items():
for (backend, backend_uris) if hasattr(backend.library, 'find_exact'):
in self._get_backends_to_uris(uris).items()] # Backends with find_exact probably don't have support for
# search with the exact kwarg, so give them the legacy calls.
if exact:
futures.append(backend.library.find_exact(
query=query, uris=backend_uris))
else:
futures.append(backend.library.search(
query=query, uris=backend_uris))
else:
# Assume backends without find_exact are up to date. Worst case
# the exact gets swallowed by the **kwargs and things hopefully
# still work.
futures.append(backend.library.search(
query=query, uris=backend_uris, exact=exact))
return [result for result in pykka.get_all(futures) if result] return [result for result in pykka.get_all(futures) if result]

View File

@ -291,16 +291,16 @@ class CoreLibraryTest(unittest.TestCase):
self.assertIn(result1, result) self.assertIn(result1, result)
self.assertIn(result2, result) self.assertIn(result2, result)
self.library1.search.assert_called_once_with( self.library1.search.assert_called_once_with(
query=dict(any=['a']), uris=None) query=dict(any=['a']), uris=None, exact=False)
self.library2.search.assert_called_once_with( self.library2.search.assert_called_once_with(
query=dict(any=['a']), uris=None) query=dict(any=['a']), uris=None, exact=False)
def test_search_with_uris_selects_dummy1_backend(self): def test_search_with_uris_selects_dummy1_backend(self):
self.core.library.search( self.core.library.search(
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy3:']) query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy3:'])
self.library1.search.assert_called_once_with( self.library1.search.assert_called_once_with(
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo']) query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'], exact=False)
self.assertFalse(self.library2.search.called) self.assertFalse(self.library2.search.called)
def test_search_with_uris_selects_both_backends(self): def test_search_with_uris_selects_both_backends(self):
@ -308,9 +308,9 @@ class CoreLibraryTest(unittest.TestCase):
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy2:']) query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy2:'])
self.library1.search.assert_called_once_with( self.library1.search.assert_called_once_with(
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo']) query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'], exact=False)
self.library2.search.assert_called_once_with( self.library2.search.assert_called_once_with(
query=dict(any=['a']), uris=['dummy2:']) query=dict(any=['a']), uris=['dummy2:'], exact=False)
def test_search_filters_out_none(self): def test_search_filters_out_none(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
@ -326,9 +326,9 @@ class CoreLibraryTest(unittest.TestCase):
self.assertIn(result1, result) self.assertIn(result1, result)
self.assertNotIn(None, result) self.assertNotIn(None, result)
self.library1.search.assert_called_once_with( self.library1.search.assert_called_once_with(
query=dict(any=['a']), uris=None) query=dict(any=['a']), uris=None, exact=False)
self.library2.search.assert_called_once_with( self.library2.search.assert_called_once_with(
query=dict(any=['a']), uris=None) query=dict(any=['a']), uris=None, exact=False)
def test_search_accepts_query_dict_instead_of_kwargs(self): def test_search_accepts_query_dict_instead_of_kwargs(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
@ -346,14 +346,14 @@ class CoreLibraryTest(unittest.TestCase):
self.assertIn(result1, result) self.assertIn(result1, result)
self.assertIn(result2, result) self.assertIn(result2, result)
self.library1.search.assert_called_once_with( self.library1.search.assert_called_once_with(
query=dict(any=['a']), uris=None) query=dict(any=['a']), uris=None, exact=False)
self.library2.search.assert_called_once_with( self.library2.search.assert_called_once_with(
query=dict(any=['a']), uris=None) query=dict(any=['a']), uris=None, exact=False)
def test_search_normalises_bad_queries(self): def test_search_normalises_bad_queries(self):
self.core.library.search({'any': 'foobar'}) self.core.library.search({'any': 'foobar'})
self.library1.search.assert_called_once_with( self.library1.search.assert_called_once_with(
query={'any': ['foobar']}, uris=None) query={'any': ['foobar']}, uris=None, exact=False)
def test_find_exact_normalises_bad_queries(self): def test_find_exact_normalises_bad_queries(self):
self.core.library.find_exact({'any': 'foobar'}) self.core.library.find_exact({'any': 'foobar'})
@ -367,7 +367,7 @@ class LegacyLibraryProvider(backend.LibraryProvider):
class LegacyCoreLibraryTest(unittest.TestCase): class LegacyCoreLibraryTest(unittest.TestCase):
def test_backend_with_find_exact_still_works(self): def test_backend_with_find_exact_gets_find_exact_call(self):
b1 = mock.Mock() b1 = mock.Mock()
b1.uri_schemes.get.return_value = ['dummy1'] b1.uri_schemes.get.return_value = ['dummy1']
b1.library = mock.Mock(spec=LegacyLibraryProvider) b1.library = mock.Mock(spec=LegacyLibraryProvider)
@ -383,3 +383,20 @@ class LegacyCoreLibraryTest(unittest.TestCase):
query=dict(any=['a']), uris=None) query=dict(any=['a']), uris=None)
b2.library.search.assert_called_once_with( b2.library.search.assert_called_once_with(
query=dict(any=['a']), uris=None, exact=True) query=dict(any=['a']), uris=None, exact=True)
def test_backend_with_find_exact_gets_search_without_exact_arg(self):
b1 = mock.Mock()
b1.uri_schemes.get.return_value = ['dummy1']
b1.library = mock.Mock(spec=LegacyLibraryProvider)
b2 = mock.Mock()
b2.uri_schemes.get.return_value = ['dummy2']
b2.library = mock.Mock(spec=backend.LibraryProvider)
c = core.Core(mixer=None, backends=[b1, b2])
c.library.search(query={'any': ['a']})
b1.library.search.assert_called_once_with(
query=dict(any=['a']), uris=None)
b2.library.search.assert_called_once_with(
query=dict(any=['a']), uris=None, exact=False)