Преглед изворни кода

Fixed #30841 -- Deprecated using non-boolean values for isnull lookup.

André Ericson пре 5 година
родитељ
комит
31174031f1

+ 13 - 0
django/db/models/lookups.py

@@ -1,5 +1,6 @@
 import itertools
 import itertools
 import math
 import math
+import warnings
 from copy import copy
 from copy import copy
 
 
 from django.core.exceptions import EmptyResultSet
 from django.core.exceptions import EmptyResultSet
@@ -9,6 +10,7 @@ from django.db.models.fields import (
 )
 )
 from django.db.models.query_utils import RegisterLookupMixin
 from django.db.models.query_utils import RegisterLookupMixin
 from django.utils.datastructures import OrderedSet
 from django.utils.datastructures import OrderedSet
+from django.utils.deprecation import RemovedInDjango40Warning
 from django.utils.functional import cached_property
 from django.utils.functional import cached_property
 
 
 
 
@@ -463,6 +465,17 @@ class IsNull(BuiltinLookup):
     prepare_rhs = False
     prepare_rhs = False
 
 
     def as_sql(self, compiler, connection):
     def as_sql(self, compiler, connection):
+        if not isinstance(self.rhs, bool):
+            # When the deprecation ends, replace with:
+            # raise ValueError(
+            #     'The QuerySet value for an isnull lookup must be True or '
+            #     'False.'
+            # )
+            warnings.warn(
+                'Using a non-boolean value for an isnull lookup is '
+                'deprecated, use True or False instead.',
+                RemovedInDjango40Warning,
+            )
         sql, params = compiler.compile(self.lhs)
         sql, params = compiler.compile(self.lhs)
         if self.rhs:
         if self.rhs:
             return "%s IS NULL" % sql, params
             return "%s IS NULL" % sql, params

+ 3 - 0
docs/internals/deprecation.txt

@@ -36,6 +36,9 @@ details on these changes.
 
 
 * The ``PASSWORD_RESET_TIMEOUT_DAYS`` setting will be removed.
 * The ``PASSWORD_RESET_TIMEOUT_DAYS`` setting will be removed.
 
 
+* The undocumented usage of the :lookup:`isnull` lookup with non-boolean values
+  as the right-hand side will no longer be allowed.
+
 See the :ref:`Django 3.1 release notes <deprecated-features-3.1>` for more
 See the :ref:`Django 3.1 release notes <deprecated-features-3.1>` for more
 details on these changes.
 details on these changes.
 
 

+ 5 - 0
docs/ref/models/querysets.txt

@@ -3281,6 +3281,11 @@ SQL equivalent:
 
 
     SELECT ... WHERE pub_date IS NULL;
     SELECT ... WHERE pub_date IS NULL;
 
 
+.. deprecated:: 3.1
+
+    Using non-boolean values as the right-hand side is deprecated, use ``True``
+    or ``False`` instead. In Django 4.0, the exception will be raised.
+
 .. fieldlookup:: regex
 .. fieldlookup:: regex
 
 
 ``regex``
 ``regex``

+ 3 - 0
docs/releases/3.1.txt

@@ -274,6 +274,9 @@ Miscellaneous
 * ``PASSWORD_RESET_TIMEOUT_DAYS`` setting is deprecated in favor of
 * ``PASSWORD_RESET_TIMEOUT_DAYS`` setting is deprecated in favor of
   :setting:`PASSWORD_RESET_TIMEOUT`.
   :setting:`PASSWORD_RESET_TIMEOUT`.
 
 
+* The undocumented usage of the :lookup:`isnull` lookup with non-boolean values
+  as the right-hand side is deprecated, use ``True`` or ``False`` instead.
+
 .. _removed-features-3.1:
 .. _removed-features-3.1:
 
 
 Features removed in 3.1
 Features removed in 3.1

+ 12 - 0
tests/lookup/models.py

@@ -96,3 +96,15 @@ class Product(models.Model):
 class Stock(models.Model):
 class Stock(models.Model):
     product = models.ForeignKey(Product, models.CASCADE)
     product = models.ForeignKey(Product, models.CASCADE)
     qty_available = models.DecimalField(max_digits=6, decimal_places=2)
     qty_available = models.DecimalField(max_digits=6, decimal_places=2)
+
+
+class Freebie(models.Model):
+    gift_product = models.ForeignKey(Product, models.CASCADE)
+    stock_id = models.IntegerField(blank=True, null=True)
+
+    stock = models.ForeignObject(
+        Stock,
+        from_fields=['stock_id', 'gift_product'],
+        to_fields=['id', 'product'],
+        on_delete=models.CASCADE,
+    )

+ 23 - 1
tests/lookup/tests.py

@@ -9,9 +9,10 @@ from django.db.models import Max
 from django.db.models.expressions import Exists, OuterRef
 from django.db.models.expressions import Exists, OuterRef
 from django.db.models.functions import Substr
 from django.db.models.functions import Substr
 from django.test import TestCase, skipUnlessDBFeature
 from django.test import TestCase, skipUnlessDBFeature
+from django.utils.deprecation import RemovedInDjango40Warning
 
 
 from .models import (
 from .models import (
-    Article, Author, Game, IsNullWithNoneAsRHS, Player, Season, Tag,
+    Article, Author, Freebie, Game, IsNullWithNoneAsRHS, Player, Season, Tag,
 )
 )
 
 
 
 
@@ -969,3 +970,24 @@ class LookupTests(TestCase):
         ).values('max_id')
         ).values('max_id')
         authors = Author.objects.filter(id=authors_max_ids[:1])
         authors = Author.objects.filter(id=authors_max_ids[:1])
         self.assertEqual(authors.get(), newest_author)
         self.assertEqual(authors.get(), newest_author)
+
+    def test_isnull_non_boolean_value(self):
+        # These tests will catch ValueError in Django 4.0 when using
+        # non-boolean values for an isnull lookup becomes forbidden.
+        # msg = (
+        #     'The QuerySet value for an isnull lookup must be True or False.'
+        # )
+        msg = (
+            'Using a non-boolean value for an isnull lookup is deprecated, '
+            'use True or False instead.'
+        )
+        tests = [
+            Author.objects.filter(alias__isnull=1),
+            Article.objects.filter(author__isnull=1),
+            Season.objects.filter(games__isnull=1),
+            Freebie.objects.filter(stock__isnull=1),
+        ]
+        for qs in tests:
+            with self.subTest(qs=qs):
+                with self.assertWarnsMessage(RemovedInDjango40Warning, msg):
+                    qs.exists()