Skip to content

Commit 8fb3532

Browse files
committed
🔧 Add Page.with_custom_options
1 parent fac2abc commit 8fb3532

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

‎fastapi_pagination/bases.py‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
from __future__ import annotations
22

33
from abc import ABC, abstractmethod
4+
from collections import ChainMap
45
from dataclasses import dataclass
5-
from typing import ClassVar, Generic, Sequence, Type, TypeVar
6+
from functools import wraps
7+
from typing import (
8+
Any,
9+
ClassVar,
10+
Dict,
11+
Generic,
12+
Mapping,
13+
Sequence,
14+
Type,
15+
TypeVar,
16+
cast,
17+
)
618

19+
from pydantic import BaseModel, create_model
720
from pydantic.generics import GenericModel
821
from pydantic.types import conint
922

@@ -23,6 +36,19 @@ def to_raw_params(self) -> RawParams:
2336
pass # pragma: no cover
2437

2538

39+
def _create_params(cls: Type[AbstractParams], fields: Dict[str, Any]) -> Mapping[str, Any]:
40+
if not issubclass(cls, BaseModel):
41+
raise ValueError(f"{cls.__name__} must be subclass of BaseModel")
42+
43+
incorrect = sorted(fields.keys() - cls.__fields__.keys())
44+
if incorrect:
45+
ending = "s" if len(incorrect) > 1 else ""
46+
raise ValueError(f"Unknown field{ending} {', '.join(incorrect)}")
47+
48+
anns = ChainMap(*(obj.__dict__.get("__annotations__", {}) for obj in cls.mro()))
49+
return {name: (anns[name], val) for name, val in fields.items()}
50+
51+
2652
class AbstractPage(GenericModel, Generic[T], ABC):
2753
__params_type__: ClassVar[Type[AbstractParams]]
2854

@@ -31,6 +57,22 @@ class AbstractPage(GenericModel, Generic[T], ABC):
3157
def create(cls: Type[C], items: Sequence[T], total: int, params: AbstractParams) -> C:
3258
pass # pragma: no cover
3359

60+
@classmethod
61+
def with_custom_options(cls: C, **kwargs: Any) -> C:
62+
params_cls = cast(Type[AbstractPage], cls).__params_type__
63+
64+
custom_params: Any = create_model(
65+
params_cls.__name__,
66+
__base__=params_cls,
67+
**_create_params(params_cls, kwargs),
68+
)
69+
70+
@wraps(cls, updated=()) # type: ignore
71+
class CustomPage(cls[T], Generic[T]): # type: ignore
72+
__params_type__: ClassVar[Type[AbstractParams]] = custom_params
73+
74+
return cast(C, CustomPage)
75+
3476
class Config:
3577
arbitrary_types_allowed = True
3678

‎tests/test_bases.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import Generic, TypeVar
2+
3+
from pytest import raises
4+
5+
from fastapi_pagination import Page
6+
from fastapi_pagination.bases import AbstractParams, RawParams
7+
8+
T = TypeVar("T")
9+
10+
11+
def test_custom_page_invalid_params_cls():
12+
class CustomParams(AbstractParams):
13+
def to_raw_params(self) -> RawParams:
14+
return RawParams(0, 0)
15+
16+
class CustomPage(Page[T], Generic[T]):
17+
__params_type__ = CustomParams
18+
19+
with raises(ValueError, match="^CustomParams must be subclass of BaseModel$"):
20+
CustomPage.with_custom_options(size=10)
21+
22+
23+
def test_custom_page_invalid_values():
24+
with raises(ValueError, match="^Unknown field smth_wrong$"):
25+
Page.with_custom_options(smth_wrong=100)
26+
27+
with raises(ValueError, match="^Unknown fields a, b, c"):
28+
Page.with_custom_options(a=1, b=2, c=3)
29+
30+
31+
def test_custom_page():
32+
page_cls = Page.with_custom_options()
33+
assert page_cls.__params_type__().dict() == {"size": 50, "page": 0}
34+
35+
page_cls = Page.with_custom_options(size=100)
36+
assert page_cls.__params_type__().dict() == {"size": 100, "page": 0}
37+
38+
page_cls = Page.with_custom_options(size=100, page=100)
39+
assert page_cls.__params_type__().dict() == {"size": 100, "page": 100}

0 commit comments

Comments
 (0)