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) and not 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)) or not isinstance(items, (list, tuple)): raise TypeError("paginate needs a list of items") if not _whole(page): raise TypeError("page must be a whole number, received %r" % (page,)) if not _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.""" if not _whole(index) or index < 0: raise ValueError("index must be a whole number of 0 or greater, received %r" % (index,)) if not _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