From a84c3afdddfe75520ac2caf92d99bda580c793bc Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 28 Jun 2018 13:09:42 -0700 Subject: [PATCH] Fix behavior of .previous() in Django 1.10+. The complications are that when the attribute is set in Django 1.10, it no longer counts as a deferred attribute, and it is not retrieved from the database. Naively updating __set__ to retrieve the value if it is deferred leads to infinite recursion because accessing the attribute involves loading data from the database and trying to set the attribute based on that value. This commit introduces a somewhat hacky flag that records whether we're already trying to set the attribute further up in the call stack. --- model_utils/tracker.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index e69d177..a145b1a 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -46,29 +46,40 @@ class DescriptorWrapper(object): tracker_instance.saved_data[self.field_name] = deepcopy(value) return value + def __set__(self, instance, value): + initialized = hasattr(instance, '_instance_intialized') + was_deferred = self.field_name in instance.get_deferred_fields() + + # Sentinel attribute to detect whether we are already trying to + # set the attribute higher up the stack. This prevents infinite + # recursion when retrieving deferred values from the database. + recursion_sentinel_attname = '_setting_' + self.field_name + already_setting = hasattr(instance, recursion_sentinel_attname) + + if initialized and was_deferred and not already_setting: + setattr(instance, recursion_sentinel_attname, True) + try: + # Retrieve the value to set the saved_data value. + # This will undefer the field + getattr(instance, self.field_name) + finally: + if already_setting: + instance.__dict__.pop(recursion_sentinel_attname, None) + if hasattr(self.descriptor, '__set__'): + self.descriptor.__set__(instance, value) + else: + instance.__dict__[self.field_name] = value + @staticmethod def cls_for_descriptor(descriptor): - has_set = hasattr(descriptor, '__set__') has_del = hasattr(descriptor, '__delete__') if has_del: return FullDescriptorWrapper - elif has_set: - return SettableDescriptorWrapper else: return DescriptorWrapper -class SettableDescriptorWrapper(DescriptorWrapper): - """ - Descriptor wrapper for descriptors with a __delete__ method. - - This should not be used for descriptors - """ - def __set__(self, instance, value): - return self.descriptor.__set__(instance, value) - - -class FullDescriptorWrapper(SettableDescriptorWrapper): +class FullDescriptorWrapper(DescriptorWrapper): """ Wrapper for descriptors with all three descriptor methods. """ @@ -222,6 +233,7 @@ class FieldTracker(object): setattr(instance, self.attname, tracker) tracker.set_saved_fields() self.patch_save(instance) + instance._instance_intialized = True def patch_save(self, instance): original_save = instance.save