Repository: beetbox/beets

Issue:
modify: Field deletion (foo!) should work for fixed attributes by setting them to null
### Problem

`foo!` works if foo is a flexible field, but it doesn't do anything if it's a fixed attribute.

Example from IRC Chat:

User not able to delete/reset artpath: to "None" as modify only results in artpath being the current working directory: 

```sh
# beet modify -a 2cellos artpath!
Modifying 1 albums.
No changes to make.
# beet modify -a 2cellos artpath=''
Modifying 1 albums. 
2CELLOS - 2Cellos
  artpath: /media/music/processed/2CELLOS/2011 - 2Cellos/cover.jpg -> /root  
Really modify, move and write tags? (Yes/no/select) n 
```

Led to this problem:

```
artpath: /media/music/processed/2CELLOS/2011 - 2Cellos/cover.jpg -> /root  
```
should be:
```
artpath: /media/music/processed/2CELLOS/2011 - 2Cellos/cover.jpg -> None
```
### Setup

* OS: Linux bedmoode 4.4.30-v7+ #919 SMP Tue Nov 1 16:57:28 GMT 2016 armv7l GNU/Linux 
* Python version: Python 3.4.2
* beets version: beets version 1.4.4
* Turning off plugins made problem go away (yes/no): no




Discussion:
Thanks! All attributes (including fixed attributes) have a null value. We should just use that when using `beet modify foo!`.
It seems confusing to me that it says "Modifying 1 albums" and then "No changes to make". Is this a bug?
That is odd! But perhaps it’s the case that your query marched one album but the new field value was equal to the old field value? That would explain it.
I think it happens any time you use `field!` on an inflexible field
Ah, indeed, because we currently don’t actually change anything when that happens. So the bug is this one, I’d say!

Resolving patch:
diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py
--- a/beets/dbcore/db.py
+++ b/beets/dbcore/db.py
@@ -290,10 +290,10 @@ def __delitem__(self, key):
         if key in self._values_flex:  # Flexible.
             del self._values_flex[key]
             self._dirty.add(key)  # Mark for dropping on store.
+        elif key in self._fields:  # Fixed
+            setattr(self, key, self._type(key).null)
         elif key in self._getters():  # Computed.
             raise KeyError(u'computed field {0} cannot be deleted'.format(key))
-        elif key in self._fields:  # Fixed.
-            raise KeyError(u'fixed field {0} cannot be deleted'.format(key))
         else:
             raise KeyError(u'no such field {0}'.format(key))
 


Test patch:
diff --git a/test/test_dbcore.py b/test/test_dbcore.py
--- a/test/test_dbcore.py
+++ b/test/test_dbcore.py
@@ -114,6 +114,19 @@ class AnotherTestModel(TestModel1):
     }
 
 
+class TestModel5(TestModel1):
+    _fields = {
+        'some_string_field': dbcore.types.STRING,
+        'some_float_field': dbcore.types.FLOAT,
+        'some_boolean_field': dbcore.types.BOOLEAN,
+    }
+
+
+class TestDatabase5(dbcore.Database):
+    _models = (TestModel5,)
+    pass
+
+
 class TestDatabaseTwoModels(dbcore.Database):
     _models = (TestModel2, AnotherTestModel)
     pass
@@ -266,9 +279,18 @@ def test_delete_non_existent_attribute(self):
             del model['foo']
 
     def test_delete_fixed_attribute(self):
-        model = TestModel1()
-        with self.assertRaises(KeyError):
-            del model['field_one']
+        model = TestModel5()
+        model.some_string_field = 'foo'
+        model.some_float_field = 1.23
+        model.some_boolean_field = True
+
+        for field, type_ in model._fields.items():
+            print("%s - %s" % (model[field], field))
+            self.assertNotEqual(model[field], type_.null)
+
+        for field, type_ in model._fields.items():
+            del model[field]
+            self.assertEqual(model[field], type_.null)
 
     def test_null_value_normalization_by_type(self):
         model = TestModel1()