From 825ddda26a14847c30522f4d1112fb506123420d Mon Sep 17 00:00:00 2001 From: Thibaut Decombe Date: Sat, 26 Apr 2025 17:02:57 +0200 Subject: [PATCH] Fixed #33174 -- Fixed migrations crash for model inheriting from Generic[T]. --- django/db/migrations/state.py | 2 + .../with_generic_model/__init__.py | 0 .../migrations/0001_initial.py | 40 +++++++++++++++++++ .../migrations/0002_customgenericmodel.py | 31 ++++++++++++++ .../with_generic_model/migrations/__init__.py | 0 .../with_generic_model/models.py | 36 +++++++++++++++++ tests/migrations/test_commands.py | 8 ++++ 7 files changed, 117 insertions(+) create mode 100644 tests/migrations/migrations_test_apps/with_generic_model/__init__.py create mode 100644 tests/migrations/migrations_test_apps/with_generic_model/migrations/0001_initial.py create mode 100644 tests/migrations/migrations_test_apps/with_generic_model/migrations/0002_customgenericmodel.py create mode 100644 tests/migrations/migrations_test_apps/with_generic_model/migrations/__init__.py create mode 100644 tests/migrations/migrations_test_apps/with_generic_model/models.py diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 92716aa5d6..fb0b3093c1 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -1,4 +1,5 @@ import copy +import typing from collections import defaultdict from contextlib import contextmanager from functools import partial @@ -969,6 +970,7 @@ class ModelState: bases = tuple( (apps.get_model(base) if isinstance(base, str) else base) for base in self.bases + if base != typing.Generic ) except LookupError: raise InvalidBasesError( diff --git a/tests/migrations/migrations_test_apps/with_generic_model/__init__.py b/tests/migrations/migrations_test_apps/with_generic_model/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/migrations_test_apps/with_generic_model/migrations/0001_initial.py b/tests/migrations/migrations_test_apps/with_generic_model/migrations/0001_initial.py new file mode 100644 index 0000000000..efb998918a --- /dev/null +++ b/tests/migrations/migrations_test_apps/with_generic_model/migrations/0001_initial.py @@ -0,0 +1,40 @@ +import typing + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + operations = [ + migrations.CreateModel( + name="GenericModel", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + bases=(typing.Generic, models.Model), + ), + migrations.CreateModel( + name="GenericModelPEP695", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + bases=(models.Model, typing.Generic), + ), + ] diff --git a/tests/migrations/migrations_test_apps/with_generic_model/migrations/0002_customgenericmodel.py b/tests/migrations/migrations_test_apps/with_generic_model/migrations/0002_customgenericmodel.py new file mode 100644 index 0000000000..7d40eec5bf --- /dev/null +++ b/tests/migrations/migrations_test_apps/with_generic_model/migrations/0002_customgenericmodel.py @@ -0,0 +1,31 @@ +from django.db import migrations, models + +from ..models import Child + + +class Migration(migrations.Migration): + + dependencies = [ + ("with_generic_model", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="CustomGenericModel", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + bases=( + Child, + models.Model, + ), + ), + ] diff --git a/tests/migrations/migrations_test_apps/with_generic_model/migrations/__init__.py b/tests/migrations/migrations_test_apps/with_generic_model/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/migrations_test_apps/with_generic_model/models.py b/tests/migrations/migrations_test_apps/with_generic_model/models.py new file mode 100644 index 0000000000..2cedbe2f5b --- /dev/null +++ b/tests/migrations/migrations_test_apps/with_generic_model/models.py @@ -0,0 +1,36 @@ +import typing + +from django.db import models + +T = typing.TypeVar("T") + + +class GenericModel(typing.Generic[T], models.Model): + """A model inheriting from typing.Generic.""" + + +class GenericModelPEP695[T](models.Model): + """A model inheriting from typing.Generic via the PEP 695 syntax.""" + + +# Example from Python docs: +# https://typing.python.org/en/latest/spec/generics.html#arbitrary-generic-types-as-base-classes +T1 = typing.TypeVar("T1") +T2 = typing.TypeVar("T2") +T3 = typing.TypeVar("T3") + + +class Parent1(typing.Generic[T1, T2]): + pass + + +class Parent2(typing.Generic[T1, T2]): + pass + + +class Child(Parent1[T1, T3], Parent2[T2, T3]): + pass + + +class CustomGenericModel(Child[T1, T3, T2], models.Model): + """A model inheriting from a custom subclass of typing.Generic.""" diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 00f97c5f3a..f1373eca1e 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -1551,6 +1551,14 @@ class MigrateTests(MigrationTestBase): recorder.record_unapplied("migrations2", "0002_second") recorder.record_unapplied("migrations2", "0001_squashed_0002") + @override_settings( + INSTALLED_APPS=[ + "migrations.migrations_test_apps.with_generic_model", + ] + ) + def test_migrate_model_inherit_generic(self): + call_command("migrate", verbosity=0) + class MakeMigrationsTests(MigrationTestBase): """