Skip to content

Commit

Permalink
Support async queryset evaluation
Browse files Browse the repository at this point in the history
  • Loading branch information
bradleyoesch committed May 16, 2024
1 parent 50e88f2 commit ad107aa
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 10 deletions.
43 changes: 33 additions & 10 deletions cursor_pagination.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from asgiref.sync import sync_to_async
from base64 import b64decode, b64encode
from collections.abc import Sequence

Expand Down Expand Up @@ -68,17 +69,17 @@ def _nulls_ordering(self, ordering, from_last=False):

return nulls_ordering



def page(self, first=None, last=None, after=None, before=None):
qs = self.queryset
def _apply_paginator_arguments(self, qs, first=None, last=None, after=None, before=None):
"""
Apply first/after, last/before filtering to the queryset
"""
page_size = first or last
if page_size is None:
return CursorPage(qs, self)

from_last = last is not None
if from_last and first is not None:
raise ValueError('Cannot process first and last')
raise ValueError('Cannot process first and last')

if after is not None:
qs = self.apply_cursor(after, qs, from_last=from_last)
Expand All @@ -89,11 +90,10 @@ def page(self, first=None, last=None, after=None, before=None):
if last is not None:
qs = qs.order_by(*self._nulls_ordering(reverse_ordering(self.ordering), from_last=True))[:last + 1]

qs = list(qs)
items = qs[:page_size]
if last is not None:
items.reverse()
has_additional = len(qs) > len(items)
def _get_cursor_page(self, items, has_additional, first, last, after, before):
"""
Create and return the cursor page for the given items
"""
additional_kwargs = {}
if first is not None:
additional_kwargs['has_next'] = has_additional
Expand All @@ -103,6 +103,29 @@ def page(self, first=None, last=None, after=None, before=None):
additional_kwargs['has_next'] = bool(before)
return CursorPage(items, self, **additional_kwargs)

def page(self, first=None, last=None, after=None, before=None):
qs = self.queryset
self._apply_paginator_arguments(qs, first, last, after, before)

qs = list(qs)
items = qs[:page_size]
if last is not None:
items.reverse()
has_additional = len(qs) > len(items)

return self._get_cursor_page(items, has_additional, first, last, after, before)

async def apage(self, first=None, last=None, after=None, before=None):
qs = self.queryset
self._apply_paginator_arguments(qs, first, last, after, before)

items = await sync_to_async(list)(qs[:page_size])
if last is not None:
items.reverse()
has_additional = (await qs.acount()) > len(items)

return self._get_cursor_page(items, has_additional, first, last, after, before)

def apply_cursor(self, cursor, queryset, from_last, reverse=False):
position = self.decode_cursor(cursor)

Expand Down
22 changes: 22 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ def test_empty(self):
self.assertFalse(page.has_next)
self.assertFalse(page.has_previous)

async def test_async_empty(self):
paginator = CursorPaginator(Post.objects.all(), ('id',))
page = await paginator.apage()
self.assertEqual(len(page), 0)
self.assertFalse(page.has_next)
self.assertFalse(page.has_previous)

def test_with_items(self):
for i in range(20):
Post.objects.create(name='Name %s' % i)
Expand All @@ -27,6 +34,15 @@ def test_with_items(self):
self.assertFalse(page.has_next)
self.assertFalse(page.has_previous)

async def test_async_with_items(self):
for i in range(20):
await Post.objects.acreate(name='Name %s' % i)
paginator = CursorPaginator(Post.objects.all(), ('id',))
page = await paginator.apage()
self.assertEqual(len(page), 20)
self.assertFalse(page.has_next)
self.assertFalse(page.has_previous)


class TestForwardPagination(TestCase):

Expand All @@ -45,6 +61,12 @@ def test_first_page(self):
self.assertTrue(page.has_next)
self.assertFalse(page.has_previous)

async def test_async_first_page(self):
page = await self.paginator.apage(first=2)
self.assertSequenceEqual(page, [self.items[0], self.items[1]])
self.assertTrue(page.has_next)
self.assertFalse(page.has_previous)

def test_second_page(self):
previous_page = self.paginator.page(first=2)
cursor = self.paginator.cursor(previous_page[-1])
Expand Down

0 comments on commit ad107aa

Please sign in to comment.