Browse Source

Fixed #34355 -- Deprecated passing positional arguments to BaseConstraint.

Xavier Fernandez 2 years ago
parent
commit
ad18a0102c

+ 24 - 3
django/db/models/constraints.py

@@ -1,3 +1,4 @@
+import warnings
 from enum import Enum
 from types import NoneType
 
@@ -9,6 +10,7 @@ from django.db.models.lookups import Exact
 from django.db.models.query_utils import Q
 from django.db.models.sql.query import Query
 from django.db.utils import DEFAULT_DB_ALIAS
+from django.utils.deprecation import RemovedInDjango60Warning
 from django.utils.translation import gettext_lazy as _
 
 __all__ = ["BaseConstraint", "CheckConstraint", "Deferrable", "UniqueConstraint"]
@@ -18,12 +20,31 @@ class BaseConstraint:
     default_violation_error_message = _("Constraint “%(name)s” is violated.")
     violation_error_message = None
 
-    def __init__(self, name, violation_error_message=None):
+    # RemovedInDjango60Warning: When the deprecation ends, replace with:
+    # def __init__(self, *, name, violation_error_message=None):
+    def __init__(self, *args, name=None, violation_error_message=None):
+        # RemovedInDjango60Warning.
+        if name is None and not args:
+            raise TypeError(
+                f"{self.__class__.__name__}.__init__() missing 1 required keyword-only "
+                f"argument: 'name'"
+            )
         self.name = name
         if violation_error_message is not None:
             self.violation_error_message = violation_error_message
         else:
             self.violation_error_message = self.default_violation_error_message
+        # RemovedInDjango60Warning.
+        if args:
+            warnings.warn(
+                f"Passing positional arguments to {self.__class__.__name__} is "
+                f"deprecated.",
+                RemovedInDjango60Warning,
+                stacklevel=2,
+            )
+            for arg, attr in zip(args, ["name", "violation_error_message"]):
+                if arg:
+                    setattr(self, attr, arg)
 
     @property
     def contains_expressions(self):
@@ -67,7 +88,7 @@ class CheckConstraint(BaseConstraint):
             raise TypeError(
                 "CheckConstraint.check must be a Q instance or boolean expression."
             )
-        super().__init__(name, violation_error_message=violation_error_message)
+        super().__init__(name=name, violation_error_message=violation_error_message)
 
     def _get_check_sql(self, model, schema_editor):
         query = Query(model=model, alias_cols=False)
@@ -186,7 +207,7 @@ class UniqueConstraint(BaseConstraint):
             F(expression) if isinstance(expression, str) else expression
             for expression in expressions
         )
-        super().__init__(name, violation_error_message=violation_error_message)
+        super().__init__(name=name, violation_error_message=violation_error_message)
 
     @property
     def contains_expressions(self):

+ 3 - 0
docs/internals/deprecation.txt

@@ -18,6 +18,9 @@ details on these changes.
 * The ``DjangoDivFormRenderer`` and ``Jinja2DivFormRenderer`` transitional form
   renderers will be removed.
 
+* Support for passing positional arguments to ``BaseConstraint`` will be
+  removed.
+
 .. _deprecation-removed-in-5.1:
 
 5.1

+ 5 - 1
docs/ref/models/constraints.txt

@@ -48,12 +48,16 @@ option.
 ``BaseConstraint``
 ==================
 
-.. class:: BaseConstraint(name, violation_error_message=None)
+.. class:: BaseConstraint(*, name, violation_error_message=None)
 
     Base class for all constraints. Subclasses must implement
     ``constraint_sql()``, ``create_sql()``, ``remove_sql()`` and
     ``validate()`` methods.
 
+    .. deprecated:: 5.0
+
+        Support for passing positional arguments is deprecated.
+
 All constraints have the following parameters in common:
 
 ``name``

+ 4 - 0
docs/releases/5.0.txt

@@ -267,6 +267,10 @@ Miscellaneous
 * The ``DjangoDivFormRenderer`` and ``Jinja2DivFormRenderer`` transitional form
   renderers are deprecated.
 
+* Passing positional arguments  ``name`` and ``violation_error_message`` to
+  :class:`~django.db.models.BaseConstraint` is deprecated in favor of
+  keyword-only arguments.
+
 Features removed in 5.0
 =======================
 

+ 28 - 9
tests/constraints/tests.py

@@ -7,6 +7,8 @@ from django.db.models.constraints import BaseConstraint, UniqueConstraint
 from django.db.models.functions import Lower
 from django.db.transaction import atomic
 from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
+from django.test.utils import ignore_warnings
+from django.utils.deprecation import RemovedInDjango60Warning
 
 from .models import (
     ChildModel,
@@ -26,48 +28,48 @@ def get_constraints(table):
 
 class BaseConstraintTests(SimpleTestCase):
     def test_constraint_sql(self):
-        c = BaseConstraint("name")
+        c = BaseConstraint(name="name")
         msg = "This method must be implemented by a subclass."
         with self.assertRaisesMessage(NotImplementedError, msg):
             c.constraint_sql(None, None)
 
     def test_contains_expressions(self):
-        c = BaseConstraint("name")
+        c = BaseConstraint(name="name")
         self.assertIs(c.contains_expressions, False)
 
     def test_create_sql(self):
-        c = BaseConstraint("name")
+        c = BaseConstraint(name="name")
         msg = "This method must be implemented by a subclass."
         with self.assertRaisesMessage(NotImplementedError, msg):
             c.create_sql(None, None)
 
     def test_remove_sql(self):
-        c = BaseConstraint("name")
+        c = BaseConstraint(name="name")
         msg = "This method must be implemented by a subclass."
         with self.assertRaisesMessage(NotImplementedError, msg):
             c.remove_sql(None, None)
 
     def test_validate(self):
-        c = BaseConstraint("name")
+        c = BaseConstraint(name="name")
         msg = "This method must be implemented by a subclass."
         with self.assertRaisesMessage(NotImplementedError, msg):
             c.validate(None, None)
 
     def test_default_violation_error_message(self):
-        c = BaseConstraint("name")
+        c = BaseConstraint(name="name")
         self.assertEqual(
             c.get_violation_error_message(), "Constraint “name” is violated."
         )
 
     def test_custom_violation_error_message(self):
         c = BaseConstraint(
-            "base_name", violation_error_message="custom %(name)s message"
+            name="base_name", violation_error_message="custom %(name)s message"
         )
         self.assertEqual(c.get_violation_error_message(), "custom base_name message")
 
     def test_custom_violation_error_message_clone(self):
         constraint = BaseConstraint(
-            "base_name",
+            name="base_name",
             violation_error_message="custom %(name)s message",
         ).clone()
         self.assertEqual(
@@ -77,7 +79,7 @@ class BaseConstraintTests(SimpleTestCase):
 
     def test_deconstruction(self):
         constraint = BaseConstraint(
-            "base_name",
+            name="base_name",
             violation_error_message="custom %(name)s message",
         )
         path, args, kwargs = constraint.deconstruct()
@@ -88,6 +90,23 @@ class BaseConstraintTests(SimpleTestCase):
             {"name": "base_name", "violation_error_message": "custom %(name)s message"},
         )
 
+    def test_deprecation(self):
+        msg = "Passing positional arguments to BaseConstraint is deprecated."
+        with self.assertRaisesMessage(RemovedInDjango60Warning, msg):
+            BaseConstraint("name", "violation error message")
+
+    def test_name_required(self):
+        msg = (
+            "BaseConstraint.__init__() missing 1 required keyword-only argument: 'name'"
+        )
+        with self.assertRaisesMessage(TypeError, msg):
+            BaseConstraint()
+
+    @ignore_warnings(category=RemovedInDjango60Warning)
+    def test_positional_arguments(self):
+        c = BaseConstraint("name", "custom %(name)s message")
+        self.assertEqual(c.get_violation_error_message(), "custom name message")
+
 
 class CheckConstraintTests(TestCase):
     def test_eq(self):