diff --git a/mopidy/models.py b/mopidy/models.py index e936e22c..35f1f9eb 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -45,7 +45,7 @@ class Field(object): def __get__(self, instance, owner): if not instance: return self - return instance.__dict__.get(self._name, self._default) + return getattr(instance, '_' + self._name, self._default) def __set__(self, instance, value): if value is not None: @@ -54,10 +54,11 @@ class Field(object): if value is None or value == self._default: self.__delete__(instance) else: - instance.__dict__[self._name] = value + setattr(instance, '_' + self._name, value) def __delete__(self, instance): - instance.__dict__.pop(self._name, None) + if hasattr(instance, '_' + self._name): + delattr(instance, '_' + self._name) class String(Field): @@ -134,12 +135,14 @@ class FieldOwner(type): """Helper to automatically assign field names to descriptors.""" def __new__(cls, name, bases, attrs): - attrs['_fields'] = [] + fields = {} for key, value in attrs.items(): if isinstance(value, Field): - attrs['_fields'].append(key) + fields[key] = '_' + key value._name = key - attrs['_fields'].sort() + + attrs['_fields'] = fields + attrs['__slots__'] = ['_' + field for field in fields] return super(FieldOwner, cls).__new__(cls, name, bases, attrs) @@ -165,14 +168,23 @@ class ImmutableObject(object): super(ImmutableObject, self).__setattr__(key, value) def __setattr__(self, name, value): + if name in self.__slots__: + return super(ImmutableObject, self).__setattr__(name, value) raise AttributeError('Object is immutable.') def __delattr__(self, name): + if name in self.__slots__: + return super(ImmutableObject, self).__delattr__(name) raise AttributeError('Object is immutable.') + def _items(self): + for field, key in self._fields.items(): + if hasattr(self, key): + yield field, getattr(self, key) + def __repr__(self): kwarg_pairs = [] - for (key, value) in sorted(self.__dict__.items()): + for key, value in sorted(self._items()): if isinstance(value, (frozenset, tuple)): if not value: continue @@ -185,15 +197,14 @@ class ImmutableObject(object): def __hash__(self): hash_sum = 0 - for key, value in self.__dict__.items(): + for key, value in self._items(): hash_sum += hash(key) + hash(value) return hash_sum def __eq__(self, other): if not isinstance(other, self.__class__): return False - - return self.__dict__ == other.__dict__ + return dict(self._items()) == dict(other._items()) def __ne__(self, other): return not self.__eq__(other) @@ -224,7 +235,7 @@ class ImmutableObject(object): def serialize(self): data = {} data['__model__'] = self.__class__.__name__ - for key, value in self.__dict__.items(): + for key, value in self._items(): if isinstance(value, (set, frozenset, list, tuple)): value = [ v.serialize() if isinstance(v, ImmutableObject) else v diff --git a/tests/models/test_fields.py b/tests/models/test_fields.py index 5347b83c..ff863487 100644 --- a/tests/models/test_fields.py +++ b/tests/models/test_fields.py @@ -29,15 +29,14 @@ class FieldDescriptorTest(unittest.TestCase): instance = create_instance(Field()) self.assertIsNone(instance.attr) - def test_field_does_not_store_default_in_dict(self): + def test_field_does_not_store_default(self): instance = create_instance(Field()) - self.assertNotIn('attr', instance.__dict__) + self.assertFalse(hasattr(instance, '_attr')) def test_field_assigment_and_retrival(self): instance = create_instance(Field()) instance.attr = 1234 self.assertEqual(1234, instance.attr) - self.assertEqual(1234, instance.__dict__['attr']) def test_field_can_be_reassigned(self): instance = create_instance(Field()) @@ -50,14 +49,14 @@ class FieldDescriptorTest(unittest.TestCase): instance.attr = 1234 del instance.attr self.assertEqual(None, instance.attr) - self.assertNotIn('attr', instance.__dict__) + self.assertFalse(hasattr(instance, '_attr')) def test_field_can_be_set_to_none(self): instance = create_instance(Field()) instance.attr = 1234 instance.attr = None self.assertEqual(None, instance.attr) - self.assertNotIn('attr', instance.__dict__) + self.assertFalse(hasattr(instance, '_attr')) def test_field_can_be_set_default(self): default = object() @@ -65,7 +64,7 @@ class FieldDescriptorTest(unittest.TestCase): instance.attr = 1234 instance.attr = default self.assertEqual(default, instance.attr) - self.assertNotIn('attr', instance.__dict__) + self.assertFalse(hasattr(instance, '_attr')) class FieldTest(unittest.TestCase): @@ -183,13 +182,11 @@ class CollectionTest(unittest.TestCase): instance = create_instance(Collection(type=int, container=frozenset)) instance.attr = [] self.assertEqual(frozenset(), instance.attr) - self.assertNotIn('attr', instance.__dict__) def test_collection_gets_stored_in_container(self): instance = create_instance(Collection(type=int, container=frozenset)) instance.attr = [1, 2, 3] self.assertEqual(frozenset([1, 2, 3]), instance.attr) - self.assertEqual(frozenset([1, 2, 3]), instance.__dict__['attr']) def test_collection_with_wrong_type(self): instance = create_instance(Collection(type=int, container=frozenset)) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 7e0d6bb9..8f72dd34 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -55,7 +55,7 @@ class GenericCopyTest(unittest.TestCase): def test_copying_track_to_remove(self): track = Track(name='foo').copy(name=None) - self.assertEqual(track.__dict__, Track().__dict__) + self.assertFalse(hasattr(track, '_name')) class RefTest(unittest.TestCase):