Repository: beetbox/beets

Issue:
Implicit path queries in a boolean OR yield a match-all query
This is best illustrated by example:
- `beet list /path/to/library/A` — works
- `beet list /path/to/library/B` — works
- `beet list /path/to/library/A , /path/to/library/B` — **lists the whole library**
- `beet list path:/path/to/library/A , path:/path/to/library/B` — works


Discussion:

Patch:
diff --git a/beets/library.py b/beets/library.py
index 69fcd34cf..c8fa2b5fc 100644
--- a/beets/library.py
+++ b/beets/library.py
@@ -1382,7 +1382,7 @@ def parse_query_parts(parts, model_cls):
     `Query` and `Sort` they represent.
 
     Like `dbcore.parse_sorted_query`, with beets query prefixes and
-    special path query detection.
+    ensuring that implicit path queries are made explicit with 'path::<query>'
     """
     # Get query types and their prefix characters.
     prefixes = {
@@ -1394,28 +1394,14 @@ def parse_query_parts(parts, model_cls):
 
     # Special-case path-like queries, which are non-field queries
     # containing path separators (/).
-    path_parts = []
-    non_path_parts = []
-    for s in parts:
-        if PathQuery.is_path_query(s):
-            path_parts.append(s)
-        else:
-            non_path_parts.append(s)
+    parts = [f"path:{s}" if PathQuery.is_path_query(s) else s for s in parts]
 
     case_insensitive = beets.config['sort_case_insensitive'].get(bool)
 
-    query, sort = dbcore.parse_sorted_query(
-        model_cls, non_path_parts, prefixes, case_insensitive
+    return dbcore.parse_sorted_query(
+        model_cls, parts, prefixes, case_insensitive
     )
 
-    # Add path queries to aggregate query.
-    # Match field / flexattr depending on whether the model has the path field
-    fast_path_query = 'path' in model_cls._fields
-    query.subqueries += [PathQuery('path', s, fast_path_query)
-                         for s in path_parts]
-
-    return query, sort
-
 
 def parse_query_string(s, model_cls):
     """Given a beets query string, return the `Query` and `Sort` they
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 72b1cf1fe..d6c74e451 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -28,6 +28,9 @@ New features:
 
 Bug fixes:
 
+* Fix implicit paths OR queries (e.g. ``beet list /path/ , /other-path/``) 
+  which have previously been returning the entire library.
+  :bug:`1865`
 * The Discogs release ID is now populated correctly to the discogs_albumid
   field again (it was no longer working after Discogs changed their release URL
   format).


Test patch:
diff --git a/test/test_query.py b/test/test_query.py
index 0be4b7d7f..8a9043fa3 100644
--- a/test/test_query.py
+++ b/test/test_query.py
@@ -31,7 +31,10 @@ from beets.dbcore.query import (NoneQuery, ParsingError,
                                 InvalidQueryArgumentValueError)
 from beets.library import Library, Item
 from beets import util
-import platform
+
+# Because the absolute path begins with something like C:, we
+# can't disambiguate it from an ordinary query.
+WIN32_NO_IMPLICIT_PATHS = 'Implicit paths are not supported on Windows'
 
 
 class TestHelper(helper.TestHelper):
@@ -521,6 +524,7 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin):
         results = self.lib.albums(q)
         self.assert_albums_matched(results, ['path album'])
 
+    @unittest.skipIf(sys.platform == 'win32', WIN32_NO_IMPLICIT_PATHS)
     def test_slashed_query_matches_path(self):
         q = '/a/b'
         results = self.lib.items(q)
@@ -529,7 +533,7 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin):
         results = self.lib.albums(q)
         self.assert_albums_matched(results, ['path album'])
 
-    @unittest.skip('unfixed (#1865)')
+    @unittest.skipIf(sys.platform == 'win32', WIN32_NO_IMPLICIT_PATHS)
     def test_path_query_in_or_query(self):
         q = '/a/b , /a/b'
         results = self.lib.items(q)
@@ -649,12 +653,8 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin):
         self.assertFalse(is_path('foo:bar/'))
         self.assertFalse(is_path('foo:/bar'))
 
+    @unittest.skipIf(sys.platform == 'win32', WIN32_NO_IMPLICIT_PATHS)
     def test_detect_absolute_path(self):
-        if platform.system() == 'Windows':
-            # Because the absolute path begins with something like C:, we
-            # can't disambiguate it from an ordinary query.
-            self.skipTest('Windows absolute paths do not work as queries')
-
         # Don't patch `os.path.exists`; we'll actually create a file when
         # it exists.
         self.patcher_exists.stop()