summaryrefslogtreecommitdiff
path: root/django/core
diff options
context:
space:
mode:
authorwookkl <wjddnr315@gmail.com>2025-03-12 00:53:04 +0900
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-03-12 09:22:44 +0100
commit2ae3044d9d4dfb8371055513e440e0384f211963 (patch)
tree771c0a6340bd0eb8a05c608ae2919652b16ce876 /django/core
parent0ebea6e5c07485a36862e9b6e2be18d1694ad2c5 (diff)
Fixed #35945 -- Added async interface to Paginator.
Diffstat (limited to 'django/core')
-rw-r--r--django/core/paginator.py324
1 files changed, 266 insertions, 58 deletions
diff --git a/django/core/paginator.py b/django/core/paginator.py
index 7b3189cc8b..422da30aa1 100644
--- a/django/core/paginator.py
+++ b/django/core/paginator.py
@@ -1,8 +1,11 @@
import collections.abc
import inspect
import warnings
+from asyncio import iscoroutinefunction
from math import ceil
+from asgiref.sync import sync_to_async
+
from django.utils.functional import cached_property
from django.utils.inspect import method_has_no_args
from django.utils.translation import gettext_lazy as _
@@ -24,7 +27,7 @@ class EmptyPage(InvalidPage):
pass
-class Paginator:
+class BasePaginator:
# Translators: String used to replace omitted page numbers in elided page
# range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10].
ELLIPSIS = _("…")
@@ -53,11 +56,74 @@ class Paginator:
else self.default_error_messages | error_messages
)
- def __iter__(self):
- for page_number in self.page_range:
- yield self.page(page_number)
+ def _check_object_list_is_ordered(self):
+ """
+ Warn if self.object_list is unordered (typically a QuerySet).
+ """
+ ordered = getattr(self.object_list, "ordered", None)
+ if ordered is not None and not ordered:
+ obj_list_repr = (
+ "{} {}".format(
+ self.object_list.model, self.object_list.__class__.__name__
+ )
+ if hasattr(self.object_list, "model")
+ else "{!r}".format(self.object_list)
+ )
+ warnings.warn(
+ "Pagination may yield inconsistent results with an unordered "
+ "object_list: {}.".format(obj_list_repr),
+ UnorderedObjectListWarning,
+ stacklevel=3,
+ )
- def validate_number(self, number):
+ def _get_elided_page_range(
+ self, number, num_pages, page_range, on_each_side=3, on_ends=2
+ ):
+ """
+ Return a 1-based range of pages with some values elided.
+
+ If the page range is larger than a given size, the whole range is not
+ provided and a compact form is returned instead, e.g. for a paginator
+ with 50 pages, if page 43 were the current page, the output, with the
+ default arguments, would be:
+
+ 1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50.
+ """
+ if num_pages <= (on_each_side + on_ends) * 2:
+ for page in page_range:
+ yield page
+ return
+
+ if number > (1 + on_each_side + on_ends) + 1:
+ for page in range(1, on_ends + 1):
+ yield page
+ yield self.ELLIPSIS
+ for page in range(number - on_each_side, number + 1):
+ yield page
+ else:
+ for page in range(1, number + 1):
+ yield page
+
+ if number < (num_pages - on_each_side - on_ends) - 1:
+ for page in range(number + 1, number + on_each_side + 1):
+ yield page
+ yield self.ELLIPSIS
+ for page in range(num_pages - on_ends + 1, num_pages + 1):
+ yield page
+ else:
+ for page in range(number + 1, num_pages + 1):
+ yield page
+
+ def _get_page(self, *args, **kwargs):
+ """
+ Return an instance of a single page.
+
+ This hook can be used by subclasses to use an alternative to the
+ standard :cls:`Page` object.
+ """
+ return Page(*args, **kwargs)
+
+ def _validate_number(self, number, num_pages):
"""Validate the given 1-based page number."""
try:
if isinstance(number, float) and not number.is_integer():
@@ -67,10 +133,19 @@ class Paginator:
raise PageNotAnInteger(self.error_messages["invalid_page"])
if number < 1:
raise EmptyPage(self.error_messages["min_page"])
- if number > self.num_pages:
+ if number > num_pages:
raise EmptyPage(self.error_messages["no_results"])
return number
+
+class Paginator(BasePaginator):
+ def __iter__(self):
+ for page_number in self.page_range:
+ yield self.page(page_number)
+
+ def validate_number(self, number):
+ return self._validate_number(number, self.num_pages)
+
def get_page(self, number):
"""
Return a valid page, even if the page argument isn't a number or isn't
@@ -93,15 +168,6 @@ class Paginator:
top = self.count
return self._get_page(self.object_list[bottom:top], number, self)
- def _get_page(self, *args, **kwargs):
- """
- Return an instance of a single page.
-
- This hook can be used by subclasses to use an alternative to the
- standard :cls:`Page` object.
- """
- return Page(*args, **kwargs)
-
@cached_property
def count(self):
"""Return the total number of objects, across all pages."""
@@ -126,56 +192,105 @@ class Paginator:
"""
return range(1, self.num_pages + 1)
- def _check_object_list_is_ordered(self):
- """
- Warn if self.object_list is unordered (typically a QuerySet).
- """
- ordered = getattr(self.object_list, "ordered", None)
- if ordered is not None and not ordered:
- obj_list_repr = (
- "{} {}".format(
- self.object_list.model, self.object_list.__class__.__name__
- )
- if hasattr(self.object_list, "model")
- else "{!r}".format(self.object_list)
- )
- warnings.warn(
- "Pagination may yield inconsistent results with an unordered "
- "object_list: {}.".format(obj_list_repr),
- UnorderedObjectListWarning,
- stacklevel=3,
- )
-
def get_elided_page_range(self, number=1, *, on_each_side=3, on_ends=2):
- """
- Return a 1-based range of pages with some values elided.
+ number = self.validate_number(number)
+ yield from self._get_elided_page_range(
+ number, self.num_pages, self.page_range, on_each_side, on_ends
+ )
- If the page range is larger than a given size, the whole range is not
- provided and a compact form is returned instead, e.g. for a paginator
- with 50 pages, if page 43 were the current page, the output, with the
- default arguments, would be:
- 1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50.
- """
- number = self.validate_number(number)
+class AsyncPaginator(BasePaginator):
+ def __init__(
+ self,
+ object_list,
+ per_page,
+ orphans=0,
+ allow_empty_first_page=True,
+ error_messages=None,
+ ):
+ super().__init__(
+ object_list, per_page, orphans, allow_empty_first_page, error_messages
+ )
+ self._cache_acount = None
+ self._cache_anum_pages = None
- if self.num_pages <= (on_each_side + on_ends) * 2:
- yield from self.page_range
- return
+ async def __aiter__(self):
+ page_range = await self.apage_range()
+ for page_number in page_range:
+ yield await self.apage(page_number)
- if number > (1 + on_each_side + on_ends) + 1:
- yield from range(1, on_ends + 1)
- yield self.ELLIPSIS
- yield from range(number - on_each_side, number + 1)
- else:
- yield from range(1, number + 1)
+ async def avalidate_number(self, number):
+ num_pages = await self.anum_pages()
+ return self._validate_number(number, num_pages)
- if number < (self.num_pages - on_each_side - on_ends) - 1:
- yield from range(number + 1, number + on_each_side + 1)
- yield self.ELLIPSIS
- yield from range(self.num_pages - on_ends + 1, self.num_pages + 1)
+ async def aget_page(self, number):
+ """See Paginator.get_page()."""
+ try:
+ number = await self.avalidate_number(number)
+ except PageNotAnInteger:
+ number = 1
+ except EmptyPage:
+ number = await self.anum_pages()
+ return await self.apage(number)
+
+ async def apage(self, number):
+ """See Paginator.page()."""
+ number = await self.avalidate_number(number)
+ bottom = (number - 1) * self.per_page
+ top = bottom + self.per_page
+ count = await self.acount()
+ if top + self.orphans >= count:
+ top = count
+
+ return self._get_page(self.object_list[bottom:top], number, self)
+
+ def _get_page(self, *args, **kwargs):
+ return AsyncPage(*args, **kwargs)
+
+ async def acount(self):
+ """See Paginator.count()."""
+ if self._cache_acount is not None:
+ return self._cache_acount
+ c = getattr(self.object_list, "acount", None)
+ if (
+ iscoroutinefunction(c)
+ and not inspect.isbuiltin(c)
+ and method_has_no_args(c)
+ ):
+ count = await c()
else:
- yield from range(number + 1, self.num_pages + 1)
+ count = len(self.object_list)
+
+ self._cache_acount = count
+ return count
+
+ async def anum_pages(self):
+ """See Paginator.num_pages()."""
+ if self._cache_anum_pages is not None:
+ return self._cache_anum_pages
+ count = await self.acount()
+ if count == 0 and not self.allow_empty_first_page:
+ self._cache_anum_pages = 0
+ return self._cache_anum_pages
+ hits = max(1, count - self.orphans)
+ num_pages = ceil(hits / self.per_page)
+
+ self._cache_anum_pages = num_pages
+ return num_pages
+
+ async def apage_range(self):
+ """See Paginator.page_range()"""
+ num_pages = await self.anum_pages()
+ return range(1, num_pages + 1)
+
+ async def aget_elided_page_range(self, number=1, *, on_each_side=3, on_ends=2):
+ number = await self.avalidate_number(number)
+ num_pages = await self.anum_pages()
+ page_range = await self.apage_range()
+ for page in self._get_elided_page_range(
+ number, num_pages, page_range, on_each_side, on_ends
+ ):
+ yield page
class Page(collections.abc.Sequence):
@@ -236,3 +351,96 @@ class Page(collections.abc.Sequence):
if self.number == self.paginator.num_pages:
return self.paginator.count
return self.number * self.paginator.per_page
+
+
+class AsyncPage:
+ def __init__(self, object_list, number, paginator):
+ self.object_list = object_list
+ self.number = number
+ self.paginator = paginator
+
+ def __repr__(self):
+ return "<Async Page %s>" % self.number
+
+ async def __aiter__(self):
+ if hasattr(self.object_list, "__aiter__"):
+ async for obj in self.object_list:
+ yield obj
+ else:
+ for obj in self.object_list:
+ yield obj
+
+ def __len__(self):
+ if not isinstance(self.object_list, list):
+ raise TypeError(
+ "AsyncPage.aget_object_list() must be awaited before calling len()."
+ )
+ return len(self.object_list)
+
+ def __reversed__(self):
+ if not isinstance(self.object_list, list):
+ raise TypeError(
+ "AsyncPage.aget_object_list() "
+ "must be awaited before calling reversed()."
+ )
+
+ return reversed(self.object_list)
+
+ def __getitem__(self, index):
+ if not isinstance(index, (int, slice)):
+ raise TypeError(
+ "AsyncPage indices must be integers or slices, not %s."
+ % type(index).__name__
+ )
+
+ if not isinstance(self.object_list, list):
+ raise TypeError(
+ "AsyncPage.aget_object_list() must be awaited before using indexing."
+ )
+ return self.object_list[index]
+
+ async def aget_object_list(self):
+ """
+ Returns self.object_list as a list.
+
+ This method must be awaited before AsyncPage can be
+ treated as a sequence of self.object_list.
+ """
+ if not isinstance(self.object_list, list):
+ if hasattr(self.object_list, "__aiter__"):
+ self.object_list = [obj async for obj in self.object_list]
+ else:
+ self.object_list = await sync_to_async(list)(self.object_list)
+ return self.object_list
+
+ async def ahas_next(self):
+ num_pages = await self.paginator.anum_pages()
+ return self.number < num_pages
+
+ async def ahas_previous(self):
+ return self.number > 1
+
+ async def ahas_other_pages(self):
+ has_previous = await self.ahas_previous()
+ has_next = await self.ahas_next()
+ return has_previous or has_next
+
+ async def anext_page_number(self):
+ return await self.paginator.avalidate_number(self.number + 1)
+
+ async def aprevious_page_number(self):
+ return await self.paginator.avalidate_number(self.number - 1)
+
+ async def astart_index(self):
+ """See Page.start_index()."""
+ count = await self.paginator.acount()
+ if count == 0:
+ return 0
+ return (self.paginator.per_page * (self.number - 1)) + 1
+
+ async def aend_index(self):
+ """See Page.end_index()."""
+ num_pages = await self.paginator.anum_pages()
+ if self.number == num_pages:
+ return await self.paginator.acount()
+ return self.number * self.paginator.per_page