diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index 41830f06ba..b2181818ea 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -11,6 +11,7 @@ from django.db.migrations.migration import Migration from django.db.migrations.operations.models import AlterModelOptions from django.db.migrations.optimizer import MigrationOptimizer from django.db.migrations.questioner import MigrationQuestioner +from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject from django.utils import six from .topological_sort import stable_topological_sort @@ -62,6 +63,8 @@ class MigrationAutodetector(object): key: self.deep_deconstruct(value) for key, value in obj.items() } + elif isinstance(obj, COMPILED_REGEX_TYPE): + return RegexObject(obj) elif isinstance(obj, type): # If this is a type that implements 'deconstruct' as an instance method, # avoid treating this as being deconstructible itself - see #22951 diff --git a/django/db/migrations/utils.py b/django/db/migrations/utils.py new file mode 100644 index 0000000000..5bea82dc51 --- /dev/null +++ b/django/db/migrations/utils.py @@ -0,0 +1,12 @@ +import re + +COMPILED_REGEX_TYPE = type(re.compile('')) + + +class RegexObject(object): + def __init__(self, obj): + self.pattern = obj.pattern + self.flags = obj.flags + + def __eq__(self, other): + return self.pattern == other.pattern and self.flags == other.flags diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index 44a4017f30..fb6a162b38 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -14,6 +14,7 @@ from django.apps import apps from django.db import migrations, models from django.db.migrations.loader import MigrationLoader from django.db.migrations.operations.base import Operation +from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject from django.utils import datetime_safe, six from django.utils._os import upath from django.utils.encoding import force_text @@ -23,8 +24,6 @@ from django.utils.module_loading import module_dir from django.utils.timezone import utc from django.utils.version import get_docs_version -COMPILED_REGEX_TYPE = type(re.compile('')) - class SettingsReference(str): """ @@ -506,7 +505,7 @@ class MigrationWriter(object): format = "(%s)" if len(strings) != 1 else "(%s,)" return format % (", ".join(strings)), imports # Compiled regex - elif isinstance(value, COMPILED_REGEX_TYPE): + elif isinstance(value, (COMPILED_REGEX_TYPE, RegexObject)): imports = {"import re"} regex_pattern, pattern_imports = cls.serialize(value.pattern) regex_flags, flag_imports = cls.serialize(value.flags) diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 5caec12db8..8bee31c217 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- +import re + from django.conf import settings from django.contrib.auth.models import AbstractBaseUser +from django.core.validators import RegexValidator, validate_slug from django.db import connection, models from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.graph import MigrationGraph @@ -997,6 +1000,46 @@ class AutodetectorTests(TestCase): self.assertOperationAttributes(changes, "testapp", 0, 0, old_name="Author", new_name="NewAuthor") self.assertOperationAttributes(changes, "testapp", 0, 1, name="newauthor", table="author_three") + def test_identical_regex_doesnt_alter(self): + from_state = ModelState( + "testapp", "model", [("id", models.AutoField(primary_key=True, validators=[ + RegexValidator( + re.compile('^[-a-zA-Z0-9_]+\\Z'), + "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", + 'invalid' + ) + ]))] + ) + to_state = ModelState( + "testapp", "model", [("id", models.AutoField(primary_key=True, validators=[validate_slug]))] + ) + before = self.make_project_state([from_state]) + after = self.make_project_state([to_state]) + autodetector = MigrationAutodetector(before, after) + changes = autodetector._detect_changes() + # Right number/type of migrations? + self.assertNumberMigrations(changes, "testapp", 0) + + def test_different_regex_does_alter(self): + from_state = ModelState( + "testapp", "model", [("id", models.AutoField(primary_key=True, validators=[ + RegexValidator( + re.compile('^[a-z]+\\Z', 32), + "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", + 'invalid' + ) + ]))] + ) + to_state = ModelState( + "testapp", "model", [("id", models.AutoField(primary_key=True, validators=[validate_slug]))] + ) + before = self.make_project_state([from_state]) + after = self.make_project_state([to_state]) + autodetector = MigrationAutodetector(before, after) + changes = autodetector._detect_changes() + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["AlterField"]) + def test_empty_foo_together(self): """ #23452 - Empty unique/index_together shouldn't generate a migration.