ソースを参照

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 math
+import warnings
 from copy import copy
 
 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.utils.datastructures import OrderedSet
+from django.utils.deprecation import RemovedInDjango40Warning
 from django.utils.functional import cached_property
 
 
@@ -463,6 +465,17 @@ class IsNull(BuiltinLookup):
     prepare_rhs = False
 
     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)
         if self.rhs:
             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 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
 details on these changes.
 

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

@@ -3281,6 +3281,11 @@ SQL equivalent:
 
     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
 
 ``regex``

+ 3 - 0
docs/releases/3.1.txt

@@ -274,6 +274,9 @@ Miscellaneous
 * ``PASSWORD_RESET_TIMEOUT_DAYS`` setting is deprecated in favor of
   :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:
 
 Features removed in 3.1

+ 12 - 0
tests/lookup/models.py

@@ -96,3 +96,15 @@ class Product(models.Model):
 class Stock(models.Model):
     product = models.ForeignKey(Product, models.CASCADE)
     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.functions import Substr
 from django.test import TestCase, skipUnlessDBFeature
+from django.utils.deprecation import RemovedInDjango40Warning
 
 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')
         authors = Author.objects.filter(id=authors_max_ids[:1])
         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()