From 846126987d3c8129ab98919212f34691640ffbe4 Mon Sep 17 00:00:00 2001 From: Konstantin Alekseev Date: Wed, 7 Oct 2026 09:27:58 +0300 Subject: [PATCH] fix(django-filter): preserve queryset model and row types --- .../check_rest_framework_backends.py | 36 +++++++++++++++++++ .../rest_framework/backends.pyi | 10 ++++-- 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 stubs/django-filter/@tests/test_cases/check_rest_framework_backends.py diff --git a/stubs/django-filter/@tests/test_cases/check_rest_framework_backends.py b/stubs/django-filter/@tests/test_cases/check_rest_framework_backends.py new file mode 100644 index 000000000000..a825d869f22d --- /dev/null +++ b/stubs/django-filter/@tests/test_cases/check_rest_framework_backends.py @@ -0,0 +1,36 @@ +from typing import Any, TypedDict +from typing_extensions import assert_type + +from django.db.models import Model, QuerySet +from django.http import HttpRequest +from django_filters.rest_framework import DjangoFilterBackend + + +class Product(Model): ... + + +class Customer(Model): ... + + +class ProductRow(TypedDict): + name: str + + +def check_filter_queryset( + request: HttpRequest, + view: object, + models: QuerySet[Product], + values: QuerySet[Product, ProductRow], + tuples: QuerySet[Product, tuple[int, str]], + dynamic_rows: QuerySet[Product, Any], +) -> None: + backend = DjangoFilterBackend() + assert_type(backend.filter_queryset(request, models, view), QuerySet[Product]) + assert_type(backend.filter_queryset(request, values, view), QuerySet[Product, ProductRow]) + assert_type(backend.filter_queryset(request, tuples, view), QuerySet[Product, tuple[int, str]]) + + assert_type(backend.filter_queryset(request, dynamic_rows, view), QuerySet[Product, Any]) + + filtered = backend.filter_queryset(request, values, view) + _wrong_model: QuerySet[Customer, ProductRow] = filtered # type: ignore[assignment] + _wrong_row: QuerySet[Product, str] = filtered # type: ignore[assignment] diff --git a/stubs/django-filter/django_filters/rest_framework/backends.pyi b/stubs/django-filter/django_filters/rest_framework/backends.pyi index c58bf4c98260..922ccafbd5a6 100644 --- a/stubs/django-filter/django_filters/rest_framework/backends.pyi +++ b/stubs/django-filter/django_filters/rest_framework/backends.pyi @@ -1,6 +1,7 @@ from typing import Any, TypeAlias +from typing_extensions import TypeVar -from django.db.models import QuerySet +from django.db.models import Model, QuerySet from django.http import HttpRequest from django_filters.filterset import FilterSetMetaclass @@ -9,6 +10,9 @@ from . import filterset # APIView placeholder - djangorestframework is optional, so we use Any for compatibility _APIView: TypeAlias = Any +_Model = TypeVar("_Model", bound=Model) +_Row = TypeVar("_Row", default=_Model) + class DjangoFilterBackend: filterset_base: FilterSetMetaclass = ... raise_exception: bool @@ -25,5 +29,7 @@ class DjangoFilterBackend: def get_filterset_kwargs(self, request: HttpRequest, queryset: QuerySet[Any], view: _APIView) -> dict[str, Any]: ... # Filters any model type - def filter_queryset(self, request: HttpRequest, queryset: QuerySet[Any], view: _APIView) -> QuerySet[Any]: ... + def filter_queryset( + self, request: HttpRequest, queryset: QuerySet[_Model, _Row], view: _APIView + ) -> QuerySet[_Model, _Row]: ... def to_html(self, request: HttpRequest, queryset: QuerySet[Any], view: _APIView) -> str: ... # Renders form for any model