Skip to content

Commit 893b9a7

Browse files
authored
[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
1 parent 1455669 commit 893b9a7

File tree

6 files changed

+227
-46
lines changed

6 files changed

+227
-46
lines changed

stubs/django-filter/@tests/stubtest_allowlist.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,13 @@ django_filters.fields.Lookup.__doc__
1010

1111
# ChoiceIteratorMixin.choices: Cannot define choices property due to incompatibility with base class ChoiceField
1212
django_filters.fields.ChoiceIteratorMixin.choices
13+
14+
# Our __init__ signatures are more precise -- ignore "stub does not have *args argument"
15+
django_filters.fields.BaseCSVField.__init__
16+
django_filters.fields.ChoiceField.__init__
17+
django_filters.fields.ChoiceIteratorMixin.__init__
18+
django_filters.fields.LookupChoiceField.__init__
19+
django_filters.fields.MultipleChoiceField.__init__
20+
django_filters.fields.RangeField.__init__
21+
django_filters.filters.QuerySetRequestMixin.__init__
22+
django_filters.widgets.CSVWidget.__init__

stubs/django-filter/django_filters/fields.pyi

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
from collections.abc import Sequence
1+
from _typeshed import Unused
2+
from collections.abc import Callable, Iterable, Mapping, Sequence
23
from typing import Any, NamedTuple
34
from typing_extensions import TypeAlias
45

56
from django import forms
7+
from django.db.models import Choices
8+
from django.forms import Widget
9+
from django_stubs_ext import StrOrPromise
610

711
DJANGO_50: bool
812

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

1931
class RangeField(forms.MultiValueField):
2032
widget: _ClassLevelWidget = ...
2133
def __init__(
22-
self, fields: tuple[forms.Field, forms.Field] | None = None, *args: Any, **kwargs: Any
34+
self,
35+
fields: tuple[forms.Field, forms.Field] | None = None,
36+
*,
37+
# Inherited from Django MultiValueField
38+
require_all_fields: bool = True,
39+
required: bool = ...,
40+
widget: Widget | type[Widget] | None = ...,
41+
label: StrOrPromise | None = ...,
42+
initial: Any | None = ..., # Type depends on the form field used.
43+
help_text: StrOrPromise = ...,
44+
error_messages: Mapping[str, StrOrPromise] | None = ...,
45+
show_hidden_initial: bool = ...,
46+
validators: Sequence[_ValidatorCallable] = ...,
47+
localize: bool = ...,
48+
disabled: bool = ...,
49+
label_suffix: str | None = ...,
2350
) -> None: ... # Args/kwargs can be any field params, passes to parent
2451
def compress(self, data_list: list[Any] | None) -> slice | None: ... # Data list elements can be any field value type
2552

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

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

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

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

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

4770
class LookupChoiceField(forms.MultiValueField):
4871
def __init__(
49-
self, field: forms.Field, lookup_choices: Sequence[tuple[str, str]], *args: Any, **kwargs: Any
72+
self,
73+
field: forms.Field,
74+
lookup_choices: Sequence[tuple[str, str]],
75+
*,
76+
empty_label: StrOrPromise = ...,
77+
widget: Unused = ...,
78+
help_text: Unused = ...,
79+
# Inherited from Django MultiValueField
80+
require_all_fields: bool = True,
81+
required: bool = ...,
82+
label: StrOrPromise | None = ...,
83+
initial: Any | None = ..., # Type depends on the form field used.
84+
error_messages: Mapping[str, StrOrPromise] | None = ...,
85+
show_hidden_initial: bool = ...,
86+
validators: Sequence[_ValidatorCallable] = ...,
87+
localize: bool = ...,
88+
disabled: bool = ...,
89+
label_suffix: str | None = ...,
5090
) -> None: ... # Args/kwargs can be any field params, uses kwargs for empty_label
5191
def compress(self, data_list: list[Any] | None) -> Lookup | None: ... # Data list can contain any lookup components
5292

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

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

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

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

85124
class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField):
86125
iterator = ChoiceIterator
87-
empty_label: str | None
88-
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Args/kwargs can be any field params for label config
126+
empty_label: StrOrPromise
127+
def __init__(
128+
self,
129+
*,
130+
empty_label: StrOrPromise = ...,
131+
# Inherited from Django ChoiceField
132+
choices: _ChoicesInput = (),
133+
required: bool = ...,
134+
widget: Widget | type[Widget] | None = ...,
135+
label: StrOrPromise | None = ...,
136+
initial: Any | None = ..., # Type depends on the form field used.
137+
help_text: StrOrPromise = ...,
138+
error_messages: Mapping[str, StrOrPromise] | None = ...,
139+
show_hidden_initial: bool = ...,
140+
validators: Sequence[_ValidatorCallable] = ...,
141+
localize: bool = ...,
142+
disabled: bool = ...,
143+
label_suffix: str | None = ...,
144+
null_label: StrOrPromise | None,
145+
null_value: Any, # Type depends on the form field used.
146+
) -> None: ...
89147

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

95152
class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField[Any]):
96153
iterator = ModelChoiceIterator

stubs/django-filter/django_filters/filters.pyi

Lines changed: 112 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from collections.abc import Callable
1+
from collections.abc import Callable, Iterable
22
from typing import Any
33

44
from django import forms
55
from django.db.models import Q, QuerySet
66
from django.forms import Field
7+
from django_stubs_ext import StrOrPromise
78

89
from .fields import (
910
BaseCSVField,
@@ -12,6 +13,7 @@ from .fields import (
1213
DateTimeRangeField,
1314
IsoDateTimeField,
1415
IsoDateTimeRangeField,
16+
Lookup,
1517
LookupChoiceField,
1618
ModelChoiceField,
1719
ModelMultipleChoiceField,
@@ -65,15 +67,15 @@ class Filter:
6567
field_name: str | None = None,
6668
lookup_expr: str | None = None,
6769
*,
68-
label: str | None = None,
70+
label: StrOrPromise | None = None,
6971
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
7072
distinct: bool = False,
7173
exclude: bool = False,
7274
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
7375
) -> None: ...
7476
def get_method(self, qs: QuerySet[Any]) -> Callable[..., QuerySet[Any]]: ... # Returns QuerySet filtering methods
7577
method: Callable[..., Any] | str | None # Custom filter methods return various types
76-
label: str | None # Filter label for display
78+
label: StrOrPromise | None # Filter label for display
7779
@property
7880
def field(self) -> Field: ...
7981
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):
8789
class ChoiceFilter(Filter):
8890
field_class: type[Any] # Base class for choice-based filters
8991
null_value: Any # Null value can be any type (None, empty string, etc.)
90-
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for null_value config
92+
def __init__(
93+
self,
94+
field_name: str | None = None,
95+
lookup_expr: str | None = None,
96+
*,
97+
null_value: Any = ..., # Null value can be any type (None, empty string, etc.)
98+
# Inherited from Filter
99+
label: StrOrPromise | None = None,
100+
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
101+
distinct: bool = False,
102+
exclude: bool = False,
103+
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
104+
) -> None: ...
91105
def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]: ...
92106

93107
class TypedChoiceFilter(Filter):
@@ -101,7 +115,20 @@ class MultipleChoiceFilter(Filter):
101115
always_filter: bool
102116
conjoined: bool
103117
null_value: Any # Multiple choice null values vary by implementation
104-
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for distinct, conjoined, null_value config
118+
def __init__(
119+
self,
120+
field_name: str | None = None,
121+
lookup_expr: str | None = None,
122+
*,
123+
distinct: bool = True, # Overrides distinct default
124+
conjoined: bool = False,
125+
null_value: Any = ..., # Multiple choice null values vary by implementation
126+
# Inherited from Filter
127+
label: StrOrPromise | None = None,
128+
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
129+
exclude: bool = False,
130+
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
131+
) -> None: ...
105132
def is_noop(self, qs: QuerySet[Any], value: Any) -> bool: ... # Value can be any filter input
106133
def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]: ...
107134
def get_filter_predicate(self, v: Any) -> Q: ... # Predicate value can be any filter input type
@@ -126,18 +153,50 @@ class DurationFilter(Filter):
126153

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

135162
class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter):
136163
field_class: type[ModelChoiceField] # More specific than parent ChoiceField
137-
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for empty_label config
164+
def __init__(
165+
self,
166+
field_name: str | None = None,
167+
lookup_expr: str | None = None,
168+
*,
169+
# Inherited from QuerySetRequestMixin
170+
queryset: QuerySet[Any] | None = None,
171+
# Inherited from ChoiceFilter
172+
null_value: Any = ..., # Null value can be any type (None, empty string, etc.)
173+
# Inherited from Filter
174+
label: StrOrPromise | None = None,
175+
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
176+
distinct: bool = False,
177+
exclude: bool = False,
178+
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
179+
) -> None: ...
138180

139181
class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter):
140182
field_class: type[ModelMultipleChoiceField] # More specific than parent MultipleChoiceField
183+
def __init__(
184+
self,
185+
field_name: str | None = None,
186+
lookup_expr: str | None = None,
187+
*,
188+
# Inherited from QuerySetRequestMixin
189+
queryset: QuerySet[Any] | None = None,
190+
# Inherited from MultipleChoiceFilter
191+
distinct: bool = True, # Overrides distinct default
192+
conjoined: bool = False,
193+
null_value: Any = ..., # Multiple choice null values vary by implementation
194+
# Inherited from Filter
195+
label: StrOrPromise | None = None,
196+
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
197+
exclude: bool = False,
198+
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
199+
) -> None: ...
141200

142201
class NumberFilter(Filter):
143202
field_class: type[forms.DecimalField]
@@ -159,7 +218,20 @@ class DateRangeFilter(ChoiceFilter):
159218
choices: list[tuple[str, str]] | None
160219
filters: dict[str, Filter] | None
161220
def __init__(
162-
self, choices: list[tuple[str, str]] | None = None, filters: dict[str, Filter] | None = None, *args: Any, **kwargs: Any
221+
self,
222+
choices: list[tuple[str, str]] | None = None,
223+
filters: dict[str, Filter] | None = None,
224+
field_name: str | None = None,
225+
lookup_expr: str | None = None,
226+
*,
227+
# Inherited from ChoiceFilter
228+
null_value: Any = ..., # Null value can be any type (None, empty string, etc.)
229+
# Inherited from Filter
230+
label: StrOrPromise | None = None,
231+
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
232+
distinct: bool = False,
233+
exclude: bool = False,
234+
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
163235
) -> None: ... # Uses args/kwargs for choice and filter configuration
164236
def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]: ...
165237

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

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

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

198267
class LookupChoiceFilter(Filter):
199268
field_class: type[forms.CharField]
200269
outer_class: type[LookupChoiceField] = ...
201-
empty_label: str | None
202-
lookup_choices: list[tuple[str, str]] | None
270+
empty_label: StrOrPromise | None
271+
lookup_choices: list[tuple[str, StrOrPromise]] | None
203272
def __init__(
204273
self,
205274
field_name: str | None = None,
206-
lookup_choices: list[tuple[str, str]] | None = None,
275+
lookup_choices: list[tuple[str, StrOrPromise]] | None = None,
207276
field_class: type[Field] | None = None,
208-
**kwargs: Any, # Handles empty_label and other field config
277+
*,
278+
empty_label: StrOrPromise = ...,
279+
# Inherited from Filter
280+
label: StrOrPromise | None = None,
281+
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
282+
distinct: bool = False,
283+
exclude: bool = False,
284+
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
209285
) -> None: ...
210286
@classmethod
211-
def normalize_lookup(cls, lookup: Any) -> tuple[Any, str]: ...
212-
def get_lookup_choices(self) -> list[tuple[str, str]]: ...
287+
def normalize_lookup(cls, lookup: str | tuple[str, StrOrPromise]) -> tuple[str, StrOrPromise]: ...
288+
def get_lookup_choices(self) -> list[tuple[str, StrOrPromise]]: ...
213289
@property
214290
def field(self) -> Field: ...
215291
lookup_expr: str
216-
def filter(self, qs: QuerySet[Any], lookup: Any) -> QuerySet[Any]: ...
292+
def filter(self, qs: QuerySet[Any], lookup: Lookup) -> QuerySet[Any]: ...
217293

218294
class OrderingFilter(BaseCSVFilter, ChoiceFilter):
219295
field_class: type[BaseCSVField] # Inherits CSV field behavior for comma-separated ordering
220296
descending_fmt: str
221297
param_map: dict[str, str] | None
222-
def __init__(self, *args: Any, **kwargs: Any) -> None: ... # Uses kwargs for fields and field_labels config
298+
def __init__(
299+
self,
300+
field_name: str | None = None,
301+
lookup_expr: str | None = None,
302+
*,
303+
fields: dict[str, str] | Iterable[tuple[str, str]] = ...,
304+
field_labels: dict[str, StrOrPromise] = ...,
305+
# Inherited from ChoiceFilter
306+
null_value: Any = ..., # Null value can be any type (None, empty string, etc.)
307+
# Inherited from Filter
308+
label: StrOrPromise | None = None,
309+
method: Callable[..., Any] | str | None = None, # Filter methods can return various types
310+
distinct: bool = False,
311+
exclude: bool = False,
312+
**kwargs: Any, # Field kwargs stored as extra (required, help_text, etc.)
313+
) -> None: ...
223314
def get_ordering_value(self, param: str) -> str: ...
224315
def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]: ...
225316
@classmethod
226317
def normalize_fields(cls, fields: Any) -> list[str]: ...
227-
def build_choices(self, fields: Any, labels: dict[str, str] | None) -> list[tuple[str, str]]: ...
318+
def build_choices(self, fields: Any, labels: dict[str, StrOrPromise] | None) -> list[tuple[str, str]]: ...
228319

229320
class FilterMethod:
230321
f: Filter

0 commit comments

Comments
 (0)