diff --git a/mopidy/models.py b/mopidy/models.py index c12da719..97b524ea 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals import copy import json +import weakref # TODO: split into base models, serialization and fields? @@ -24,7 +25,7 @@ class Field(object): """ def __init__(self, default=None, type=None, choices=None): - self._name = None # Set by FieldOwner + self._name = None # Set by ImmutableObjectMeta self._choices = choices self._default = default self._type = type @@ -130,7 +131,7 @@ class Collection(Field): return self._default.__class__(value) or None -class FieldOwner(type): +class ImmutableObjectMeta(type): """Helper to automatically assign field names to descriptors.""" @@ -142,8 +143,20 @@ class FieldOwner(type): value._name = key attrs['_fields'] = fields + attrs['_instances'] = weakref.WeakValueDictionary() attrs['__slots__'] = ['_' + field for field in fields] - return super(FieldOwner, cls).__new__(cls, name, bases, attrs) + + for base in bases: + if '__weakref__' in getattr(base, '__slots__', []): + break + else: + attrs['__slots__'].append('__weakref__') + + return super(ImmutableObjectMeta, cls).__new__(cls, name, bases, attrs) + + def __call__(cls, *args, **kwargs): # noqa: N805 + instance = super(ImmutableObjectMeta, cls).__call__(*args, **kwargs) + return cls._instances.setdefault(weakref.ref(instance), instance) class ImmutableObject(object): @@ -157,7 +170,7 @@ class ImmutableObject(object): :type kwargs: any """ - __metaclass__ = FieldOwner + __metaclass__ = ImmutableObjectMeta def __init__(self, *args, **kwargs): for key, value in kwargs.items(): @@ -232,7 +245,7 @@ class ImmutableObject(object): raise TypeError( 'copy() got an unexpected keyword argument "%s"' % key) super(ImmutableObject, other).__setattr__(key, value) - return other + return self._instances.setdefault(weakref.ref(other), other) def serialize(self): data = {} diff --git a/tests/models/test_fields.py b/tests/models/test_fields.py index ff863487..1bf46b7f 100644 --- a/tests/models/test_fields.py +++ b/tests/models/test_fields.py @@ -9,7 +9,7 @@ def create_instance(field): """Create an instance of a dummy class for testing fields.""" class Dummy(object): - __metaclass__ = FieldOwner + __metaclass__ = ImmutableObjectMeta attr = field return Dummy() diff --git a/tests/models/test_models.py b/tests/models/test_models.py index ef484fa9..90701b14 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -8,6 +8,22 @@ from mopidy.models import ( TlTrack, Track, model_json_decoder) +class CachingTest(unittest.TestCase): + + def test_same_instance(self): + self.assertIs(Track(), Track()) + + def test_same_instance_with_values(self): + self.assertIs(Track(uri='test'), Track(uri='test')) + + def test_different_instance_with_different_values(self): + self.assertIsNot(Track(uri='test1'), Track(uri='test2')) + + def test_different_instance_with_copy(self): + t = Track(uri='test1') + self.assertIsNot(t, t.copy(uri='test2')) + + class GenericCopyTest(unittest.TestCase): def compare(self, orig, other):