fastapi-pagination icon indicating copy to clipboard operation
fastapi-pagination copied to clipboard

Bug with empty sequences

Open alexandreczg opened this issue 2 years ago • 4 comments

In the case our pagination returns an empty sequence, the execution will throw:

Traceback (most recent call last):
  File "/workspaces/allegro-api/./allegro_api/util/routing.py", line 37, in wrapper
    return await func(*args, **kwargs)
  File "/workspaces/allegro-api/./allegro_api/routers/content.py", line 157, in get_content_entries
    return paginate(
  File "/workspaces/allegro-api/.venv/lib/python3.10/site-packages/fastapi_pagination/paginator.py", line 28, in paginate
    return create_page(
  File "/workspaces/allegro-api/.venv/lib/python3.10/site-packages/fastapi_pagination/api.py", line 180, in create_page
    return _page_val.get().create(items, **kwargs)
  File "/workspaces/allegro-api/.venv/lib/python3.10/site-packages/fastapi_pagination/default.py", line 65, in create
    pages = ceil(total / size) if total is not None else None
ZeroDivisionError: division by zero

This is due to line #65:

pages = ceil(total / size) if total is not None else None

Full method:

class Page(BasePage[T], Generic[T]):
    page: Optional[GreaterEqualOne]
    size: Optional[GreaterEqualOne]
    pages: Optional[GreaterEqualZero] = None

    __params_type__ = Params

    @classmethod
    def create(
        cls,
        items: Sequence[T],
        params: AbstractParams,
        *,
        total: Optional[int] = None,
        **kwargs: Any,
    ) -> Page[T]:
        if not isinstance(params, Params):
            raise TypeError("Page should be used with Params")

        size = params.size if params.size is not None else total
        page = params.page if params.page is not None else 1
        pages = ceil(total / size) if total is not None else None

        return create_pydantic_model(
            cls,
            total=total,
            items=items,
            page=page,
            size=size,
            pages=pages,
            **kwargs,
        )

There should be guardrails in place for this.

if size == 0:
   pages = 0
else:
  pages = ceil(total / size) if total is not None else None

alexandreczg avatar Dec 14 '23 18:12 alexandreczg

Hi @alexandreczg,

New version 0.12.14 has been released, this issue should be fixed.

uriyyo avatar Dec 17 '23 14:12 uriyyo

Hi @uriyyo , thanks for the quick action. I'm still facing an error with empty sequences.

Traceback (most recent call last):
  File "/workspaces/allegro-api/./allegro_api/util/routing.py", line 37, in wrapper
    return await func(*args, **kwargs)
  File "/workspaces/allegro-api/./allegro_api/routers/v1/packages.py", line 304, in get_package_entries
    return paginate(
  File "/workspaces/allegro-api/.venv/lib/python3.10/site-packages/fastapi_pagination/paginator.py", line 28, in paginate
    return create_page(
  File "/workspaces/allegro-api/.venv/lib/python3.10/site-packages/fastapi_pagination/api.py", line 180, in create_page
    return _page_val.get().create(items, **kwargs)
  File "/workspaces/allegro-api/.venv/lib/python3.10/site-packages/fastapi_pagination/default.py", line 73, in create
    return create_pydantic_model(
  File "/workspaces/allegro-api/.venv/lib/python3.10/site-packages/fastapi_pagination/utils.py", line 151, in create_pydantic_model
    return model_cls(**kwargs)
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for CustomizedPage[Package]
size
  ensure this value is greater than or equal to 1 (type=value_error.number.not_ge; limit_value=1)

I think the size parameter on the customized page model needs to accept sizes of 0.

For what is worth, this is my custom paginate implementation that works:

def paginate(
    sequence: Sequence[T],
    params: Optional[AbstractParams] = None,
    length_function: Callable[[Sequence[T]], int] = len,
    *,
    transformer: Optional[SyncItemsTransformer] = None,
    additional_data: Optional[AdditionalData] = None,
) -> Any:
    check_installed_extensions()

    params, raw_params = verify_params(params, "limit-offset")

    items = sequence[raw_params.as_slice()]
    t_items = apply_items_transformer(items, transformer)

    length = length_function(sequence)
    length = length if length > 0 else None

    return create_page(
        t_items,
        total=length if raw_params.include_total else None,
        params=params,
        **(additional_data or {}),
    )

I get the following schema on empty sequences:

{
  "items": [],
  "total": null,
  "page": 1,
  "size": null,
  "pages": null
}

alexandreczg avatar Dec 18 '23 18:12 alexandreczg

I guess you need to remove size constraints from size from your custom page.

Could you please show how CustomizedPage definition looks like?

uriyyo avatar Dec 19 '23 14:12 uriyyo

it's just the default page with optional parameters

from typing import Any, Callable, Optional, Sequence, TypeVar

from fastapi_pagination import Page
from fastapi_pagination.api import apply_items_transformer, create_page
from fastapi_pagination.bases import AbstractParams
from fastapi_pagination.default import OptionalParams
from fastapi_pagination.types import AdditionalData, SyncItemsTransformer
from fastapi_pagination.utils import check_installed_extensions, verify_params

T = TypeVar("T")
PageWithOptionalParams = Page.with_params(
    OptionalParams
)  # https://github.com/uriyyo/fastapi-pagination/discussions/542


# The paginate method will not work if sequence is empty.
# Below is a rewrite of the paginate method that accepts empty sequences.
# Tracking bug: https://github.com/uriyyo/fastapi-pagination/issues/958
def paginate(
    sequence: Sequence[T],
    params: Optional[AbstractParams] = None,
    length_function: Callable[[Sequence[T]], int] = len,
    *,
    transformer: Optional[SyncItemsTransformer] = None,
    additional_data: Optional[AdditionalData] = None,
) -> Any:
    check_installed_extensions()

    params, raw_params = verify_params(params, "limit-offset")

    items = sequence[raw_params.as_slice()]
    t_items = apply_items_transformer(items, transformer)

    length = length_function(sequence)
    length = length if length > 0 else None

    return create_page(
        t_items,
        total=length if raw_params.include_total else None,
        params=params,
        **(additional_data or {}),
    )

alexandreczg avatar Dec 22 '23 23:12 alexandreczg

I have the same issue.

whatoeat2night avatar Feb 09 '24 04:02 whatoeat2night

It works fine when there are records in database filtered by the query. sql_paginate(db, query, params=OptionalParams( page=params.page, size=params.size)) But if there is no records, it will fail, the same error as above.

whatoeat2night avatar Feb 09 '24 04:02 whatoeat2night

Hi @whatoeat2night @alexandreczg I am planing to release the fix tomorrow.

uriyyo avatar Feb 09 '24 12:02 uriyyo

Hi @whatoeat2night @alexandreczg

New version 0.12.15 has been released, and this issue should be fixed

uriyyo avatar Feb 11 '24 13:02 uriyyo

@whatoeat2night @alexandreczg Any updates? Can I close this issue?

uriyyo avatar Feb 19 '24 11:02 uriyyo

@uriyyo I can confirm it is now working. Thank you for your dedication.

alexandreczg avatar Feb 25 '24 01:02 alexandreczg

@alexandreczg Thanks for your feedback, I am closing this issue

uriyyo avatar Feb 25 '24 10:02 uriyyo