Skip to content

Commit bbd8d39

Browse files
committed
documented the async paginator
1 parent 91d9e6e commit bbd8d39

File tree

2 files changed

+207
-18
lines changed

2 files changed

+207
-18
lines changed

django_async_extensions/acore/paginator.py

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ async def __aiter__(self):
3030
yield await self.apage(page_number)
3131

3232
def _validate_number(self, number, num_pages):
33-
"""Validate the given 1-based page number."""
34-
3533
try:
3634
if isinstance(number, float) and not number.is_integer():
3735
raise ValueError
@@ -46,10 +44,15 @@ def _validate_number(self, number, num_pages):
4644
return number
4745

4846
async def avalidate_number(self, number):
47+
"""Validate the given 1-based page number."""
4948
num_page = await self.anum_pages()
5049
return self._validate_number(number, num_page)
5150

5251
async def aget_page(self, number):
52+
"""
53+
Return a valid page, even if the page argument isn't a number or isn't
54+
in range.
55+
"""
5356
try:
5457
number = await self.avalidate_number(number)
5558
except PageNotAnInteger:
@@ -59,7 +62,7 @@ async def aget_page(self, number):
5962
return await self.apage(number)
6063

6164
async def apage(self, number):
62-
"""See Paginator.page()."""
65+
"""Return a AsyncPage object for the given 1-based page number."""
6366
number = await self.avalidate_number(number)
6467
bottom = (number - 1) * self.per_page
6568
top = bottom + self.per_page
@@ -71,10 +74,16 @@ async def apage(self, number):
7174
return self._get_page(object_list, number, self)
7275

7376
def _get_page(self, *args, **kwargs):
77+
"""
78+
Return an instance of a single page.
79+
80+
This hook can be used by subclasses to use an alternative to the
81+
standard :cls:`AsyncPage` object.
82+
"""
7483
return AsyncPage(*args, **kwargs)
7584

7685
async def acount(self):
77-
"""See Paginator.count()."""
86+
"""Return the total number of objects, across all pages."""
7887
if self._cache_acount is not None:
7988
return self._cache_acount
8089

@@ -97,7 +106,7 @@ async def acount(self):
97106
return count
98107

99108
async def anum_pages(self):
100-
"""See Paginator.num_pages()."""
109+
"""Return the total number of pages."""
101110
if self._cache_anum_pages is not None:
102111
return self._cache_anum_pages
103112

@@ -113,11 +122,24 @@ async def anum_pages(self):
113122
return num_pages
114123

115124
async def apage_range(self):
116-
"""See Paginator.page_range()"""
125+
"""
126+
Return a 1-based range of pages for iterating through within
127+
a template for loop.
128+
"""
117129
num_pages = await self.anum_pages()
118130
return range(1, num_pages + 1)
119131

120132
async def aget_elided_page_range(self, number=1, *, on_each_side=3, on_ends=2):
133+
"""
134+
Return a 1-based range of pages with some values elided.
135+
136+
If the page range is larger than a given size, the whole range is not
137+
provided and a compact form is returned instead, e.g. for a paginator
138+
with 50 pages, if page 43 were the current page, the output, with the
139+
default arguments, would be:
140+
141+
1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50.
142+
"""
121143
number = await self.avalidate_number(number)
122144
num_pages = await self.anum_pages()
123145
page_range = await self.apage_range()
@@ -130,16 +152,6 @@ async def aget_elided_page_range(self, number=1, *, on_each_side=3, on_ends=2):
130152
def _get_elided_page_range(
131153
self, number, num_pages, page_range, on_each_side=3, on_ends=2
132154
):
133-
"""
134-
Return a 1-based range of pages with some values elided.
135-
136-
If the page range is larger than a given size, the whole range is not
137-
provided and a compact form is returned instead, e.g. for a paginator
138-
with 50 pages, if page 43 were the current page, the output, with the
139-
default arguments, would be:
140-
141-
1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50.
142-
"""
143155
if num_pages <= (on_each_side + on_ends) * 2:
144156
for page in page_range:
145157
yield page
@@ -201,9 +213,11 @@ async def agetitem(self, index):
201213
return self.object_list[index]
202214

203215
async def alen(self):
216+
"""an async interface to be used instead of `len(page)`"""
204217
return len(await self.alist())
205218

206219
async def alist(self):
220+
"""make a list of the items in the queryset"""
207221
if hasattr(self.object_list, "__aiter__"):
208222
return [obj async for obj in self.object_list]
209223
return await sync_to_async(list)(self.object_list)
@@ -227,14 +241,21 @@ async def aprevious_page_number(self):
227241
return await self.paginator.avalidate_number(self.number - 1)
228242

229243
async def astart_index(self):
230-
"""See Page.start_index()."""
244+
"""
245+
Return the 1-based index of the first object on this page,
246+
relative to total objects in the paginator.
247+
"""
231248
count = await self.paginator.acount()
232249
if count == 0:
233250
return 0
234251
return (self.paginator.per_page * (self.number - 1)) + 1
235252

236253
async def aend_index(self):
237-
"""See Page.end_index()."""
254+
"""
255+
Return the 1-based index of the last object on this page,
256+
relative to total objects found (hits).
257+
"""
258+
# Special case for the last page because there can be orphans.
238259
num_pages = await self.paginator.anum_pages()
239260
if self.number == num_pages:
240261
return await self.paginator.acount()

docs/core/async-paginator.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
## AsyncPaginator
2+
3+
the `AsyncPaginator` class is an async version of django's [Paginator](https://docs.djangoproject.com/en/5.1/ref/paginator/#django.core.paginator.Paginator).
4+
5+
although `AsyncPaginator` inherits from django's `Paginator`, but this is only for code reuse and easier migration from sync to async, and you should not call the sync methods from an async environment.
6+
7+
the paginator can take a queryset or a list, but they have slightly different behaviours,
8+
if you pass in a queryset to the paginator, the queryset is preserved until it has to be evaluated, so until it is evaluated it needs to be treated as a queryset,
9+
and using querysets in an async environment is different from using querysets in sync environments.
10+
11+
### Examples
12+
13+
**note**: in normal django shell you can not `await`, to use `await` you need a shell tht supports that, such as [ipython](https://ipython.org/)
14+
15+
#### Example of list pagination
16+
17+
```pycon
18+
In [1]: from django_async_extensions.acore.paginator import AsyncPaginator
19+
20+
In [2]: objects = ["john", "paul", "george", "ringo"]
21+
22+
In [3]: p = AsyncPaginator(objects, 2)
23+
24+
In [4]: await p.acount()
25+
Out[4]: 4
26+
27+
In [5]: await p.anum_pages()
28+
Out[5]: 2
29+
30+
In [6]: type(await p.apage_range())
31+
Out[6]: range
32+
33+
In [7]: await p.apage_range()
34+
Out[7]: range(1, 3)
35+
36+
In [8]: page1 = await p.apage(1)
37+
38+
In [9]: page1
39+
Out[9]: <Async Page 1>
40+
41+
In [10]: page1.object_list
42+
Out[10]: ['john', 'paul']
43+
44+
In [11]: page2 = await p.apage(2)
45+
46+
In [12]: page2.object_list
47+
Out[12]: ['george', 'ringo']
48+
49+
In [13]: await page2.ahas_next()
50+
Out[13]: False
51+
52+
In [14]: await page2.ahas_previous()
53+
Out[14]: True
54+
55+
In [15]: await page2.ahas_other_pages()
56+
Out[15]: True
57+
58+
await page2.anext_page_number()
59+
Traceback (most recent call last):
60+
...
61+
EmptyPage: That page contains no results
62+
63+
In [17]: await page2.aprevious_page_number()
64+
Out[17]: 1
65+
66+
In [18]: await page2.astart_index()
67+
Out[18]: 3
68+
69+
In [19]: await page2.aend_index()
70+
Out[19]: 4
71+
72+
In [20]: await p.apage(0)
73+
Traceback (most recent call last):
74+
...
75+
EmptyPage: That page number is less than 1
76+
77+
In [21]: await p.apage(3)
78+
Traceback (most recent call last):
79+
...
80+
EmptyPage: That page contains no results
81+
```
82+
83+
84+
#### Example of queryset pagination
85+
86+
```pycon
87+
In [1]: from django.contrib.auth.models import User
88+
89+
In [2]: objs = [User(username=f"test{i}", password="testpass123") for i in range
90+
...: (1, 5)]
91+
92+
In [3]: User.objects.bulk_create(objs)
93+
Out[3]: [<User: test1>, <User: test2>, <User: test3>, <User: test4>]
94+
95+
In [4]: from django_async_extensions.acore.paginator import AsyncPaginator
96+
97+
In [5]: users = User.objects.order_by("username")
98+
99+
In [6]: p = AsyncPaginator(users, 2)
100+
101+
In [7]: p.object_list
102+
Out[7]: <QuerySet [<User: test1>, <User: test2>, <User: test3>, <User: test4>]>
103+
104+
In [8]: page1 = await p.apage(1)
105+
106+
In [9]: page1
107+
Out[9]: <Async Page 1>
108+
109+
In [10]: page1.object_list
110+
Out[10]: <QuerySet [<User: test1>, <User: test2>]>
111+
112+
In [11]: page2 = await p.apage(2)
113+
114+
In [12]: page2.object_list
115+
Out[12]: <QuerySet [<User: test3>, <User: test4>]>
116+
117+
In [13]: await page2.ahas_next()
118+
Out[13]: False
119+
120+
In [14]: await page2.alen() # use this instead of `len()`
121+
Out[14]: 2
122+
123+
In [15]: len(page2)
124+
---------------------------------------------------------------------------
125+
TypeError Traceback (most recent call last)
126+
Cell In[15], line 1
127+
----> 1 len(page2)
128+
129+
TypeError: object of type 'AsyncPage' has no len()
130+
131+
In [16]: await page2.alist() # use this instead of `list()`
132+
Out[16]: [<User: test3>, <User: test4>]
133+
134+
In [17]: list(page2)
135+
---------------------------------------------------------------------------
136+
TypeError Traceback (most recent call last)
137+
Cell In[17], line 1
138+
----> 1 list(page2)
139+
140+
TypeError: 'AsyncPage' object is not iterable
141+
142+
In [18]: page2.object_list
143+
Out[19]: <QuerySet [<User: test3>, <User: test4>]>
144+
145+
In [20]: await page2.agetitem(1) # use this instead of `__getitem__()`
146+
Out[20]: <User: test4>
147+
148+
In [21]: page2.object_list
149+
Out[21]: [<User: test3>, <User: test4>] # after `agetitem()` is called, the object list gets turned into a list
150+
151+
In [22]: page1.object_list
152+
Out[22]: <QuerySet [<User: test1>, <User: test2>]>
153+
154+
In [23]: page1[0]
155+
---------------------------------------------------------------------------
156+
TypeError Traceback (most recent call last)
157+
Cell In[23], line 1
158+
----> 1 page1[0]
159+
160+
TypeError: 'AsyncPage' object is not subscriptable
161+
162+
In [24]: await page1.agetitem(slice(2)) # you can pass a slice to `agetitem()`
163+
Out[24]: [<User: test1>, <User: test2>]
164+
165+
In [25]: page1.object_list
166+
Out[25]: [<User: test1>, <User: test2>] # turned into a list
167+
168+
```

0 commit comments

Comments
 (0)