diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 86078fb6a..d4928af2b 100644 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -159,6 +159,19 @@ class Model(object): if old_value != value: self._dirty.add(key) + def __delitem__(self, key): + """Remove a flexible attribute from the model. + """ + 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._getters(): # Computed. + raise KeyError('computed field {0} cannot be deleted'.format(key)) + elif key in self._fields: # Fixed. + raise KeyError('fixed field {0} cannot be deleted'.format(key)) + else: + raise KeyError('no such field {0}'.format(key)) + def keys(self, computed=False): """Get a list of available field names for this object. The `computed` parameter controls whether computed (plugin-provided) @@ -224,6 +237,12 @@ class Model(object): else: self[key] = value + def __delattr__(self, key): + if key.startswith('_'): + super(Model, self).__delattr__(key) + else: + del self[key] + # Database interaction (CRUD methods). @@ -237,6 +256,7 @@ class Model(object): subvars = [] for key in self._fields: if key != 'id' and key in self._dirty: + self._dirty.remove(key) assignments += key + '=?,' value = self[key] # Wrap path strings in buffers so they get stored @@ -255,9 +275,10 @@ class Model(object): subvars.append(self.id) tx.mutate(query, subvars) - # Flexible attributes. + # Modified/added flexible attributes. for key, value in self._values_flex.items(): if key in self._dirty: + self._dirty.remove(key) tx.mutate( 'INSERT INTO {0} ' '(entity_id, key, value) ' @@ -265,6 +286,14 @@ class Model(object): (self.id, key, value), ) + # Deleted flexible attributes. + for key in self._dirty: + tx.mutate( + 'DELETE FROM {0} ' + 'WHERE entity_id=? AND key=?'.format(self._flex_table), + (self.id, key) + ) + self.clear_dirty() def load(self): diff --git a/test/test_dbcore.py b/test/test_dbcore.py index f173ff08b..8dd554e06 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -182,6 +182,43 @@ class ModelTest(_common.TestCase): other_model = self.db._get(TestModel1, model.id) self.assertEqual(other_model.foo, 'bar') + def test_delete_flexattr(self): + model = TestModel1() + model['foo'] = 'bar' + self.assertTrue('foo' in model) + del model['foo'] + self.assertFalse('foo' in model) + + def test_delete_flexattr_via_dot(self): + model = TestModel1() + model['foo'] = 'bar' + self.assertTrue('foo' in model) + del model.foo + self.assertFalse('foo' in model) + + def test_delete_flexattr_persists(self): + model = TestModel1() + model.add(self.db) + model.foo = 'bar' + model.store() + + model = self.db._get(TestModel1, model.id) + del model['foo'] + model.store() + + model = self.db._get(TestModel1, model.id) + self.assertFalse('foo' in model) + + def test_delete_non_existent_attribute(self): + model = TestModel1() + with self.assertRaises(KeyError): + del model['foo'] + + def test_delete_fixed_attribute(self): + model = TestModel1() + with self.assertRaises(KeyError): + del model['field_one'] + class FormatTest(_common.TestCase): def test_format_fixed_field(self):