summaryrefslogtreecommitdiff
path: root/tests/queries/tests.py
diff options
context:
space:
mode:
authorKeryn Knight <keryn@kerynknight.com>2021-07-20 13:04:51 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2026-03-11 18:05:44 +0100
commit8d8a8713432a88737c4400610eef11c5c8457b86 (patch)
tree5df6a43a6e620a1ad1eba5c935ed8d7c097fe002 /tests/queries/tests.py
parent3483bfc0920b0ef0b28563aabe8ff546699b6ece (diff)
Refs #28455 -- Implemented private API methods for preventing QuerySet cloning.
Multiple calls are idempotent assuming they're balanced. Also, multiple calls to disable cloning followed by a single call to re-enable cloning will subsequently cause clones to occur - it is not a stack, just a toggle. @contextlib.contextmanager is intentionally not used for performance reasons: - decorator takes 1.1µs to execute, or 2µs if used correctly in a `with ...:` statement - custom class takes 300ns to execute, or 900ns if used correctly in a `with ...:` statement Based on work originally done by Anssi Kääriäinen and Tim Graham.
Diffstat (limited to 'tests/queries/tests.py')
-rw-r--r--tests/queries/tests.py46
1 files changed, 46 insertions, 0 deletions
diff --git a/tests/queries/tests.py b/tests/queries/tests.py
index 74929e4944..af657b2580 100644
--- a/tests/queries/tests.py
+++ b/tests/queries/tests.py
@@ -4629,3 +4629,49 @@ class Ticket23622Tests(TestCase):
set(Ticket23605A.objects.filter(qy).values_list("pk", flat=True)),
)
self.assertSequenceEqual(Ticket23605A.objects.filter(qx), [a2])
+
+
+class QuerySetCloningTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ SimpleCategory.objects.bulk_create(
+ [
+ SimpleCategory(name="first"),
+ SimpleCategory(name="second"),
+ SimpleCategory(name="third"),
+ SimpleCategory(name="fourth"),
+ ]
+ )
+
+ def test_context_manager(self):
+ """
+ _avoid_cloning() makes modifications apply to the original QuerySet.
+ """
+ qs = SimpleCategory.objects.all()
+ with qs._avoid_cloning():
+ qs2 = qs.filter(name__in={"first", "second"}).exclude(name="second")
+ self.assertIs(qs2, qs)
+ qs3 = qs2.exclude(name__in={"third", "fourth"})
+ # qs3 is not a mutation of qs2 (which is actually also qs) but a new
+ # instance entirely.
+ self.assertIsNot(qs3, qs)
+ self.assertIsNot(qs3, qs2)
+
+ def test_explicit_toggling(self):
+ qs = SimpleCategory.objects.filter(name__in={"first", "second"})
+ qs2 = qs._disable_cloning()
+ # The _disable_cloning() method doesn't return a new QuerySet, but
+ # toggles the value on the current instance. qs2 can be ignored.
+ self.assertIs(qs2, qs)
+ qs3 = qs.filter(name__in={"first", "second"})
+ qs3 = qs3.exclude(name="second")
+ qs3._enable_cloning()
+ # These are still both references to the same QuerySet, despite
+ # re-binding as if they were normal chained operations providing new
+ # QuerySet instances.
+ self.assertIs(qs3, qs)
+ qs3 = qs3.filter(name="second")
+ # Cloning has been re-enabled so subsequent operations yield a new
+ # QuerySet. qs3 is now all of the filters applied to qs + an additional
+ # filter.
+ self.assertIsNot(qs3, qs)