Source code for save_the_change.descriptors

# -*- coding: utf-8 -*-

from __future__ import division, absolute_import, print_function, unicode_literals

from copy import deepcopy

from .util import DoesNotExist, is_mutable


[docs]class ChangeTrackingDescriptor(object): """ Descriptor that wraps model attributes to detect changes. Not all fields in older versions of Django are represented by descriptors themselves, so we handle both getting/setting bare attributes on the model and calling out to descriptors if they exist. """ def __init__(self, name, django_descriptor=None): self.name = name self.django_descriptor = django_descriptor def __get__(self, instance=None, owner=None): if instance is None: return self.django_descriptor if self.django_descriptor: value = self.django_descriptor.__get__(instance, owner) else: value = instance.__dict__.get(self.name, DoesNotExist) # We'll never have to check the value's mutability more than once, and # then only if it's ever accessed. If it's not mutable the only way # it'll change (normally) is through a call to our __set__, at which # point the original value will end up in _changed_fields. if not ( self.name in instance.__dict__['_mutability_checked'] or self.name in instance.__dict__['_changed_fields'] or self.name in instance.__dict__['_mutable_fields'] ): if is_mutable(value): instance.__dict__['_mutable_fields'][self.name] = deepcopy(value) instance.__dict__['_mutability_checked'].add(self.name) return value def __set__(self, instance, value): if self.name not in instance.__dict__['_mutable_fields']: old_value = instance.__dict__.get(self.name, DoesNotExist) if old_value is DoesNotExist and self.django_descriptor and hasattr(self.django_descriptor, 'cache_name'): old_value = instance.__dict__.get(self.django_descriptor.cache_name, DoesNotExist) if old_value is not DoesNotExist: if instance.__dict__['_changed_fields'].get(self.name, DoesNotExist) == value: instance.__dict__['_changed_fields'].pop(self.name, None) elif value != old_value: # Unfortunately we need to make a deep copy here, which is # a bit more expensive than a shallow copy. This is to # avoid situations like: # # >>> m = Model.objects.get(pk=1) # ... m.mutable_attr # [1, 2, 3] # >>> reference = m.mutable_attr # >>> m.mutable_attr = None # >>> reference.append(4) # >>> reference # [1, 2, 3, 4] # >>> m.revert_fields('mutable_attr') # >>> m.mutable_attr == reference # True # # It's an edge case, to be sure, but one we can't see coming # without likely worse solutions (such as checking *all* # attributes for immutability on model # instantiation/refresh). instance.__dict__['_changed_fields'].setdefault(self.name, deepcopy(old_value)) if self.django_descriptor and hasattr(self.django_descriptor, '__set__'): self.django_descriptor.__set__(instance, value) else: instance.__dict__[self.name] = value
[docs]def _inject_descriptors(cls): """ Iterates over concrete fields in a model and wraps them in a descriptor to \ track their changes. """ for field in cls._meta.concrete_fields: setattr(cls, field.attname, ChangeTrackingDescriptor(field.attname, cls.__dict__.get(field.attname))) if field.attname != field.name: setattr(cls, field.name, ChangeTrackingDescriptor(field.name, cls.__dict__.get(field.name)))