diff --git a/docs/changelog.rst b/docs/changelog.rst index 94f35433..d3573e89 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -103,6 +103,11 @@ v1.0.0 (UNRELEASED) - **Deprecated:** :meth:`mopidy.core.PlaylistsController.filter`. Use :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** - Remove default implementation of diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 80c61bbb..44375f58 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -127,62 +127,12 @@ class LibraryController(object): return results 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 - Previously, if the query was empty, and the backend could support - 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` + Use :meth:`search` with ``exact`` set. """ - query = _normalize_query(query or 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] + return self.search(query=query, uris=uris, exact=True, **kwargs) def lookup(self, uri=None, uris=None): """ @@ -248,7 +198,7 @@ class LibraryController(object): for b in self.backends.with_library.values()] 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``. @@ -287,12 +237,29 @@ class LibraryController(object): :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` + + .. versionadded:: 1.0 + The ``exact`` keyword argument, which replaces :meth:`find_exact`. """ query = _normalize_query(query or kwargs) - futures = [ - backend.library.search(query=query, uris=backend_uris) - for (backend, backend_uris) - in self._get_backends_to_uris(uris).items()] + futures = [] + for backend, backend_uris in self._get_backends_to_uris(uris).items(): + if hasattr(backend.library, 'find_exact'): + # 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] diff --git a/tests/core/test_library.py b/tests/core/test_library.py index 98b25f38..50eb834f 100644 --- a/tests/core/test_library.py +++ b/tests/core/test_library.py @@ -291,16 +291,16 @@ class CoreLibraryTest(unittest.TestCase): self.assertIn(result1, result) self.assertIn(result2, result) 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( - query=dict(any=['a']), uris=None) + query=dict(any=['a']), uris=None, exact=False) def test_search_with_uris_selects_dummy1_backend(self): self.core.library.search( query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy3:']) 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) 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:']) 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( - query=dict(any=['a']), uris=['dummy2:']) + query=dict(any=['a']), uris=['dummy2:'], exact=False) def test_search_filters_out_none(self): track1 = Track(uri='dummy1:a') @@ -326,9 +326,9 @@ class CoreLibraryTest(unittest.TestCase): self.assertIn(result1, result) self.assertNotIn(None, result) 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( - query=dict(any=['a']), uris=None) + query=dict(any=['a']), uris=None, exact=False) def test_search_accepts_query_dict_instead_of_kwargs(self): track1 = Track(uri='dummy1:a') @@ -346,14 +346,14 @@ class CoreLibraryTest(unittest.TestCase): self.assertIn(result1, result) self.assertIn(result2, result) 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( - query=dict(any=['a']), uris=None) + query=dict(any=['a']), uris=None, exact=False) def test_search_normalises_bad_queries(self): self.core.library.search({'any': 'foobar'}) 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): self.core.library.find_exact({'any': 'foobar'}) @@ -367,7 +367,7 @@ class LegacyLibraryProvider(backend.LibraryProvider): 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.uri_schemes.get.return_value = ['dummy1'] b1.library = mock.Mock(spec=LegacyLibraryProvider) @@ -383,3 +383,20 @@ class LegacyCoreLibraryTest(unittest.TestCase): query=dict(any=['a']), uris=None) b2.library.search.assert_called_once_with( 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)