Skip to content

Commit 9dc0046

Browse files
authored
Merge branch 'master' into pagination-items-attribute
2 parents eeb9d63 + cd273d0 commit 9dc0046

File tree

3 files changed

+129
-8
lines changed

3 files changed

+129
-8
lines changed

ninja/pagination.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def paginate_queryset(
4242
self,
4343
queryset: QuerySet,
4444
pagination: Any,
45+
request: HttpRequest,
4546
**params: Any,
4647
) -> Any:
4748
pass # pragma: no cover
@@ -64,6 +65,7 @@ async def apaginate_queryset(
6465
self,
6566
queryset: QuerySet,
6667
pagination: Any,
68+
request: HttpRequest,
6769
**params: Any,
6870
) -> Any:
6971
pass # pragma: no cover
@@ -92,6 +94,7 @@ def paginate_queryset(
9294
self,
9395
queryset: QuerySet,
9496
pagination: Input,
97+
request: HttpRequest,
9598
**params: Any,
9699
) -> Any:
97100
offset = pagination.offset
@@ -105,6 +108,7 @@ async def apaginate_queryset(
105108
self,
106109
queryset: QuerySet,
107110
pagination: Input,
111+
request: HttpRequest,
108112
**params: Any,
109113
) -> Any:
110114
offset = pagination.offset
@@ -144,6 +148,7 @@ def paginate_queryset(
144148
self,
145149
queryset: QuerySet,
146150
pagination: Input,
151+
request: HttpRequest,
147152
**params: Any,
148153
) -> Any:
149154
page_size = self._get_page_size(pagination.page_size)
@@ -157,6 +162,7 @@ async def apaginate_queryset(
157162
self,
158163
queryset: QuerySet,
159164
pagination: Input,
165+
request: HttpRequest,
160166
**params: Any,
161167
) -> Any:
162168
page_size = self._get_page_size(pagination.page_size)
@@ -214,13 +220,20 @@ def _inject_pagination(
214220
**paginator_params: Any,
215221
) -> Callable[..., Any]:
216222
paginator = paginator_class(**paginator_params)
223+
224+
# Check if Input schema has any fields
225+
# If it has no fields, we should make it optional to support Pydantic 2.12+
226+
has_input_fields = bool(paginator.Input.model_fields)
227+
217228
if is_async_callable(func):
218229
if not hasattr(paginator, "apaginate_queryset"):
219230
raise ConfigError("Pagination class not configured for async requests")
220231

221232
@wraps(func)
222233
async def view_with_pagination(request: HttpRequest, **kwargs: Any) -> Any:
223-
pagination_params = kwargs.pop("ninja_pagination")
234+
pagination_params = kwargs.pop("ninja_pagination", None)
235+
if pagination_params is None:
236+
pagination_params = paginator.Input()
224237
if paginator.pass_parameter:
225238
kwargs[paginator.pass_parameter] = pagination_params
226239

@@ -245,7 +258,9 @@ async def evaluate(results: Union[List, QuerySet]) -> AsyncGenerator:
245258

246259
@wraps(func)
247260
def view_with_pagination(request: HttpRequest, **kwargs: Any) -> Any:
248-
pagination_params = kwargs.pop("ninja_pagination")
261+
pagination_params = kwargs.pop("ninja_pagination", None)
262+
if pagination_params is None:
263+
pagination_params = paginator.Input()
249264
if paginator.pass_parameter:
250265
kwargs[paginator.pass_parameter] = pagination_params
251266

@@ -261,12 +276,15 @@ def view_with_pagination(request: HttpRequest, **kwargs: Any) -> Any:
261276
# ^ forcing queryset evaluation #TODO: check why pydantic did not do it here
262277
return result
263278

264-
contribute_operation_args(
265-
view_with_pagination,
266-
"ninja_pagination",
267-
paginator.Input,
268-
paginator.InputSource,
269-
)
279+
# Only contribute args if Input has fields
280+
# For empty Input schemas, don't add the parameter at all to support Pydantic 2.12+
281+
if has_input_fields:
282+
contribute_operation_args(
283+
view_with_pagination,
284+
"ninja_pagination",
285+
paginator.Input,
286+
paginator.InputSource,
287+
)
270288

271289
if paginator.Output: # type: ignore
272290
contribute_operation_callback(

tests/test_pagination.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ class CustomItemsPageNumberPagination(PageNumberPagination):
117117
class Output(Schema):
118118
results: List[int]
119119
count: int
120+
class NoPagination(PaginationBase):
121+
"""
122+
Pagination class that returns all records without slicing.
123+
Does not define its own Input - uses empty PaginationBase.Input.
124+
This reproduces bug from https://github.com/vitalik/django-ninja/issues/1564
125+
"""
126+
127+
def paginate_queryset(self, items, pagination: PaginationBase.Input, **params):
128+
return {
129+
"count": self._items_count(items),
130+
"items": items,
131+
}
120132

121133

122134
@api.get("/items_1", response=List[int])
@@ -192,6 +204,12 @@ def items_12(request):
192204
return list(range(100))
193205

194206

207+
@api.get("/items_no_pagination", response=List[int])
208+
@paginate(NoPagination)
209+
def items_no_pagination(request):
210+
return ITEMS
211+
212+
195213
client = TestClient(api)
196214

197215

@@ -635,3 +653,30 @@ def test_pagination_works_with_unnamed_classes():
635653
PydanticSchemaGenerationError
636654
): # It does fail after we passed the logic that we are testing
637655
make_response_paginated(LimitOffsetPagination, operation)
656+
657+
658+
def test_no_pagination_without_query_params():
659+
"""
660+
Test that NoPagination works without any query parameters.
661+
Reproduces bug from https://github.com/vitalik/django-ninja/issues/1564
662+
663+
NoPagination doesn't define any Input fields (uses empty PaginationBase.Input),
664+
so it should NOT require any query parameters.
665+
"""
666+
response = client.get("/items_no_pagination")
667+
if response.status_code != 200:
668+
print(f"Status: {response.status_code}")
669+
print(f"Response: {response.json()}")
670+
assert response.status_code == 200
671+
result = response.json()
672+
assert result == {"count": 100, "items": ITEMS}
673+
674+
# Check OpenAPI schema - should have no required parameters
675+
schema = api.get_openapi_schema()["paths"]["/api/items_no_pagination"]["get"]
676+
params = schema.get("parameters", [])
677+
678+
# If there are any parameters, they should all be optional
679+
for param in params:
680+
assert (
681+
param.get("required", False) is False
682+
), f"Parameter {param['name']} should not be required"

tests/test_pagination_async.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@ async def _items_count(self, queryset: QuerySet) -> int:
6565
return len(queryset)
6666

6767

68+
class AsyncNoPagination(AsyncPaginationBase):
69+
"""
70+
Async pagination class that returns all records without slicing.
71+
Does not define its own Input - uses empty PaginationBase.Input.
72+
This tests the bug fix from https://github.com/vitalik/django-ninja/issues/1564
73+
"""
74+
75+
async def apaginate_queryset(
76+
self, items, pagination: PaginationBase.Input, **params
77+
):
78+
await asyncio.sleep(0)
79+
return {
80+
"count": await self._aitems_count(items),
81+
"items": items,
82+
}
83+
84+
def paginate_queryset(self, items, pagination: PaginationBase.Input, **params):
85+
return {
86+
"count": self._items_count(items),
87+
"items": items,
88+
}
89+
90+
6891
@pytest.mark.asyncio
6992
async def test_async_config_error():
7093
api = NinjaAPI()
@@ -178,3 +201,38 @@ async def cats_paginated_page_number(request):
178201
"items": [{"title": "cat1"}, {"title": "cat2"}],
179202
"count": 2,
180203
}
204+
205+
206+
@pytest.mark.asyncio
207+
async def test_async_no_pagination_without_query_params():
208+
"""
209+
Test that AsyncNoPagination works without any query parameters.
210+
Tests the async branch of the bug fix from https://github.com/vitalik/django-ninja/issues/1564
211+
212+
AsyncNoPagination doesn't define any Input fields (uses empty PaginationBase.Input),
213+
so it should NOT require any query parameters.
214+
"""
215+
api = NinjaAPI()
216+
217+
@api.get("/items_no_pagination_async", response=List[int])
218+
@paginate(AsyncNoPagination)
219+
async def items_no_pagination_async(request):
220+
await asyncio.sleep(0)
221+
return ITEMS
222+
223+
client = TestAsyncClient(api)
224+
225+
response = await client.get("/items_no_pagination_async")
226+
assert response.status_code == 200
227+
result = response.json()
228+
assert result == {"count": 100, "items": ITEMS}
229+
230+
# Check OpenAPI schema - should have no required parameters
231+
schema = api.get_openapi_schema()["paths"]["/api/items_no_pagination_async"]["get"]
232+
params = schema.get("parameters", [])
233+
234+
# If there are any parameters, they should all be optional
235+
for param in params:
236+
assert (
237+
param.get("required", False) is False
238+
), f"Parameter {param['name']} should not be required"

0 commit comments

Comments
 (0)