Functional Weave
Code in Rust

collections.paginate@1.0.0

impl/python.py

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) 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