Commit 91f701f4 authored by Markus Holtermann's avatar Markus Holtermann
Browse files

Fixed #25280 -- Properly checked regex objects for equality to prevent infinite migrations

Thanks Sayid Munawar and Tim Graham for the report, investigation and
review.
parent 11750276
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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
+12 −0
Original line number Diff line number Diff line
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
+2 −3
Original line number Diff line number Diff line
@@ -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)
+43 −0
Original line number Diff line number Diff line
# -*- 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.