From ae340399f00bcef6d9c4f240fb0a02ce21b7b707 Mon Sep 17 00:00:00 2001 From: "pullapprove5-fix[bot]" <4489445+pullapprove5-fix[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:53:04 +0000 Subject: [PATCH] Fix: Paginator accepts per_page <= 0 and crashes later instead of failing at construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reproduced the finding directly against plain/plain/paginator.py: Paginator([1,2,3], 0) raised ZeroDivisionError from num_pages (ceil(count/0)), and Paginator([1,2,3,4,5], -1) produced num_pages == -5, with get_page(1) then raising an uncaught EmptyPage. Fixed by validating per_page in Paginator.__init__: coerce to int and raise ValueError("per_page must be at least 1, got {n}") when per_page < 1, before any state is set — closing the invariant at construction rather than relying on each caller (e.g. AdminListView) to clamp. Added plain/tests/public/test_paginator.py covering per_page=0, per_page=-1, and a normal positive case. Ran ./scripts/fix plain (ruff/oxlint/oxfmt/prettier, all clean) and the plain package's pytest suite (uv, with a bootstrapped uv since it wasn't preinstalled in this sandbox) — all 700 tests passed, including the 3 new ones. Full ./scripts/test could not run because this sandbox has neither Docker nor a local Postgres server for scripts/start-postgres to use; I instead ran `uv run --isolated --package plain --with psycopg[binary] python -m pytest` directly with a placeholder PLAIN_POSTGRES_URL, which was sufficient since none of the plain package's tests in this run touch a live database connection. Committed as a single commit on master. --- plain/plain/paginator.py | 5 ++++- plain/tests/public/test_paginator.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 plain/tests/public/test_paginator.py diff --git a/plain/plain/paginator.py b/plain/plain/paginator.py index 26c059b0a4..902f8a7a51 100644 --- a/plain/plain/paginator.py +++ b/plain/plain/paginator.py @@ -35,9 +35,12 @@ def __init__( orphans: int = 0, allow_empty_first_page: bool = True, ) -> None: + per_page = int(per_page) + if per_page < 1: + raise ValueError(f"per_page must be at least 1, got {per_page}") self.object_list = object_list self._check_object_list_is_ordered() - self.per_page = int(per_page) + self.per_page = per_page self.orphans = int(orphans) self.allow_empty_first_page = allow_empty_first_page diff --git a/plain/tests/public/test_paginator.py b/plain/tests/public/test_paginator.py new file mode 100644 index 0000000000..ad494c84e4 --- /dev/null +++ b/plain/tests/public/test_paginator.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import pytest +from plain.paginator import Paginator + + +@pytest.mark.parametrize("per_page", [0, -1]) +def test_raises_for_non_positive_per_page(per_page): + with pytest.raises(ValueError, match="per_page must be at least 1"): + Paginator([1, 2, 3], per_page) + + +def test_accepts_positive_per_page(): + paginator = Paginator([1, 2, 3], 1) + assert paginator.num_pages == 3