diff --git a/beets/library.py b/beets/library.py index 9f448227f..889f2094c 100644 --- a/beets/library.py +++ b/beets/library.py @@ -75,6 +75,17 @@ class PathQuery(dbcore.FieldQuery): # As a directory (prefix). self.dir_path = util.bytestring_path(os.path.join(self.file_path, b'')) + @classmethod + def is_path_query(cls, query_part): + """Try to guess whether a unicode query part is a path query. + + Condition: separator precedes colon and the file exists. + """ + colon = query_part.find(':') + if colon != -1: + query_part = query_part[:colon] + return os.sep in query_part and os.path.exists(query_part) + def match(self, item): path = item.path if self.case_sensitive else item.path.lower() return (path == self.file_path) or path.startswith(self.dir_path) @@ -1097,8 +1108,7 @@ def parse_query_parts(parts, model_cls): path_parts = [] non_path_parts = [] for s in parts: - if s.find(os.sep, 0, s.find(':')) != -1: - # Separator precedes colon. + if PathQuery.is_path_query(s): path_parts.append(s) else: non_path_parts.append(s) diff --git a/docs/changelog.rst b/docs/changelog.rst index db90d4837..24ad9a0d2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,8 @@ Changelog Features: +* :ref:`pathquery` are automatically triggered only if the + path targeted by the query exists. * :doc:`/plugins/duplicates` now accepts a ``--strict`` option that will only report duplicates if all attributes are explicitly set. :bug:`1000` diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 20c5360f8..102c689d2 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -166,6 +166,8 @@ Find all items with a file modification time between 2008-12-01 and $ beet ls 'mtime:2008-12-01..2008-12-02' +.. _pathquery: + Path Queries ------------ @@ -175,8 +177,8 @@ Sometimes it's useful to find all the items in your library that are $ beet list path:/my/music/directory In fact, beets automatically recognizes any query term containing a path -separator (``/`` on POSIX systems) as a path query, so this command is -equivalent:: +separator (``/`` on POSIX systems) as a path query if that path exists, so this +command is equivalent as long as ``/my/music/directory`` exist:: $ beet list /my/music/directory diff --git a/test/test_query.py b/test/test_query.py index 6d8d744fe..04235906d 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -18,6 +18,8 @@ from __future__ import (division, absolute_import, print_function, unicode_literals) from functools import partial +from mock import patch +import os from test import _common from test._common import unittest @@ -374,6 +376,13 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin): self.i.store() self.lib.add_album([self.i]) + self.patcher = patch('beets.library.os.path.exists') + self.patcher.start().return_value = True + + def tearDown(self): + super(PathQueryTest, self).tearDown() + self.patcher.stop() + def test_path_exact_match(self): q = 'path:/a/b/c.mp3' results = self.lib.items(q) @@ -503,6 +512,42 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin): q = makeq() self.assertEqual(q.case_sensitive, False) + @patch('beets.library.os') + def test_path_sep_detection(self, mock_os): + mock_os.sep = '/' + is_path = beets.library.PathQuery.is_path_query + self.assertTrue(is_path('/foo/bar')) + self.assertTrue(is_path('foo/bar')) + self.assertTrue(is_path('foo/')) + self.assertFalse(is_path('foo')) + self.assertTrue(is_path('foo/:bar')) + self.assertFalse(is_path('foo:bar/')) + self.assertFalse(is_path('foo:/bar')) + + def test_path_detection(self): + # cover existence test + self.patcher.stop() + is_path = beets.library.PathQuery.is_path_query + + try: + self.touch(b'foo/bar') + # test absolute + self.assertTrue(is_path(os.path.join(self.temp_dir, b'foo/bar'))) + self.assertTrue(is_path(os.path.join(self.temp_dir, b'foo'))) + self.assertFalse(is_path(b'foo/bar')) + + cur_dir = os.getcwd() + try: + os.chdir(self.temp_dir) + self.assertTrue(is_path(b'foo/')) + self.assertTrue(is_path(b'foo/bar')) + self.assertTrue(is_path(b'foo/bar:tagada')) + self.assertFalse(is_path(b'bar')) + finally: + os.chdir(cur_dir) + finally: + self.patcher.start() + class IntQueryTest(unittest.TestCase, TestHelper):