diff options
| author | wookkl <wjddnr315@gmail.com> | 2025-03-12 00:53:04 +0900 |
|---|---|---|
| committer | Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> | 2025-03-12 09:22:44 +0100 |
| commit | 2ae3044d9d4dfb8371055513e440e0384f211963 (patch) | |
| tree | 771c0a6340bd0eb8a05c608ae2919652b16ce876 /django/core | |
| parent | 0ebea6e5c07485a36862e9b6e2be18d1694ad2c5 (diff) | |
Fixed #35945 -- Added async interface to Paginator.
Diffstat (limited to 'django/core')
| -rw-r--r-- | django/core/paginator.py | 324 |
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 |
