Add numeric conversion support to LazyObject

Enable int(), float(), and complex() on SimpleLazyObject to fix regression with nested subquery annotations.
This restores compatibility with querysets expecting numeric types and prevents TypeError when filtering with SimpleLazyObject.
Includes regression tests for numeric conversions and subquery usage.

Fixes regression described in #3543129822.
This commit is contained in:
utkarsh.arya@zomato.com 2025-11-15 23:01:25 +00:00
parent fa5e7e46d8
commit 02d1a8952e
3 changed files with 58 additions and 1 deletions

View file

@ -324,6 +324,11 @@ class LazyObject:
__str__ = new_method_proxy(str) __str__ = new_method_proxy(str)
__bool__ = new_method_proxy(bool) __bool__ = new_method_proxy(bool)
# Numeric conversion support
__int__ = new_method_proxy(int)
__float__ = new_method_proxy(float)
__complex__ = new_method_proxy(complex)
# Introspection support # Introspection support
__dir__ = new_method_proxy(dir) __dir__ = new_method_proxy(dir)

View file

@ -6,11 +6,12 @@ from operator import attrgetter
from django.core.exceptions import EmptyResultSet, FieldError from django.core.exceptions import EmptyResultSet, FieldError
from django.db import DEFAULT_DB_ALIAS, connection from django.db import DEFAULT_DB_ALIAS, connection
from django.db.models import Count, Exists, F, OuterRef, Q from django.db.models import Count, Exists, F, OuterRef, Q, Subquery
from django.db.models.sql.constants import LOUTER from django.db.models.sql.constants import LOUTER
from django.db.models.sql.where import NothingNode, WhereNode from django.db.models.sql.where import NothingNode, WhereNode
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
from django.utils.functional import SimpleLazyObject
from .models import ( from .models import (
FK1, Annotation, Article, Author, BaseA, Book, CategoryItem, FK1, Annotation, Article, Author, BaseA, Book, CategoryItem,
@ -2130,6 +2131,39 @@ class SubqueryTests(TestCase):
).order_by('id').values_list('id', flat=True), [2, 4] ).order_by('id').values_list('id', flat=True), [2, 4]
) )
def test_nested_subquery_with_simplelazyobject(self):
"""
Test that SimpleLazyObject works with nested subquery annotations.
Regression test for the issue where using SimpleLazyObject in a filter
with nested subquery annotation would fail with:
TypeError: int() argument must be a string, a bytes-like object or a
number, not 'SimpleLazyObject'
"""
# Create test data
category = NamedCategory.objects.create(id=5, name="test")
# Create a nested subquery annotation
# This simulates the scenario in the bug report where we have:
# - An outer query on DumbCategory
# - Annotated with a subquery on Tag
# - That subquery is itself annotated with a subquery on another model
# - And we filter using a SimpleLazyObject
tag_subquery = Tag.objects.filter(
category=OuterRef('pk')
).values('category')
# Wrap the category id in a SimpleLazyObject
# This is the key part - wrapping a numeric value (like a User.id)
lazy_category_id = SimpleLazyObject(lambda: category.id)
# This should not raise a TypeError
queryset = NamedCategory.objects.annotate(
tag_category=Subquery(tag_subquery)
).filter(tag_category=lazy_category_id)
# Force evaluation - this is where the bug would occur
str(queryset.query)
@skipUnlessDBFeature('allow_sliced_subqueries_with_in') @skipUnlessDBFeature('allow_sliced_subqueries_with_in')
class QuerySetBitwiseOperationTests(TestCase): class QuerySetBitwiseOperationTests(TestCase):

View file

@ -91,6 +91,24 @@ class LazyObjectTestCase(TestCase):
for t in [True, 1, (1,), {1: 2}, [1], object(), {1}]: for t in [True, 1, (1,), {1: 2}, [1], object(), {1}]:
self.assertTrue(t) self.assertTrue(t)
def test_int(self):
obj = self.lazy_wrap(42)
self.assertEqual(int(obj), 42)
obj = self.lazy_wrap(3.14)
self.assertEqual(int(obj), 3)
def test_float(self):
obj = self.lazy_wrap(42)
self.assertEqual(float(obj), 42.0)
obj = self.lazy_wrap(3.14)
self.assertEqual(float(obj), 3.14)
def test_complex(self):
obj = self.lazy_wrap(42)
self.assertEqual(complex(obj), 42+0j)
obj = self.lazy_wrap(3.14)
self.assertEqual(complex(obj), 3.14+0j)
def test_dir(self): def test_dir(self):
obj = self.lazy_wrap('foo') obj = self.lazy_wrap('foo')
self.assertEqual(dir(obj), dir('foo')) self.assertEqual(dir(obj), dir('foo'))