Repository: beetbox/beets

Issue:
Erroneous @classmethod on NoneQuery.match
### Problem

When running a query against the library that contains a NoneQuery such as:

> dbcore.query.NoneQuery(u'gender', fast=False)

beet exits with the following traceback:

```sh
Traceback (most recent call last):
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/bin/beet", line 8, in <module>
    sys.exit(main())
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/lib/python3.7/site-packages/beets/ui/__init__.py", line 1266, in main
    _raw_main(args)
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/lib/python3.7/site-packages/beets/ui/__init__.py", line 1253, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/Users/jackisback/Documents/Projects/Python/BeetsPluginXtractor/beetsplug/xtractor/command.py", line 133, in func
    self.xtract()
  File "/Users/jackisback/Documents/Projects/Python/BeetsPluginXtractor/beetsplug/xtractor/command.py", line 157, in xtract
    if len(library_items) == 0:
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/lib/python3.7/site-packages/beets/dbcore/db.py", line 741, in __len__
    for obj in self:
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/lib/python3.7/site-packages/beets/dbcore/db.py", line 689, in _get_objects
    if not self.query or self.query.match(obj):
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/lib/python3.7/site-packages/beets/dbcore/query.py", line 462, in match
    return all([q.match(item) for q in self.subqueries])
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/lib/python3.7/site-packages/beets/dbcore/query.py", line 462, in <listcomp>
    return all([q.match(item) for q in self.subqueries])
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/lib/python3.7/site-packages/beets/dbcore/query.py", line 472, in match
    return any([q.match(item) for q in self.subqueries])
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/lib/python3.7/site-packages/beets/dbcore/query.py", line 472, in <listcomp>
    return any([q.match(item) for q in self.subqueries])
  File "/Users/jackisback/opt/miniconda3/envs/beetsdevel/lib/python3.7/site-packages/beets/dbcore/query.py", line 168, in match
    return item[cls.field] is None
AttributeError: type object 'NoneQuery' has no attribute 'field'
```

The above example is using a flex attribute but the same error can be reproduced using a fixed field such as: `dbcore.query.NoneQuery(u'title')`

This behaviour has already been reported in #1634, identifying the problem within the `@classmethod` decorated match method using `self` in the NoneQuery class. The bug was fixed by correcting `self` to `cls` leaving the method itself decorated with `@classmethod`.


### Setup
beets version 1.4.9
Python version 3.7.6
plugins: info, xtractor
* Turning off plugins made problem go away (yes/no): no

### Possible solution
The NotQuery.match method should probably not be decorated with `@classmethod`.



Discussion:

Resolving patch:
diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py
--- a/beets/dbcore/query.py
+++ b/beets/dbcore/query.py
@@ -156,12 +156,8 @@ def __init__(self, field, fast=True):
     def col_clause(self):
         return self.field + " IS NULL", ()
 
-    @classmethod
-    def match(cls, item):
-        try:
-            return item[cls.field] is None
-        except KeyError:
-            return True
+    def match(self, item):
+        return item.get(self.field, default=None) is None
 
     def __repr__(self):
         return "{0.__class__.__name__}({0.field!r}, {0.fast})".format(self)


Test patch:
diff --git a/test/test_query.py b/test/test_query.py
--- a/test/test_query.py
+++ b/test/test_query.py
@@ -772,6 +772,21 @@ def test_match_after_set_none(self):
         matched = self.lib.items(NoneQuery(u'rg_track_gain'))
         self.assertInResult(item, matched)
 
+    def test_match_slow(self):
+        item = self.add_item()
+        matched = self.lib.items(NoneQuery(u'rg_track_peak', fast=False))
+        self.assertInResult(item, matched)
+
+    def test_match_slow_after_set_none(self):
+        item = self.add_item(rg_track_gain=0)
+        matched = self.lib.items(NoneQuery(u'rg_track_gain', fast=False))
+        self.assertNotInResult(item, matched)
+
+        item['rg_track_gain'] = None
+        item.store()
+        matched = self.lib.items(NoneQuery(u'rg_track_gain', fast=False))
+        self.assertInResult(item, matched)
+
 
 class NotQueryMatchTest(_common.TestCase):
     """Test `query.NotQuery` matching against a single item, using the same