Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions stubs/django-filter/@tests/stubtest_allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,13 @@ django_filters.fields.Lookup.__doc__

# ChoiceIteratorMixin.choices: Cannot define choices property due to incompatibility with base class ChoiceField
django_filters.fields.ChoiceIteratorMixin.choices

# Our __init__ signatures are more precise -- ignore "stub does not have *args argument"
django_filters.fields.BaseCSVField.__init__
django_filters.fields.ChoiceField.__init__
django_filters.fields.ChoiceIteratorMixin.__init__
django_filters.fields.LookupChoiceField.__init__
django_filters.fields.MultipleChoiceField.__init__
django_filters.fields.RangeField.__init__
django_filters.filters.QuerySetRequestMixin.__init__
django_filters.widgets.CSVWidget.__init__
85 changes: 71 additions & 14 deletions stubs/django-filter/django_filters/fields.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from collections.abc import Sequence
from _typeshed import Unused
from collections.abc import Callable, Iterable, Mapping, Sequence
from typing import Any, NamedTuple
from typing_extensions import TypeAlias

from django import forms
from django.db.models import Choices
from django.forms import Widget
from django_stubs_ext import StrOrPromise

DJANGO_50: bool

Expand All @@ -15,38 +19,74 @@ DJANGO_50: bool
# `widget = Select` will not typecheck.
# `Any` gives too much freedom, but does not create false positives.
_ClassLevelWidget: TypeAlias = Any
# Validator parameter type depends on type of the form field used.
_ValidatorCallable: TypeAlias = Callable[[Any], None]
# Based on django-stubs utils/choices.pyi
_Choice: TypeAlias = tuple[Any, Any]
_ChoiceNamedGroup: TypeAlias = tuple[str, Iterable[_Choice]]
_Choices: TypeAlias = Iterable[_Choice | _ChoiceNamedGroup]
_ChoicesMapping: TypeAlias = Mapping[Any, Any]
_ChoicesInput: TypeAlias = _Choices | _ChoicesMapping | type[Choices] | Callable[[], _Choices | _ChoicesMapping]

class RangeField(forms.MultiValueField):
widget: _ClassLevelWidget = ...
def __init__(
self, fields: tuple[forms.Field, forms.Field] | None = None, *args: Any, **kwargs: Any
self,
fields: tuple[forms.Field, forms.Field] | None = None,
*,
# Inherited from Django MultiValueField
require_all_fields: bool = True,
required: bool = ...,
widget: Widget | type[Widget] | None = ...,
label: StrOrPromise | None = ...,
initial: Any | None = ..., # Type depends on the form field used.
help_text: StrOrPromise = ...,
error_messages: Mapping[str, StrOrPromise] | None = ...,
show_hidden_initial: bool = ...,
validators: Sequence[_ValidatorCallable] = ...,
localize: bool = ...,
disabled: bool = ...,
label_suffix: str | None = ...,
) -> None: ... # Args/kwargs can be any field params, passes to parent
def compress(self, data_list: list[Any] | None) -> slice | None: ... # Data list elements can be any field value type

class DateRangeField(RangeField):
widget: _ClassLevelWidget = ...
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any field params for parent
def compress(self, data_list: list[Any] | None) -> slice | None: ... # Date values in list can be any date type

class DateTimeRangeField(RangeField):
widget: _ClassLevelWidget = ...
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any field params for parent

class IsoDateTimeRangeField(RangeField):
widget: _ClassLevelWidget = ...
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any field params for parent

class TimeRangeField(RangeField):
widget: _ClassLevelWidget = ...
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any field params for parent

class Lookup(NamedTuple):
value: Any # Lookup values can be any filterable type
lookup_expr: str

class LookupChoiceField(forms.MultiValueField):
def __init__(
self, field: forms.Field, lookup_choices: Sequence[tuple[str, str]], *args: Any, **kwargs: Any
self,
field: forms.Field,
lookup_choices: Sequence[tuple[str, str]],
*,
empty_label: StrOrPromise = ...,
widget: Unused = ...,
help_text: Unused = ...,
# Inherited from Django MultiValueField
require_all_fields: bool = True,
required: bool = ...,
label: StrOrPromise | None = ...,
initial: Any | None = ..., # Type depends on the form field used.
error_messages: Mapping[str, StrOrPromise] | None = ...,
show_hidden_initial: bool = ...,
validators: Sequence[_ValidatorCallable] = ...,
localize: bool = ...,
disabled: bool = ...,
label_suffix: str | None = ...,
) -> None: ... # Args/kwargs can be any field params, uses kwargs for empty_label
def compress(self, data_list: list[Any] | None) -> Lookup | None: ... # Data list can contain any lookup components

Expand All @@ -57,7 +97,6 @@ class IsoDateTimeField(forms.DateTimeField):

class BaseCSVField(forms.Field):
base_widget_class: _ClassLevelWidget = ...
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any field params for widget config
def clean(self, value: Any) -> Any: ... # Cleaned values can be any valid field type

class BaseRangeField(BaseCSVField):
Expand All @@ -78,19 +117,37 @@ class ModelChoiceIterator(forms.models.ModelChoiceIterator):
def __len__(self) -> int: ...

class ChoiceIteratorMixin:
null_label: str | None
null_label: StrOrPromise | None
null_value: Any # Null choice values can be any type (None, empty string, etc.)
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any field params for null config
def __init__(self, *, null_label: StrOrPromise | None, null_value: Any) -> None: ...

class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField):
iterator = ChoiceIterator
empty_label: str | None
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any field params for label config
empty_label: StrOrPromise
def __init__(
self,
*,
empty_label: StrOrPromise = ...,
# Inherited from Django ChoiceField
choices: _ChoicesInput = (),
required: bool = ...,
widget: Widget | type[Widget] | None = ...,
label: StrOrPromise | None = ...,
initial: Any | None = ..., # Type depends on the form field used.
help_text: StrOrPromise = ...,
error_messages: Mapping[str, StrOrPromise] | None = ...,
show_hidden_initial: bool = ...,
validators: Sequence[_ValidatorCallable] = ...,
localize: bool = ...,
disabled: bool = ...,
label_suffix: str | None = ...,
null_label: StrOrPromise | None,
null_value: Any, # Type depends on the form field used.
) -> None: ...

class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField):
iterator = ChoiceIterator
empty_label: str | None
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any field params, sets empty_label
empty_label: StrOrPromise | None

class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField[Any]):
iterator = ModelChoiceIterator
Expand Down
133 changes: 112 additions & 21 deletions stubs/django-filter/django_filters/filters.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from collections.abc import Callable
from collections.abc import Callable, Iterable
from typing import Any

from django import forms
from django.db.models import Q, QuerySet
from django.forms import Field
from django_stubs_ext import StrOrPromise

from .fields import (
BaseCSVField,
Expand All @@ -12,6 +13,7 @@ from .fields import (
DateTimeRangeField,
IsoDateTimeField,
IsoDateTimeRangeField,
Lookup,
LookupChoiceField,
ModelChoiceField,
ModelMultipleChoiceField,
Expand Down Expand Up @@ -65,15 +67,15 @@ class Filter:
field_name: str | None = None,
lookup_expr: str | None = None,
*,
label: str | None = None,
label: StrOrPromise | None = None,
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
distinct: bool = False,
exclude: bool = False,
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
) -> None: ...
def get_method(self, qs: QuerySet[Any]) -> Callable[..., QuerySet[Any]]: ... # Returns QuerySet filtering methods
method: Callable[..., Any] | str | None # Custom filter methods return various types
label: str | None # Filter label for display
label: StrOrPromise | None # Filter label for display
@property
def field(self) -> Field: ...
def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]: ... # Filter value can be any user input type
Expand All @@ -87,7 +89,19 @@ class BooleanFilter(Filter):
class ChoiceFilter(Filter):
field_class: type[Any] # Base class for choice-based filters
null_value: Any # Null value can be any type (None, empty string, etc.)
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for null_value config
def __init__(
self,
field_name: str | None = None,
lookup_expr: str | None = None,
*,
null_value: Any = ..., # Null value can be any type (None, empty string, etc.)
# Inherited from Filter
label: StrOrPromise | None = None,
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
distinct: bool = False,
exclude: bool = False,
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
) -> None: ...
def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]: ...

class TypedChoiceFilter(Filter):
Expand All @@ -101,7 +115,20 @@ class MultipleChoiceFilter(Filter):
always_filter: bool
conjoined: bool
null_value: Any # Multiple choice null values vary by implementation
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for distinct, conjoined, null_value config
def __init__(
self,
field_name: str | None = None,
lookup_expr: str | None = None,
*,
distinct: bool = True, # Overrides distinct default
conjoined: bool = False,
null_value: Any = ..., # Multiple choice null values vary by implementation
# Inherited from Filter
label: StrOrPromise | None = None,
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
exclude: bool = False,
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
) -> None: ...
def is_noop(self, qs: QuerySet[Any], value: Any) -> bool: ... # Value can be any filter input
def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]: ...
def get_filter_predicate(self, v: Any) -> Q: ... # Predicate value can be any filter input type
Expand All @@ -126,18 +153,50 @@ class DurationFilter(Filter):

class QuerySetRequestMixin:
queryset: QuerySet[Any] | None
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for queryset config
def __init__(self, *, queryset: QuerySet[Any] | None) -> None: ...
def get_request(self) -> Any: ... # Request can be HttpRequest or other request types
def get_queryset(self, request: Any) -> QuerySet[Any]: ... # Request parameter accepts various request types
@property
def field(self) -> Field: ...

class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter):
field_class: type[ModelChoiceField] # More specific than parent ChoiceField
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for empty_label config
def __init__(
self,
field_name: str | None = None,
lookup_expr: str | None = None,
*,
# Inherited from QuerySetRequestMixin
queryset: QuerySet[Any] | None = None,
# Inherited from ChoiceFilter
null_value: Any = ..., # Null value can be any type (None, empty string, etc.)
# Inherited from Filter
label: StrOrPromise | None = None,
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
distinct: bool = False,
exclude: bool = False,
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
) -> None: ...

class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter):
field_class: type[ModelMultipleChoiceField] # More specific than parent MultipleChoiceField
def __init__(
self,
field_name: str | None = None,
lookup_expr: str | None = None,
*,
# Inherited from QuerySetRequestMixin
queryset: QuerySet[Any] | None = None,
# Inherited from MultipleChoiceFilter
distinct: bool = True, # Overrides distinct default
conjoined: bool = False,
null_value: Any = ..., # Multiple choice null values vary by implementation
# Inherited from Filter
label: StrOrPromise | None = None,
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
exclude: bool = False,
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
) -> None: ...

class NumberFilter(Filter):
field_class: type[forms.DecimalField]
Expand All @@ -159,7 +218,20 @@ class DateRangeFilter(ChoiceFilter):
choices: list[tuple[str, str]] | None
filters: dict[str, Filter] | None
def __init__(
self, choices: list[tuple[str, str]] | None = None, filters: dict[str, Filter] | None = None, *args: Any, **kwargs: Any
self,
choices: list[tuple[str, str]] | None = None,
filters: dict[str, Filter] | None = None,
field_name: str | None = None,
lookup_expr: str | None = None,
*,
# Inherited from ChoiceFilter
null_value: Any = ..., # Null value can be any type (None, empty string, etc.)
# Inherited from Filter
label: StrOrPromise | None = None,
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
distinct: bool = False,
exclude: bool = False,
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
) -> None: ... # Uses args/kwargs for choice and filter configuration
def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]: ...

Expand All @@ -186,45 +258,64 @@ class AllValuesMultipleFilter(MultipleChoiceFilter):
class BaseCSVFilter(Filter):
base_field_class: type[BaseCSVField] = ...
field_class: type[Any] # Base class for CSV-based filters
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for help_text and widget config

class BaseInFilter(BaseCSVFilter):
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Sets lookup_expr and passes through
class BaseInFilter(BaseCSVFilter): ...

class BaseRangeFilter(BaseCSVFilter):
base_field_class: type[BaseRangeField] = ...
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Sets lookup_expr and passes through

class LookupChoiceFilter(Filter):
field_class: type[forms.CharField]
outer_class: type[LookupChoiceField] = ...
empty_label: str | None
lookup_choices: list[tuple[str, str]] | None
empty_label: StrOrPromise | None
lookup_choices: list[tuple[str, StrOrPromise]] | None
def __init__(
self,
field_name: str | None = None,
lookup_choices: list[tuple[str, str]] | None = None,
lookup_choices: list[tuple[str, StrOrPromise]] | None = None,
field_class: type[Field] | None = None,
**kwargs: Any, # Handles empty_label and other field config
*,
empty_label: StrOrPromise = ...,
# Inherited from Filter
label: StrOrPromise | None = None,
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
distinct: bool = False,
exclude: bool = False,
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
) -> None: ...
@classmethod
def normalize_lookup(cls, lookup: Any) -> tuple[Any, str]: ...
def get_lookup_choices(self) -> list[tuple[str, str]]: ...
def normalize_lookup(cls, lookup: str | tuple[str, StrOrPromise]) -> tuple[str, StrOrPromise]: ...
def get_lookup_choices(self) -> list[tuple[str, StrOrPromise]]: ...
@property
def field(self) -> Field: ...
lookup_expr: str
def filter(self, qs: QuerySet[Any], lookup: Any) -> QuerySet[Any]: ...
def filter(self, qs: QuerySet[Any], lookup: Lookup) -> QuerySet[Any]: ...

class OrderingFilter(BaseCSVFilter, ChoiceFilter):
field_class: type[BaseCSVField] # Inherits CSV field behavior for comma-separated ordering
descending_fmt: str
param_map: dict[str, str] | None
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for fields and field_labels config
def __init__(
self,
field_name: str | None = None,
lookup_expr: str | None = None,
*,
fields: dict[str, str] | Iterable[tuple[str, str]] = ...,
field_labels: dict[str, StrOrPromise] = ...,
# Inherited from ChoiceFilter
null_value: Any = ..., # Null value can be any type (None, empty string, etc.)
# Inherited from Filter
label: StrOrPromise | None = None,
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
distinct: bool = False,
exclude: bool = False,
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
) -> None: ...
def get_ordering_value(self, param: str) -> str: ...
def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]: ...
@classmethod
def normalize_fields(cls, fields: Any) -> list[str]: ...
def build_choices(self, fields: Any, labels: dict[str, str] | None) -> list[tuple[str, str]]: ...
def build_choices(self, fields: Any, labels: dict[str, StrOrPromise] | None) -> list[tuple[str, str]]: ...

class FilterMethod:
f: Filter
Expand Down
Loading
Loading