[django-filter] Improve constructor param types and add missing re-exports (#14556)

* Accept `StrOrPromise` for field labels -- allow Django lazy translated strings.
* Added `__init__` params that are inherited from parent classes. Reduced usage of loosely typed `*args, **kwargs`.
* Removed `__init__` method type hints from classes whose parameters are same as parent class -- to avoid duplicating them.
* Added missing re-exports to `django_filters/rest_framework/__init__.pyi` -- the imports in this file are clearly meant as re-export
This commit is contained in:
Marti Raudsepp
2025-08-14 21:56:30 +03:00
committed by GitHub
parent 1455669dde
commit 893b9a760d
6 changed files with 227 additions and 46 deletions
@@ -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__
+71 -14
View File
@@ -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
@@ -15,30 +19,49 @@ 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
@@ -46,7 +69,24 @@ class Lookup(NamedTuple):
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
@@ -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):
@@ -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
+112 -21
View File
@@ -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,
@@ -12,6 +13,7 @@ from .fields import (
DateTimeRangeField,
IsoDateTimeField,
IsoDateTimeRangeField,
Lookup,
LookupChoiceField,
ModelChoiceField,
ModelMultipleChoiceField,
@@ -65,7 +67,7 @@ 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,
@@ -73,7 +75,7 @@ class Filter:
) -> 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
@@ -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):
@@ -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
@@ -126,7 +153,7 @@ 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
@@ -134,10 +161,42 @@ class QuerySetRequestMixin:
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]
@@ -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]: ...
@@ -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
@@ -1,3 +1,34 @@
from .backends import DjangoFilterBackend as DjangoFilterBackend
from .filters import *
from .filters import (
AllValuesFilter as AllValuesFilter,
AllValuesMultipleFilter as AllValuesMultipleFilter,
BaseCSVFilter as BaseCSVFilter,
BaseInFilter as BaseInFilter,
BaseRangeFilter as BaseRangeFilter,
BooleanFilter as BooleanFilter,
CharFilter as CharFilter,
ChoiceFilter as ChoiceFilter,
DateFilter as DateFilter,
DateFromToRangeFilter as DateFromToRangeFilter,
DateRangeFilter as DateRangeFilter,
DateTimeFilter as DateTimeFilter,
DateTimeFromToRangeFilter as DateTimeFromToRangeFilter,
DurationFilter as DurationFilter,
Filter as Filter,
IsoDateTimeFilter as IsoDateTimeFilter,
IsoDateTimeFromToRangeFilter as IsoDateTimeFromToRangeFilter,
LookupChoiceFilter as LookupChoiceFilter,
ModelChoiceFilter as ModelChoiceFilter,
ModelMultipleChoiceFilter as ModelMultipleChoiceFilter,
MultipleChoiceFilter as MultipleChoiceFilter,
NumberFilter as NumberFilter,
NumericRangeFilter as NumericRangeFilter,
OrderingFilter as OrderingFilter,
RangeFilter as RangeFilter,
TimeFilter as TimeFilter,
TimeRangeFilter as TimeRangeFilter,
TypedChoiceFilter as TypedChoiceFilter,
TypedMultipleChoiceFilter as TypedMultipleChoiceFilter,
UUIDFilter as UUIDFilter,
)
from .filterset import FilterSet as FilterSet
@@ -1,5 +1,3 @@
from typing import Any
from ..filters import (
AllValuesFilter,
AllValuesMultipleFilter,
@@ -67,5 +65,4 @@ __all__ = [
]
# REST framework specific BooleanFilter that uses BooleanWidget by default
class BooleanFilter(_BaseBooleanFilter):
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Accepts any filter initialization params
class BooleanFilter(_BaseBooleanFilter): ...
@@ -30,7 +30,6 @@ class LinkWidget(forms.Widget):
class SuffixedMultiWidget(forms.MultiWidget):
suffixes: list[str]
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any widget params for MultiWidget
def suffixed(self, name: str, suffix: str) -> str: ...
# Widget value and context can contain any data types
def get_context(self, name: str, value: Any, attrs: dict[str, Any] | None) -> dict[str, Any]: ...
@@ -70,16 +69,12 @@ class BaseCSVWidget(forms.Widget):
# Can be widget class or instance - __init__ converts to instance via instantiation or deepcopy
surrogate: type[Any] = ...
# Args/kwargs can be any widget params for surrogate init
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
# CSV widget data can contain any types
def value_from_datadict(self, data: Mapping[str, Any], files: Mapping[str, Any], name: str) -> list[str]: ...
# Widget value and renderer can be any type
def render(self, name: str, value: Any, attrs: dict[str, Any] | None = None, renderer: Any | None = None) -> SafeString: ...
class CSVWidget(BaseCSVWidget, forms.TextInput):
# Args/kwargs can be any widget params, attrs for styling
def __init__(self, *args: Any, attrs: dict[str, Any] | None = None, **kwargs: Any) -> None: ...
class CSVWidget(BaseCSVWidget, forms.TextInput): ...
class QueryArrayWidget(BaseCSVWidget, forms.TextInput):
# Query array widget data can contain any types