-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
util: Implement Paginator
class as interface to access a list by pages
#11247
Changes from 5 commits
a21eb44
7f9f53d
a948633
9be6b8b
34a41c1
ca5b10e
04d9b60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,46 @@ | ||||||||||
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 | ||||||||||
_page_size_limit: int = 100 | ||||||||||
|
||||||||||
def __post_init__(self) -> None: | ||||||||||
if self._page_size_limit < 1: | ||||||||||
raise InvalidPageSizeLimit(self._page_size_limit) | ||||||||||
if self._page_size > self._page_size_limit: | ||||||||||
raise InvalidPageSizeError(self._page_size, self._page_size_limit) | ||||||||||
|
||||||||||
def page_size(self) -> int: | ||||||||||
return self._page_size | ||||||||||
|
||||||||||
def page_size_limit(self) -> int: | ||||||||||
return self._page_size_limit | ||||||||||
|
||||||||||
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() - 1: | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You wanna raise for valid values? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. uh... right... i like 'em in order. (remember when you had an approval and gave me more chances to complain?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I almost like it, but what about 04d9b60 😄 (swap the order) |
||||||||||
raise PageOutOfBoundsError(page, self.page_count() - 1) | ||||||||||
offset = page * self._page_size | ||||||||||
return self._source[offset : offset + self._page_size] |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,75 @@ | ||||||
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(source, page_size, page_size_limit) | ||||||
assert paginator.page_size() == page_size | ||||||
assert paginator.page_size_limit() == page_size_limit | ||||||
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([], 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(range(0, i), page_size).page_count() == max(1, ceil(i / page_size)) | ||||||
|
||||||
|
||||||
def test_empty_source() -> None: | ||||||
assert Paginator([], 5).get_page(0) == [] | ||||||
|
||||||
|
||||||
@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(list(range(0, length)), page_size).get_page(page) == expected_data | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
|
||||||
|
||||||
@pytest.mark.parametrize("page", [-1000, -10, -1, 5, 10, 1000]) | ||||||
def test_get_page_invalid(page: int) -> None: | ||||||
with pytest.raises(PageOutOfBoundsError): | ||||||
Paginator(range(0, 17), 5).get_page(page) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want 1-based paging instead of 0-based? I don't know what is normal for HTTP APIs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know either. But i changed it to 0-based now because i was also not super convinced about the decision, see a948633