I am getting a weird error after upgrading the Django version from 4.0 to 4.2.15.
The error being encountered is:
RecursionError: maximum recursion depth exceeded while calling a Python object
.
The ModelDiffMixin looks something like below:
from django.forms.models import model_to_dict
class ModelDiffMixin(object):
def __init__(self, *args, **kwargs):
super(ModelDiffMixin, self).__init__(*args, **kwargs)
self.__initial = self._dict
@property
def diff(self):
d1 = self.__initial
d2 = self._dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
return dict(diffs)
@property
def has_changed(self):
return bool(self.diff)
@property
def changed_fields(self):
return self.diff.keys()
def get_field_diff(self, field_name):
"""
Returns a diff for field if it's changed and None otherwise.
"""
return self.diff.get(field_name, None)
def save(self, *args, **kwargs):
"""
Saves model and set initial state.
"""
super(ModelDiffMixin, self).save(*args, **kwargs)
self.__initial = self._dict
@property
def _dict(self):
return model_to_dict(self, fields=[field.name for field in
self._meta.fields])
Referenced from the gist here:
https://gist.github.com/goloveychuk/72499a7251e070742f00
Attaching the stack trace here. I have gone through the django docs as well and it seems like accessing django fields in the init method of model might cause issues.
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/query.py", line 122, in __iter__
obj = model_cls.from_db(
^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/base.py", line 582, in from_db
new = cls(*values)
^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/base.py", line 571, in __init__
super().__init__()
File "/Users/bm/Desktop/Django/bapi/core/utils.py", line 2146, in __init__
self.__initial = self._dict
^^^^^^^^^^
File "/Users/bm/Desktop/Django/bapi/core/utils.py", line 2201, in _dict
return model_to_dict(self, fields=[field.name for field in self._meta.fields])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/forms/models.py", line 115, in model_to_dict
data[f.name] = f.value_from_object(instance)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/fields/__init__.py", line 1088, in value_from_object
return getattr(obj, self.attname)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/query_utils.py", line 178, in __get__
instance.refresh_from_db(fields=[field_name])
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/base.py", line 724, in refresh_from_db
db_instance = db_instance_qs.get()
^^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/query.py", line 633, in get
num = len(clone)
^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/query.py", line 380, in __len__
self._fetch_all()
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/query.py", line 1881, in _fetch_all
self._result_cache = list(self._iterable_class(self))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/query.py", line 91, in __iter__
results = compiler.execute_sql(
^^^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 1547, in execute_sql
sql, params = self.as_sql()
^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/psqlextra/compiler.py", line 76, in as_sql
sql, params = super().as_sql(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 734, in as_sql
extra_select, order_by, group_by = self.pre_sql_setup(
^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 85, in pre_sql_setup
order_by = self.get_order_by()
^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 457, in get_order_by
for expr, is_ref in self._order_by_pairs():
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 339, in _order_by_pairs
selected_exprs[expr] = pos_expr
~~~~~~~~~~~~~~^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/expressions.py", line 502, in __hash__
return hash(self.identity)
^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/utils/functional.py", line 57, in __get__
res = instance.__dict__[self.name] = self.func(instance)
^^^^^^^^^^^^^^^^^^^
File "/Users/bm/.virtualenvs/django4.2/lib/python3.11/site-packages/django/db/models/expressions.py", line 479, in identity
constructor_signature = inspect.signature(self.__init__)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.9/Frameworks/Python.framework/Versions/3.11/lib/python3.11/inspect.py", line 3263, in signature
return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.9/Frameworks/Python.framework/Versions/3.11/lib/python3.11/inspect.py", line 3011, in from_callable
return _signature_from_callable(obj, sigcls=cls,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.9/Frameworks/Python.framework/Versions/3.11/lib/python3.11/inspect.py", line 2447, in _signature_from_callable
_get_signature_of = functools.partial(_signature_from_callable,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RecursionError: maximum recursion depth exceeded while calling a Python object
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Accessing in Django models like:
class Member(ModelDiffMixin):
class Meta:
index_together = [['facebook_id'], ['enrollment_referrer']]
indexes = [models.Index(fields=['-created_ts'])]
member_name = models.CharField(max_length=100, null=True, blank=True)
middle_name = models.CharField(max_length=100, null=True, blank=True)
There is no change made in the code and the same code seems to work with Django 4.0 but not with Django 4.2.15. Would really appreciate if someone can help out with this.
---- Edited ------
Figured out the actual cause. The stack trace was misleading, and debugged bit deeper to find the piece of code that is making this happen. The error is because of serializing model instance:
from django.core import serializers
def get_serialized_instance(instance):
data = serializers.serialize('json', [instance])
result = json.loads(data)[0].get('fields')
result['id'] = instance.id
return result
This piece of code is the actual cause of recursion. It works with Django 4.0 but not with Django 4.2.15. Wonder if Django has dropped supporting serializing model instances.
Figured out the issue:
The dependency between two models caused the recursion issue.
class Offer(A, ModelDiffMixin):
offer_name = models.CharField(max_length=50)
locations = models.ManyToManyField(SponsorLocation)
class SponsorLocation(B, ModelDiffMixin):
sponsor = models.ForeignKey(Sponsor, on_delete=models.CASCADE)
When trying to serialize Offer model, the issue occured because of this particular line of code:
data = serializers.serialize('json', [instance])
ModelDiffMixin might be trying to track changes in the locations field by traversing the ManyToMany relationship between Offer and SponsorLocation. As it checks the changes in Offer, it is likely navigating to SponsorLocation, and since SponsorLocation also has ModelDiffMixin, it might be traversing back to Offer, causing a recursive loop.
The solution is sort of a hack, where the locations field is excluded from the serialization process and then appended later in the result.
def get_serialized_instance(instance):
if type(instance) == Offer:
fields = [
field.name
for field in instance._meta.get_fields()
if field.name not in ['locations']
]
data = serializers.serialize('json', [instance], fields=fields)
result = json.loads(data)[0].get('fields')
# Serialize the locations field separately for Offer model
result['locations'] = list(instance.locations.values_list('id', flat=True))
else:
data = serializers.serialize('json', [instance])
result = json.loads(data)[0].get('fields')
result['id'] = instance.id
return result