Skip to content

Commit 141b89e

Browse files
authored
Implement aget, acount, and aexists. (#103)
1 parent 9510431 commit 141b89e

File tree

4 files changed

+140
-10
lines changed

4 files changed

+140
-10
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ Bugfixes
1616
Improvements
1717
------------
1818

19-
* Initial support for `asynchronous queries`_. (`#99 <https://github.com/clokep/django-querysetsequence/pull/99>`_)
19+
* Initial support for `asynchronous queries`_. (`#99 <https://github.com/clokep/django-querysetsequence/pull/99>`_,
20+
`#103 <https://github.com/clokep/django-querysetsequence/pull/103>`_)
2021

2122
.. _asynchronous queries: https://docs.djangoproject.com/en/4.1/topics/db/queries/#async-queries
2223

docs/api.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Summary of Supported APIs
125125
- |check|
126126
- See [1]_ for information on the ``QuerySet`` lookup: ``'#'``.
127127
* - |aget|_
128-
- |xmark|
128+
- |check|
129129
-
130130
* - |create|_
131131
- |xmark|
@@ -161,7 +161,7 @@ Summary of Supported APIs
161161
- |check|
162162
-
163163
* - |acount|_
164-
- |xmark|
164+
- |check|
165165
-
166166
* - |in_bulk|_
167167
- |xmark|
@@ -213,7 +213,7 @@ Summary of Supported APIs
213213
- |check|
214214
-
215215
* - |aexists|_
216-
- |xmark|
216+
- |check|
217217
-
218218
* - |contains|_
219219
- |check|

queryset_sequence/__init__.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import functools
23
from collections import defaultdict
34
from itertools import dropwhile
@@ -958,7 +959,35 @@ def get(self, **kwargs):
958959
if django.VERSION >= (4, 1):
959960

960961
async def aget(self, **kwargs):
961-
raise NotImplementedError()
962+
clone = self.filter(**kwargs)
963+
964+
awaitables = []
965+
for qs in clone._querysets:
966+
awaitables.append(qs.aget())
967+
968+
result = None
969+
results = await asyncio.gather(*awaitables, return_exceptions=True)
970+
for obj in results:
971+
# If the object doesn't exist, hopefully another QuerySet has it.
972+
if isinstance(obj, ObjectDoesNotExist):
973+
continue
974+
975+
# Re-raise a MultipleObjectsReturned() exception.
976+
if isinstance(obj, MultipleObjectsReturned):
977+
raise obj
978+
979+
# If a second object is found, raise an exception.
980+
if result:
981+
raise MultipleObjectsReturned()
982+
983+
result = obj
984+
985+
# Checked all QuerySets and no object was found.
986+
if result is None:
987+
raise self.model.DoesNotExist()
988+
989+
# Return the only result found.
990+
return result
962991

963992
def create(self, **kwargs):
964993
raise NotImplementedError()
@@ -1020,7 +1049,8 @@ def count(self):
10201049
if django.VERSION >= (4, 1):
10211050

10221051
async def acount(self):
1023-
raise NotImplementedError()
1052+
awaitables = [qs.acount() for qs in self._querysets]
1053+
return sum(await asyncio.gather(*awaitables)) - self._low_mark
10241054

10251055
def in_bulk(self, id_list=None, *, field_name="pk"):
10261056
raise NotImplementedError()
@@ -1176,7 +1206,8 @@ def exists(self):
11761206
if django.VERSION >= (4, 1):
11771207

11781208
async def aexists(self):
1179-
raise NotImplementedError()
1209+
awaitables = [qs.aexists() for qs in self._querysets]
1210+
return any(await asyncio.gather(*awaitables, return_exceptions=True))
11801211

11811212
def contains(self, obj):
11821213
return any(qs.contains(obj) for qs in self._querysets)

tests/test_querysetsequence.py

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ def test_count(self):
119119
with self.assertNumQueries(2):
120120
self.assertEqual(self.all.count(), 5)
121121

122+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
123+
async def test_acount(self):
124+
# The proper length should be returned via database queries.
125+
self.assertEqual(await self.all.acount(), 5)
126+
122127
def test_len(self):
123128
# Calling len() evaluates the QuerySet.
124129
with self.assertNumQueries(2):
@@ -138,10 +143,20 @@ def test_slice(self):
138143
with self.assertNumQueries(4):
139144
self.assertEqual(len(self.all[1:]), 4)
140145

146+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
147+
async def test_acount_slice(self):
148+
"""Ensure the proper length is calculated when a slice is taken."""
149+
self.assertEqual(await self.all[1:].acount(), 4)
150+
141151
def test_empty_count(self):
142152
"""An empty QuerySetSequence has a count of 0."""
143153
self.assertEqual(self.empty.count(), 0)
144154

155+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
156+
async def test_acount_empty_count(self):
157+
"""An empty QuerySetSequence has a count of 0."""
158+
self.assertEqual(await self.empty.acount(), 0)
159+
145160
def test_empty_len(self):
146161
"""An empty QuerySetSequence has a count of 0."""
147162
self.assertEqual(len(self.empty), 0)
@@ -1380,45 +1395,95 @@ def test_get(self):
13801395
self.assertEqual(book.title, "Biography")
13811396
self.assertIsInstance(book, Book)
13821397

1398+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1399+
async def test_aget(self):
1400+
"""
1401+
Ensure that aget() returns the expected element or raises DoesNotExist.
1402+
"""
1403+
# Get a particular item.
1404+
book = await self.all.aget(title="Biography")
1405+
self.assertEqual(book.title, "Biography")
1406+
self.assertIsInstance(book, Book)
1407+
13831408
def test_not_found(self):
13841409
# An exception is raised if get() is called and nothing is found.
13851410
with self.assertNumQueries(2):
13861411
with self.assertRaises(ObjectDoesNotExist):
13871412
self.all.get(title="")
13881413

1414+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1415+
async def test_aget_not_found(self):
1416+
with self.assertRaises(ObjectDoesNotExist):
1417+
await self.all.aget(title="")
1418+
13891419
def test_multi_found(self):
13901420
"""Test multiple found in the same QuerySet."""
13911421
# ...or if get() is called and multiple objects are found.
13921422
with self.assertNumQueries(1):
13931423
with self.assertRaises(MultipleObjectsReturned):
13941424
self.all.get(author=self.bob)
13951425

1426+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1427+
async def test_aget_multi_found(self):
1428+
"""Test multiple found in the same QuerySet."""
1429+
# ...or if aget() is called and multiple objects are found.
1430+
with self.assertRaises(MultipleObjectsReturned):
1431+
await self.all.aget(author=self.bob)
1432+
1433+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
13961434
def test_multi_found_separate_querysets(self):
13971435
"""Test one found in each QuerySet."""
13981436
# ...or if get() is called and multiple objects are found.
13991437
with self.assertNumQueries(2):
14001438
with self.assertRaises(MultipleObjectsReturned):
14011439
self.all.get(title__contains="A")
14021440

1441+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1442+
async def test_aget_multi_found_separate_querysets(self):
1443+
"""Test one found in each QuerySet."""
1444+
# ...or if aget() is called and multiple objects are found.
1445+
with self.assertRaises(MultipleObjectsReturned):
1446+
await self.all.aget(title__contains="A")
1447+
14031448
def test_related_model(self):
14041449
qss = QuerySetSequence(Article.objects.all(), BlogPost.objects.all())
14051450
with self.assertNumQueries(2):
14061451
post = qss.get(publisher__name="Wacky Website")
14071452
self.assertEqual(post.title, "Post")
14081453
self.assertIsInstance(post, BlogPost)
14091454

1455+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1456+
async def test_aget_related_model(self):
1457+
qss = QuerySetSequence(Article.objects.all(), BlogPost.objects.all())
1458+
post = await qss.aget(publisher__name="Wacky Website")
1459+
self.assertEqual(post.title, "Post")
1460+
self.assertIsInstance(post, BlogPost)
1461+
14101462
def test_queryset_lookup(self):
14111463
"""Test using the special QuerySet lookup."""
14121464
with self.assertNumQueries(1):
14131465
article = self.all.get(**{"#": 1, "author": self.bob})
14141466
self.assertEqual(article.title, "Some Article")
14151467
self.assertIsInstance(article, Article)
14161468

1469+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1470+
async def test_aget_queryset_lookup(self):
1471+
"""Test using the special QuerySet lookup."""
1472+
article = await self.all.aget(**{"#": 1, "author": self.bob})
1473+
self.assertEqual(article.title, "Some Article")
1474+
self.assertIsInstance(article, Article)
1475+
14171476
def test_empty(self):
14181477
"""Calling get on an empty QuerySetSequence raises ObjectDoesNotExist."""
14191478
with self.assertRaises(ObjectDoesNotExist):
14201479
self.empty.get(pk=1)
14211480

1481+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1482+
async def test_aget_empty(self):
1483+
"""Calling get on an empty QuerySetSequence raises ObjectDoesNotExist."""
1484+
with self.assertRaises(ObjectDoesNotExist):
1485+
await self.empty.get(pk=1)
1486+
14221487

14231488
class TestBoolean(TestBase):
14241489
"""Tests related to casting the QuerySetSequence to a boolean."""
@@ -1553,27 +1618,57 @@ def test_exists(self):
15531618
with self.assertNumQueries(1):
15541619
self.assertTrue(self.all.filter(title="Biography").exists())
15551620

1621+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1622+
async def test_aexists(self):
1623+
"""
1624+
Ensure that aexists() returns True if the item is found in the first QuerySet.
1625+
"""
1626+
self.assertTrue(await self.all.filter(title="Biography").aexists())
1627+
15561628
def test_exists_second(self):
15571629
"""
15581630
Ensure that exists() returns True if the item is found in a subsequent QuerySet.
15591631
"""
15601632
with self.assertNumQueries(2):
15611633
self.assertTrue(self.all.filter(title="Alice in Django-land").exists())
15621634

1635+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1636+
async def test_aexists_second(self):
1637+
"""
1638+
Ensure that aexists() returns True if the item is found in a subsequent
1639+
QuerySet.
1640+
"""
1641+
self.assertTrue(await self.all.filter(title="Alice in Django-land").aexists())
1642+
15631643
def test_not_found(self):
15641644
"""Ensure that exists() returns False if the item is not found."""
15651645
with self.assertNumQueries(2):
15661646
self.assertFalse(self.all.filter(title="").exists())
15671647

1648+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1649+
async def test_aexists_not_found(self):
1650+
"""Ensure that aexists() returns False if the item is not found."""
1651+
self.assertFalse(await self.all.filter(title="").aexists())
1652+
15681653
def test_multi_found(self):
15691654
"""Ensure that exists() returns True if multiple items are found."""
15701655
with self.assertNumQueries(1):
15711656
self.assertTrue(self.all.filter(author=self.bob).exists())
15721657

1658+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1659+
async def test_aexists_multi_found(self):
1660+
"""Ensure that aexists() returns True if multiple items are found."""
1661+
self.assertTrue(await self.all.filter(author=self.bob).aexists())
1662+
15731663
def test_empty(self):
15741664
"""An empty QuerySetSequence should return False."""
15751665
self.assertFalse(self.empty.exists())
15761666

1667+
@skipIf(django.VERSION < (4, 1), "Not supported in Django < 4.1.")
1668+
async def test_aexists_empty(self):
1669+
"""An empty QuerySetSequence should return False."""
1670+
self.assertFalse(await self.empty.aexists())
1671+
15771672

15781673
@skipIf(django.VERSION < (4, 0), "Not supported in Django < 4.0.")
15791674
class TestContains(TestBase):
@@ -1786,12 +1881,14 @@ def test_raw(self):
17861881
with self.assertRaises(NotImplementedError):
17871882
self.all.raw("")
17881883

1884+
@skipIf(django.VERSION >= (4, 1), "aget exists starting on Django 4.1.")
17891885
async def test_aget(self):
1790-
with self.assertRaises(ImplementedIn41):
1886+
with self.assertRaises(AttributeError):
17911887
await self.all.aget()
17921888

1889+
@skipIf(django.VERSION >= (4, 1), "acount exists starting on Django 4.1.")
17931890
async def test_acount(self):
1794-
with self.assertRaises(ImplementedIn41):
1891+
with self.assertRaises(AttributeError):
17951892
await self.all.acount()
17961893

17971894
async def test_aiterator(self):
@@ -1822,8 +1919,9 @@ async def test_aaggregate(self):
18221919
with self.assertRaises(ImplementedIn41):
18231920
await self.all.aaggregate()
18241921

1922+
@skipIf(django.VERSION >= (4, 1), "aexists exists starting on Django 4.1.")
18251923
async def test_aexists(self):
1826-
with self.assertRaises(ImplementedIn41):
1924+
with self.assertRaises(AttributeError):
18271925
await self.all.aexists()
18281926

18291927
async def test_acontains(self):

0 commit comments

Comments
 (0)