2,762 bytes · the Python implementation · view raw
from typing import Any, Sequence, TypeVar
from .collections_paginate_types import Page
T = TypeVar("T")
def _whole(value: Any) -> bool:
# bool is a subclass of int in Python, so True would otherwise pass as 1# and silently paginate. Every language rejects it, so reject it here too.return isinstance(value, int) andnot isinstance(value, bool)
def paginate(items: Sequence[T], page: int, per_page: int) -> Page[T]:
"""Take the 1-based page ``page`` of ``items``, ``per_page`` items at a time. Page numbers are 1-based because every pagination UI, query string and API contract in the wild is 1-based; page 0 can only come from a caller that has the contract wrong, so it raises. A page past the end is deliberately not an error. Between two requests a collection can shrink under a client that is already on page 9, and that client should render an empty page, not a 500. """if isinstance(items, (str, bytes)) ornot isinstance(items, (list, tuple)):
raise TypeError("paginate needs a list of items")
ifnot _whole(page):
raise TypeError("page must be a whole number, received %r" % (page,))
ifnot _whole(per_page):
raise TypeError("per_page must be a whole number, received %r" % (per_page,))
# A page size of zero cannot mean "no limit": that would quietly hand back# the entire collection, which is the outage pagination exists to prevent.if per_page < 1:
raise ValueError("per_page must be at least 1, received %d" % (per_page,))
if page < 1:
raise ValueError("page must be 1 or greater, received %d" % (page,))
total = len(items)
# Zero items is zero pages, not one empty page, so `page <= total_pages` is# the one correct test for "does this page exist".
total_pages = -(-total // per_page)
start = (page - 1) * per_page
return Page(
# Slicing already clamps, so a start past the end yields [] rather than# raising - the documented behaviour for a page past the end.
items=list(items[start : start + per_page]),
page=page,
per_page=per_page,
total=total,
total_pages=total_pages,
has_next=page < total_pages,
# On page 9 of a 2-page collection there really are earlier pages.
has_prev=page > 1,
)
def page_of_index(index: int, per_page: int) -> int:
"""The 1-based page number a zero-based item index falls on."""ifnot _whole(index) or index < 0:
raise ValueError("index must be a whole number of 0 or greater, received %r" % (index,))
ifnot _whole(per_page) or per_page < 1:
raise ValueError("per_page must be at least 1, received %r" % (per_page,))
return index // per_page + 1