#19362 closed Bug (fixed)
Recursion error when deleting model object through admin
Reported by: | Mark Wolf | Owned by: | nobody |
---|---|---|---|
Component: | Python 2 | Version: | 1.5-alpha-1 |
Severity: | Release blocker | Keywords: | python2.7.3 |
Cc: | Triage Stage: | Accepted | |
Has patch: | yes | Needs documentation: | no |
Needs tests: | yes | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
When I try to delete a specific object using the admin interface I get an error:
RuntimeError at /admin/gtd/node/1470/delete/ maximum recursion depth exceeded while calling a Python object
It appears to be trying to convert something to unicode but I don't know what. From the Django error message I can see that the object appears as <Node: [<span style="color: rgba(230, 138, 0, 1.0)"><strong>DFRD</strong></span>] Test todo item>
, which doesn't seem to have any weird characters in it. The title is what I expect to be return from __str__(self)
as described below.
If I delete a different object (which has similar HTML markup in it) from the same model it works with no errors. The model in question defines __str__(self)
which returns the value from a models.TextField() attribute wrapped in some HTML (<span style="..."></span>) which is passed through mark_safe() before being returned. The model is decorated with @python_2_unicode_compatible
. Following the python3 migration guide, I added from __future__ import unicode_literals
at the top of my models.py and removed __unicode__(self)
but no difference.
The stacktrace doesn't seem to go through my code anywhere.
I git-pulled the latest changes to stable/1.5.x from github and reinstalled Django after removed the previous installation from dist-packages but the problem persists.
I have not tried deleting this object pythonically; I figured I'd keep that object for now in case anyone needs me to reproduce the problem.
Stacktrace: https://gist.github.com/4148524
Change History (11)
comment:1 by , 12 years ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:2 by , 12 years ago
Anssi first encountered the problem by checking out stable/1.4.x, running the pagination tests, checking out master, and running the pagination tests again.
(The pagination tests were moved from modeltests
to regressiontests
; the sequence above leaves stale .pyc
files in modeltests/pagination
.)
At least, that gives us a reproducible way to trigger this infinite recursion.
comment:3 by , 12 years ago
Indeed, replacing force_text(self)
with self.__unicode__()
makes the recursion go away. But replacing it with unicode(self)
(ortype(self).__unicode__(self)
, which is how the interpreter computes unicode(self)
) brings it back.
In the situation explained just above, it seems that Django will run the tests from the stale .pyc
in modeltests
, but the Article
model from regressiontests
is also loaded in the app cache, and that causes problems.
comment:4 by , 12 years ago
Here's what happens when a subclass of models.Model
that doesn't define __str__
is decorated with python_2_unicode_compatible
:
- when the decorator is executed:
- its
__unicode__
method points to the__str__
method frommodels.Model
— which returns astr
and not aunicode
as expected bypython_2_unicode_compatible
- its
__str__
method is a new method that calls__unicode__
and returns the result encoded as utf-8
- its
- when attempting to create the unicode representation of an instance, say,
obj
:- the interpreter treats
unicode(obj)
astype(obj).__unicode__(obj)
- in this case, that's actually
models.Model.__str__(obj)
- this function calls
force_text(obj)
, which callsobj.__unicode__()
— and that's alsomodels.Model.__str__(obj)
, hence the infinite recursion.
- the interpreter treats
I wish I could just remove the definition of models.Model.__str__
, but after having spent years educating developers to write a __unicode__
method for their models, this isn't a viable option.
It's possible to catch this programming mistake and raise an explicit exception at runtime like this:
--- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -416,6 +416,11 @@ class Model(six.with_metaclass(ModelBase, object)): def __str__(self): if not six.PY3 and hasattr(self, '__unicode__'): + if type(self).__unicode__ == Model.__str__: + klass_name = type(self).__name__ + raise RuntimeError("%s.__unicode__ is aliased to __str__. Did" + " you apply @python_2_unicode_compatible" + " without defining __str__?" % klass_name) return force_text(self).encode('utf-8') return '%s object' % self.__class__.__name__
That ugly, but it works... It's more difficult to catch the exception at compile time because python_2_unicode_compatible
is defined in django.utils.encoding
, and I don't want to import django.db.models.Model
there.
m3wolf, I have an explanation for why you can delete one instance and not another of the same model.
You probably have a foreign key pointing from an instance of another model to the instance you can't delete, and that other model has @python_2_unicode_compatible
but no __str__
method. When Django tries to display that instance to warn you about the cascade deletion, it enters the infinite loop.
If you apply the patch above, Django should tell you which model has this issue. Could you test that and report your results here?
comment:5 by , 12 years ago
Thanks, aaugustine. Applied your patch and it correctly identified the offending model. Defined __str__
for that model and I can now delete the instance.
comment:6 by , 12 years ago
Component: | Uncategorized → Python 2 |
---|---|
Has patch: | set |
Needs tests: | set |
Severity: | Normal → Release blocker |
comment:7 by , 12 years ago
Resolution: | → fixed |
---|---|
Status: | new → closed |
comment:9 by , 11 years ago
#21198 suggests a better approach: raising an exception at compile time rather than at runtime.
I was able to reproduce similar stack trace with:
I don't get a similar stack trace if I define
__str__
instead of__unicode__
or if I don't have python_2_unicode_compatible.I have also seen the same stack trace somewhere else, don't remember where and why.
There seems to be something strange going on. In the following code
if I replace the force_text(self) with
self.__unicode__()
then the recursion goes away. But to me it seems calling the__unicode__
method is the only thing that force_text is actually doing (apart of a couple of isinstance checks).In any case it seems the
__unicode__
+ force_text() form an endless loop in some cases. I am marking this as accepted as it seems likely there is something strange going on in here. Although it is possible this is some kind of user error after all.