-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
util: Implement
Paginator
class as interface to access a list by pa…
…ges (#11247) * util: Implement `Paginator` class as interface to access a list by pages * Be less restrictive about page sizes and refactor tests * Make the pages based of 0 instead of 1 and some more test refactoring * More tests * Adjust workflows after rebase * Introduce `Paginator.create` * `<=` instead of `- 1`
- Loading branch information
1 parent
abbe9f4
commit baa67e8
Showing
2 changed files
with
116 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
from __future__ import annotations | ||
|
||
import dataclasses | ||
from math import ceil | ||
from typing import Sequence | ||
|
||
|
||
class InvalidPageSizeLimit(Exception): | ||
def __init__(self, page_size_limit: int) -> None: | ||
super().__init__(f"Page size limit must be one or more, not: {page_size_limit}") | ||
|
||
|
||
class InvalidPageSizeError(Exception): | ||
def __init__(self, page_size: int, page_size_limit: int) -> None: | ||
super().__init__(f"Invalid page size {page_size}. Must be between: 1 and {page_size_limit}") | ||
|
||
|
||
class PageOutOfBoundsError(Exception): | ||
def __init__(self, page_size: int, max_page_size: int) -> None: | ||
super().__init__(f"Page {page_size} out of bounds. Available pages: 0-{max_page_size}") | ||
|
||
|
||
@dataclasses.dataclass | ||
class Paginator: | ||
_source: Sequence[object] | ||
_page_size: int | ||
|
||
@classmethod | ||
def create(cls, source: Sequence[object], page_size: int, page_size_limit: int = 100) -> Paginator: | ||
if page_size_limit < 1: | ||
raise InvalidPageSizeLimit(page_size_limit) | ||
if page_size > page_size_limit: | ||
raise InvalidPageSizeError(page_size, page_size_limit) | ||
return cls(source, page_size) | ||
|
||
def page_size(self) -> int: | ||
return self._page_size | ||
|
||
def page_count(self) -> int: | ||
return max(1, ceil(len(self._source) / self._page_size)) | ||
|
||
def get_page(self, page: int) -> Sequence[object]: | ||
if page < 0 or page >= self.page_count(): | ||
raise PageOutOfBoundsError(page, self.page_count() - 1) | ||
offset = page * self._page_size | ||
return self._source[offset : offset + self._page_size] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
from math import ceil | ||
from typing import List, Type | ||
|
||
import pytest | ||
|
||
from chia.util.paginator import InvalidPageSizeError, InvalidPageSizeLimit, PageOutOfBoundsError, Paginator | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"source, page_size, page_size_limit", | ||
[([], 1, 1), ([1], 1, 2), ([1, 2], 2, 2), ([], 10, 100), ([1, 2, 10], 1000, 1000)], | ||
) | ||
def test_constructor_valid_inputs(source: List[int], page_size: int, page_size_limit: int) -> None: | ||
paginator: Paginator = Paginator.create(source, page_size, page_size_limit) | ||
assert paginator.page_size() == page_size | ||
assert paginator.page_count() == 1 | ||
assert paginator.get_page(0) == source | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"page_size, page_size_limit, exception", | ||
[ | ||
(5, -1, InvalidPageSizeLimit), | ||
(5, 0, InvalidPageSizeLimit), | ||
(2, 1, InvalidPageSizeError), | ||
(100, 1, InvalidPageSizeError), | ||
(1001, 1000, InvalidPageSizeError), | ||
], | ||
) | ||
def test_constructor_invalid_inputs(page_size: int, page_size_limit: int, exception: Type[Exception]) -> None: | ||
with pytest.raises(exception): | ||
Paginator.create([], page_size, page_size_limit) | ||
|
||
|
||
def test_page_count() -> None: | ||
for page_size in range(1, 10): | ||
for i in range(0, 10): | ||
assert Paginator.create(range(0, i), page_size).page_count() == max(1, ceil(i / page_size)) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"length, page_size, page, expected_data", | ||
[ | ||
(17, 5, 0, [0, 1, 2, 3, 4]), | ||
(17, 5, 1, [5, 6, 7, 8, 9]), | ||
(17, 5, 2, [10, 11, 12, 13, 14]), | ||
(17, 5, 3, [15, 16]), | ||
(3, 4, 0, [0, 1, 2]), | ||
(3, 3, 0, [0, 1, 2]), | ||
(3, 2, 0, [0, 1]), | ||
(3, 2, 1, [2]), | ||
(3, 1, 0, [0]), | ||
(3, 1, 1, [1]), | ||
(3, 1, 2, [2]), | ||
(2, 2, 0, [0, 1]), | ||
(2, 1, 0, [0]), | ||
(2, 1, 1, [1]), | ||
(1, 2, 0, [0]), | ||
(0, 2, 0, []), | ||
(0, 10, 0, []), | ||
], | ||
) | ||
def test_get_page_valid(length: int, page: int, page_size: int, expected_data: List[int]) -> None: | ||
assert Paginator.create(list(range(0, length)), page_size).get_page(page) == expected_data | ||
|
||
|
||
@pytest.mark.parametrize("page", [-1000, -10, -1, 5, 10, 1000]) | ||
def test_get_page_invalid(page: int) -> None: | ||
with pytest.raises(PageOutOfBoundsError): | ||
Paginator.create(range(0, 17), 5).get_page(page) |