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:

Patch:
diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py
index 0f4dc1513..e92cba40c 100755
--- a/beets/dbcore/db.py
+++ b/beets/dbcore/db.py
@@ -290,10 +290,10 @@ class Model(object):
         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))
 
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 2d456ae88..2b8ede053 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -42,6 +42,10 @@ New features:
 * The ``albumdisambig`` field no longer includes the MusicBrainz release group
   disambiguation comment. A new ``releasegroupdisambig`` field has been added.
   :bug:`3024`
+* The :ref:`modify-cmd` command now allows resetting fixed attributes. For
+  example, ``beet modify -a artist:beatles artpath!`` resets ``artpath``
+  attribute from matching albums back to the default value.
+  :bug:`2497`
 
 Changes:
 


Test patch:
diff --git a/test/test_dbcore.py b/test/test_dbcore.py
index d2d97d4b3..4c646802d 100644
--- 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 @@ class ModelTest(unittest.TestCase):
             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()