-
Notifications
You must be signed in to change notification settings - Fork 109
Description
Describe the bug
While running Django migrations, it can happen that subclasses of DirtyFieldsMixin get created (and later unreferenced, and thus destroyed). When a class gets unreferenced, the location that class holds in memory gets cleared up again, and a new Python object could be instantiated in that location. That new Python object might be another model class.
To Reproduce
See the attached project for a minimal example.
Running python manage.py migrate (after having installed the latest version of Django and django-dirtyfields) triggers the issue most of the time.
Expected behavior
Migrations always run completely pass.
Actual Behavior
Traceback (most recent call last):
File "/home/sjoerdjobpostmus/dev/minimal/./manage.py", line 22, in <module>
main()
~~~~^^
File "/home/sjoerdjobpostmus/dev/minimal/./manage.py", line 18, in main
execute_from_command_line(sys.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
utility.execute()
~~~~~~~~~~~~~~~^^
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/core/management/__init__.py", line 436, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/core/management/base.py", line 413, in run_from_argv
self.execute(*args, **cmd_options)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/core/management/base.py", line 459, in execute
output = self.handle(*args, **options)
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/core/management/base.py", line 107, in wrapper
res = handle_func(*args, **kwargs)
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/core/management/commands/migrate.py", line 357, in handle
post_migrate_state = executor.migrate(
targets,
...<3 lines>...
fake_initial=fake_initial,
)
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/db/migrations/executor.py", line 135, in migrate
state = self._migrate_all_forwards(
state, plan, full_plan, fake=fake, fake_initial=fake_initial
)
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/db/migrations/executor.py", line 167, in _migrate_all_forwards
state = self.apply_migration(
state, migration, fake=fake, fake_initial=fake_initial
)
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/db/migrations/executor.py", line 255, in apply_migration
state = migration.apply(state, schema_editor)
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/db/migrations/migration.py", line 132, in apply
operation.database_forwards(
~~~~~~~~~~~~~~~~~~~~~~~~~~~^
self.app_label, schema_editor, old_state, project_state
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/db/migrations/operations/special.py", line 196, in database_forwards
self.code(from_state.apps, schema_editor)
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/sjoerdjobpostmus/dev/minimal/stress/migrations/0002_auto_20241119_1734.py", line 28, in update_withoutdirty_instance
inst.save()
~~~~~~~~~^^
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/db/models/base.py", line 891, in save
self.save_base(
~~~~~~~~~~~~~~^
using=using,
^^^^^^^^^^^^
...<2 lines>...
update_fields=update_fields,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/db/models/base.py", line 1012, in save_base
post_save.send(
~~~~~~~~~~~~~~^
sender=origin,
^^^^^^^^^^^^^^
...<4 lines>...
using=using,
^^^^^^^^^^^^
)
^
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/django/dispatch/dispatcher.py", line 189, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/sjoerdjobpostmus/.virtualenvs/minimal/lib/python3.13/site-packages/dirtyfields/dirtyfields.py", line 172, in reset_state
new_state = instance._as_dict(check_relationship=True)
^^^^^^^^^^^^^^^^^
AttributeError: 'WithoutDirty' object has no attribute '_as_dict'
Environment (please complete the following information):
- OS: Debian (but also noticed under Ubuntu)
- Python version: 3.12 and 3.13.
- Django version: 4.2+
- django-dirtyfields version: 1.9.5 and newer.
Additional context
A related Django issue is https://code.djangoproject.com/ticket/35801#comment:6 . I'm not sure if either of the codebases can fix this bug separately though, or whether it requires changes in both projects.
Suggested improvements:
- add a class-attribute
registered_post_save_signal(or something) to make surepost_save.connectis only called once per model class. - In the code that is calling
post_save.connect, also add 'something' that makes sure thatpost_save.disconnect(reset_state, sender=self.__class__, dispatch_uid=...)gets called whenever the model class (not instance) gets unreferenced.