Skip to content

Commit 3548429

Browse files
committed
pagination fix
1 parent ecc3f98 commit 3548429

File tree

3 files changed

+125
-8
lines changed

3 files changed

+125
-8
lines changed

ninja/pagination.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,20 @@ def _inject_pagination(
214214
**paginator_params: Any,
215215
) -> Callable[..., Any]:
216216
paginator = paginator_class(**paginator_params)
217+
218+
# Check if Input schema has any fields
219+
# If it has no fields, we should make it optional to support Pydantic 2.12+
220+
has_input_fields = bool(paginator.Input.model_fields)
221+
217222
if is_async_callable(func):
218223
if not hasattr(paginator, "apaginate_queryset"):
219224
raise ConfigError("Pagination class not configured for async requests")
220225

221226
@wraps(func)
222227
async def view_with_pagination(request: HttpRequest, **kwargs: Any) -> Any:
223-
pagination_params = kwargs.pop("ninja_pagination")
228+
pagination_params = kwargs.pop("ninja_pagination", None)
229+
if pagination_params is None:
230+
pagination_params = paginator.Input()
224231
if paginator.pass_parameter:
225232
kwargs[paginator.pass_parameter] = pagination_params
226233

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

246253
@wraps(func)
247254
def view_with_pagination(request: HttpRequest, **kwargs: Any) -> Any:
248-
pagination_params = kwargs.pop("ninja_pagination")
255+
pagination_params = kwargs.pop("ninja_pagination", None)
256+
if pagination_params is None:
257+
pagination_params = paginator.Input()
249258
if paginator.pass_parameter:
250259
kwargs[paginator.pass_parameter] = pagination_params
251260

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

264-
contribute_operation_args(
265-
view_with_pagination,
266-
"ninja_pagination",
267-
paginator.Input,
268-
paginator.InputSource,
269-
)
273+
# Only contribute args if Input has fields
274+
# For empty Input schemas, don't add the parameter at all to support Pydantic 2.12+
275+
if has_input_fields:
276+
contribute_operation_args(
277+
view_with_pagination,
278+
"ninja_pagination",
279+
paginator.Input,
280+
paginator.InputSource,
281+
)
270282

271283
if paginator.Output: # type: ignore
272284
contribute_operation_callback(

tests/test_pagination.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ def paginate_queryset(self, items, pagination: Input, request, **params):
9999
}
100100

101101

102+
class NoPagination(PaginationBase):
103+
"""
104+
Pagination class that returns all records without slicing.
105+
Does not define its own Input - uses empty PaginationBase.Input.
106+
This reproduces bug from https://github.com/vitalik/django-ninja/issues/1564
107+
"""
108+
109+
def paginate_queryset(self, items, pagination: PaginationBase.Input, **params):
110+
return {
111+
"count": self._items_count(items),
112+
"items": items,
113+
}
114+
115+
102116
@api.get("/items_1", response=List[int])
103117
@paginate # WITHOUT brackets (should use default pagination)
104118
def items_1(request, **kwargs):
@@ -160,6 +174,12 @@ def items_10(request):
160174
return ITEMS
161175

162176

177+
@api.get("/items_no_pagination", response=List[int])
178+
@paginate(NoPagination)
179+
def items_no_pagination(request):
180+
return ITEMS
181+
182+
163183
client = TestClient(api)
164184

165185

@@ -593,3 +613,30 @@ def test_pagination_works_with_unnamed_classes():
593613
PydanticSchemaGenerationError
594614
): # It does fail after we passed the logic that we are testing
595615
make_response_paginated(LimitOffsetPagination, operation)
616+
617+
618+
def test_no_pagination_without_query_params():
619+
"""
620+
Test that NoPagination works without any query parameters.
621+
Reproduces bug from https://github.com/vitalik/django-ninja/issues/1564
622+
623+
NoPagination doesn't define any Input fields (uses empty PaginationBase.Input),
624+
so it should NOT require any query parameters.
625+
"""
626+
response = client.get("/items_no_pagination")
627+
if response.status_code != 200:
628+
print(f"Status: {response.status_code}")
629+
print(f"Response: {response.json()}")
630+
assert response.status_code == 200
631+
result = response.json()
632+
assert result == {"count": 100, "items": ITEMS}
633+
634+
# Check OpenAPI schema - should have no required parameters
635+
schema = api.get_openapi_schema()["paths"]["/api/items_no_pagination"]["get"]
636+
params = schema.get("parameters", [])
637+
638+
# If there are any parameters, they should all be optional
639+
for param in params:
640+
assert (
641+
param.get("required", False) is False
642+
), 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)