Skip to content

Commit c84dd6b

Browse files
committed
Refactor _WidgetTypeOrInstance to generic descriptor, updated tests and added widgets to allowlist.txt
1 parent 6c0e4a0 commit c84dd6b

File tree

14 files changed

+246
-114
lines changed

14 files changed

+246
-114
lines changed

django-stubs/contrib/auth/forms.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ from django.contrib.auth.tokens import PasswordResetTokenGenerator
88
from django.core.exceptions import ValidationError
99
from django.db import models
1010
from django.db.models.fields import _ErrorMessagesDict
11+
from django.forms.fields import _WidgetTypeOrInstance
1112
from django.forms.widgets import Widget
1213
from django.http.request import HttpRequest
1314
from django.utils.functional import _StrOrPromise
@@ -21,6 +22,7 @@ class ReadOnlyPasswordHashWidget(forms.Widget):
2122
def get_context(self, name: str, value: Any, attrs: dict[str, Any] | None) -> dict[str, Any]: ...
2223

2324
class ReadOnlyPasswordHashField(forms.Field):
25+
widget: _WidgetTypeOrInstance[ReadOnlyPasswordHashWidget] # type: ignore[assignment]
2426
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
2527

2628
class UsernameField(forms.CharField):

django-stubs/contrib/gis/forms/fields.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from typing import Any
22

33
from django import forms
4+
from django.contrib.gis.forms.widgets import OpenLayersWidget
5+
from django.forms.fields import _WidgetTypeOrInstance
46

57
class GeometryField(forms.Field):
8+
widget: _WidgetTypeOrInstance[OpenLayersWidget] # type: ignore[assignment]
69
geom_type: str
710
srid: Any
811
def __init__(self, *, srid: Any | None = ..., geom_type: Any | None = ..., **kwargs: Any) -> None: ...

django-stubs/contrib/postgres/forms/array.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ from typing import Any, ClassVar
33

44
from django import forms
55
from django.db.models.fields import _ErrorMessagesDict
6+
from django.forms.fields import _WidgetTypeOrInstance
67
from django.forms.utils import _DataT, _FilesT
78
from django.forms.widgets import _OptAttrs
89

@@ -44,6 +45,7 @@ class SplitArrayWidget(forms.Widget):
4445
def needs_multipart_form(self) -> bool: ... # type: ignore[override]
4546

4647
class SplitArrayField(forms.Field):
48+
widget: _WidgetTypeOrInstance[forms.TextInput, SplitArrayWidget] # type: ignore[assignment]
4749
default_error_messages: ClassVar[_ErrorMessagesDict]
4850
base_field: forms.Field
4951
size: int

django-stubs/contrib/postgres/forms/ranges.pyi

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ from typing import Any, ClassVar
22

33
from django import forms
44
from django.db.models.fields import _ErrorMessagesDict
5-
from django.forms.widgets import MultiWidget, _OptAttrs
5+
from django.forms.fields import _WidgetTypeOrInstance
6+
from django.forms.widgets import MultiWidget, TextInput, _OptAttrs
67
from psycopg2.extras import Range # type: ignore [import-untyped]
78

89
class RangeWidget(MultiWidget):
@@ -17,6 +18,7 @@ class BaseRangeField(forms.MultiValueField):
1718
base_field: type[forms.Field]
1819
range_type: type[Range]
1920
hidden_widget: type[forms.Widget]
21+
widget: _WidgetTypeOrInstance[TextInput, RangeWidget] # type: ignore[assignment]
2022
def __init__(self, **kwargs: Any) -> None: ...
2123
def prepare_value(self, value: Any) -> Any: ...
2224
def compress(self, values: tuple[Any | None, Any | None]) -> Range | None: ...

django-stubs/forms/fields.pyi

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,54 @@ import datetime
22
from collections.abc import Collection, Iterator, Sequence
33
from decimal import Decimal
44
from re import Pattern
5-
from typing import Any, ClassVar, Protocol, overload, type_check_only
5+
from typing import Any, ClassVar, Generic, Protocol, TypeVar, overload, type_check_only
66
from uuid import UUID
77

88
from django.core.files import File
99
from django.core.validators import _ValidatorCallable
1010
from django.db.models.fields import _ErrorMessagesDict, _ErrorMessagesMapping
1111
from django.forms.boundfield import BoundField
1212
from django.forms.forms import BaseForm
13-
from django.forms.widgets import Widget
13+
from django.forms.widgets import (
14+
CheckboxInput,
15+
ClearableFileInput,
16+
DateInput,
17+
DateTimeInput,
18+
EmailInput,
19+
NullBooleanSelect,
20+
NumberInput,
21+
Select,
22+
SelectMultiple,
23+
SplitDateTimeWidget,
24+
Textarea,
25+
TextInput,
26+
TimeInput,
27+
URLInput,
28+
Widget,
29+
)
1430
from django.utils.choices import CallableChoiceIterator, _ChoicesCallable, _ChoicesInput
1531
from django.utils.datastructures import _PropertyDescriptor
1632
from django.utils.functional import _StrOrPromise
1733

34+
_ClassWidget = TypeVar("_ClassWidget", bound=Widget)
35+
_InstanceWidget = TypeVar("_InstanceWidget", bound=Widget, default=_ClassWidget)
36+
1837
@type_check_only
19-
class _WidgetTypeOrInstance:
38+
class _WidgetTypeOrInstance(Generic[_ClassWidget, _InstanceWidget]):
2039
@overload
21-
def __get__(self, instance: None, owner: type[Field]) -> type[Widget] | Widget: ...
40+
def __get__(self, instance: None, owner: type[Field]) -> type[_ClassWidget] | _ClassWidget: ...
2241
@overload
23-
def __get__(self, instance: Field, owner: type[Field]) -> Widget: ...
42+
def __get__(self, instance: Field, owner: type[Field]) -> _InstanceWidget: ...
2443
@overload
25-
def __set__(self, instance: None, value: type[Widget] | Widget) -> None: ...
44+
def __set__(self, instance: None, value: type[_ClassWidget] | _ClassWidget) -> None: ...
2645
@overload
27-
def __set__(self, instance: Field, value: Widget) -> None: ...
46+
def __set__(self, instance: Field, value: _InstanceWidget) -> None: ...
2847

2948
class Field:
3049
initial: Any
3150
label: _StrOrPromise | None
3251
required: bool
33-
widget: _WidgetTypeOrInstance
52+
widget: _WidgetTypeOrInstance[TextInput]
3453
hidden_widget: type[Widget]
3554
default_validators: list[_ValidatorCallable]
3655
default_error_messages: ClassVar[_ErrorMessagesDict]
@@ -69,6 +88,7 @@ class Field:
6988
def get_bound_field(self, form: BaseForm, field_name: str) -> BoundField: ...
7089

7190
class CharField(Field):
91+
widget: _WidgetTypeOrInstance[TextInput] # type: ignore[assignment]
7292
max_length: int | None
7393
min_length: int | None
7494
strip: bool
@@ -96,6 +116,7 @@ class CharField(Field):
96116
def widget_attrs(self, widget: Widget) -> dict[str, Any]: ...
97117

98118
class IntegerField(Field):
119+
widget: _WidgetTypeOrInstance[NumberInput] # type: ignore[assignment]
99120
max_value: int | None
100121
min_value: int | None
101122
step_size: int | None
@@ -193,17 +214,20 @@ class BaseTemporalField(Field):
193214
def strptime(self, value: str, format: str) -> Any: ...
194215

195216
class DateField(BaseTemporalField):
217+
widget: _WidgetTypeOrInstance[DateInput] # type: ignore[assignment]
196218
def to_python(self, value: None | str | datetime.datetime | datetime.date) -> datetime.date | None: ...
197219
def strptime(self, value: str, format: str) -> datetime.date: ...
198220

199221
class TimeField(BaseTemporalField):
222+
widget: _WidgetTypeOrInstance[TimeInput] # type: ignore[assignment]
200223
def to_python(self, value: None | str | datetime.time) -> datetime.time | None: ...
201224
def strptime(self, value: str, format: str) -> datetime.time: ...
202225

203226
class DateTimeFormatsIterator:
204227
def __iter__(self) -> Iterator[str]: ...
205228

206229
class DateTimeField(BaseTemporalField):
230+
widget: _WidgetTypeOrInstance[DateTimeInput] # type: ignore[assignment]
207231
def to_python(self, value: None | str | datetime.datetime | datetime.date) -> datetime.datetime | None: ...
208232
def strptime(self, value: str, format: str) -> datetime.datetime: ...
209233

@@ -235,6 +259,7 @@ class RegexField(CharField):
235259
) -> None: ...
236260

237261
class EmailField(CharField):
262+
widget: _WidgetTypeOrInstance[EmailInput] # type: ignore[assignment]
238263
def __init__(
239264
self,
240265
*,
@@ -256,6 +281,7 @@ class EmailField(CharField):
256281
) -> None: ...
257282

258283
class FileField(Field):
284+
widget: _WidgetTypeOrInstance[ClearableFileInput] # type: ignore[assignment]
259285
allow_empty_file: bool
260286
max_length: int | None
261287
def __init__(
@@ -285,6 +311,7 @@ class ImageField(FileField):
285311
def widget_attrs(self, widget: Widget) -> dict[str, Any]: ...
286312

287313
class URLField(CharField):
314+
widget: _WidgetTypeOrInstance[URLInput] # type: ignore[assignment]
288315
def __init__(
289316
self,
290317
*,
@@ -308,15 +335,18 @@ class URLField(CharField):
308335
def to_python(self, value: Any | None) -> str | None: ...
309336

310337
class BooleanField(Field):
338+
widget: _WidgetTypeOrInstance[CheckboxInput] # type: ignore[assignment]
311339
def to_python(self, value: Any | None) -> bool: ...
312340
def validate(self, value: Any) -> None: ...
313341
def has_changed(self, initial: Any | None, data: Any | None) -> bool: ...
314342

315343
class NullBooleanField(BooleanField):
344+
widget: _WidgetTypeOrInstance[NullBooleanSelect] # type: ignore[assignment]
316345
def to_python(self, value: Any | None) -> bool | None: ... # type: ignore[override]
317346
def validate(self, value: Any) -> None: ...
318347

319348
class ChoiceField(Field):
349+
widget: _WidgetTypeOrInstance[Select] # type: ignore[assignment]
320350
choices: _PropertyDescriptor[
321351
_ChoicesInput | _ChoicesCallable | CallableChoiceIterator,
322352
_ChoicesInput | CallableChoiceIterator,
@@ -371,6 +401,7 @@ class TypedChoiceField(ChoiceField):
371401
def clean(self, value: Any) -> Any: ...
372402

373403
class MultipleChoiceField(ChoiceField):
404+
widget: _WidgetTypeOrInstance[SelectMultiple] # type: ignore[assignment]
374405
def to_python(self, value: Any | None) -> list[str]: ...
375406
def validate(self, value: Any) -> None: ...
376407
def has_changed(self, initial: Collection[Any] | None, data: Collection[Any] | None) -> bool: ...
@@ -474,6 +505,7 @@ class FilePathField(ChoiceField):
474505
) -> None: ...
475506

476507
class SplitDateTimeField(MultiValueField):
508+
widget: _WidgetTypeOrInstance[SplitDateTimeWidget] # type: ignore[assignment]
477509
def __init__(
478510
self,
479511
*,
@@ -548,6 +580,7 @@ class JSONString(str): ...
548580

549581
class JSONField(CharField):
550582
default_error_messages: ClassVar[_ErrorMessagesDict]
583+
widget: _WidgetTypeOrInstance[Textarea] # type: ignore[assignment]
551584
encoder: Any
552585
decoder: Any
553586
def __init__(self, encoder: Any | None = None, decoder: Any | None = None, **kwargs: Any) -> None: ...

django-stubs/forms/models.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ from django.db.models.base import Model
88
from django.db.models.fields import _AllLimitChoicesTo, _LimitChoicesTo
99
from django.db.models.manager import Manager
1010
from django.db.models.query import QuerySet
11-
from django.forms.fields import ChoiceField, Field
11+
from django.forms.fields import ChoiceField, Field, _WidgetTypeOrInstance
1212
from django.forms.forms import BaseForm, DeclarativeFieldsMetaclass
1313
from django.forms.formsets import BaseFormSet
1414
from django.forms.renderers import BaseRenderer
1515
from django.forms.utils import ErrorList, _DataT, _FilesT
16-
from django.forms.widgets import Widget
16+
from django.forms.widgets import HiddenInput, Widget
1717
from django.utils.choices import BaseChoiceIterator, CallableChoiceIterator, _ChoicesCallable, _ChoicesInput
1818
from django.utils.datastructures import _PropertyDescriptor
1919
from django.utils.functional import _StrOrPromise
@@ -222,6 +222,7 @@ def inlineformset_factory(
222222
) -> type[BaseInlineFormSet[_M, _ParentM, _ModelFormT]]: ...
223223

224224
class InlineForeignKeyField(Field):
225+
widget: _WidgetTypeOrInstance[HiddenInput] # type: ignore[assignment]
225226
disabled: bool
226227
help_text: _StrOrPromise
227228
required: bool

scripts/stubtest/allowlist.txt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,66 @@ django.contrib.auth.models.PermissionsMixin.Meta
434434
django.contrib.flatpages.forms.FlatpageForm.Meta
435435
django.contrib.sessions.base_session.AbstractBaseSession.Meta
436436

437+
# Ignore `widget` for `Field` subclasses, see PR #2615 for the related discussion
438+
django.contrib.auth.forms.ReadOnlyPasswordHashField.widget
439+
django.contrib.gis.forms.BooleanField.widget
440+
django.contrib.gis.forms.CharField.widget
441+
django.contrib.gis.forms.ChoiceField.widget
442+
django.contrib.gis.forms.DateField.widget
443+
django.contrib.gis.forms.DateTimeField.widget
444+
django.contrib.gis.forms.EmailField.widget
445+
django.contrib.gis.forms.Field.widget
446+
django.contrib.gis.forms.fields.GeometryField.widget
447+
django.contrib.gis.forms.FileField.widget
448+
django.contrib.gis.forms.GeometryField.widget
449+
django.contrib.gis.forms.IntegerField.widget
450+
django.contrib.gis.forms.JSONField.widget
451+
django.contrib.gis.forms.ModelMultipleChoiceField.widget
452+
django.contrib.gis.forms.MultipleChoiceField.widget
453+
django.contrib.gis.forms.NullBooleanField.widget
454+
django.contrib.gis.forms.SplitDateTimeField.widget
455+
django.contrib.gis.forms.TimeField.widget
456+
django.contrib.gis.forms.URLField.widget
457+
django.contrib.postgres.forms.array.SplitArrayField.widget
458+
django.contrib.postgres.forms.BaseRangeField.widget
459+
django.contrib.postgres.forms.hstore.HStoreField.widget
460+
django.contrib.postgres.forms.HStoreField.widget
461+
django.contrib.postgres.forms.ranges.BaseRangeField.widget
462+
django.contrib.postgres.forms.SplitArrayField.widget
463+
django.forms.BooleanField.widget
464+
django.forms.CharField.widget
465+
django.forms.ChoiceField.widget
466+
django.forms.DateField.widget
467+
django.forms.DateTimeField.widget
468+
django.forms.EmailField.widget
469+
django.forms.Field.widget
470+
django.forms.fields.BooleanField.widget
471+
django.forms.fields.CharField.widget
472+
django.forms.fields.ChoiceField.widget
473+
django.forms.fields.DateField.widget
474+
django.forms.fields.DateTimeField.widget
475+
django.forms.fields.EmailField.widget
476+
django.forms.fields.Field.widget
477+
django.forms.fields.FileField.widget
478+
django.forms.fields.IntegerField.widget
479+
django.forms.fields.JSONField.widget
480+
django.forms.fields.MultipleChoiceField.widget
481+
django.forms.fields.NullBooleanField.widget
482+
django.forms.fields.SplitDateTimeField.widget
483+
django.forms.fields.TimeField.widget
484+
django.forms.fields.URLField.widget
485+
django.forms.FileField.widget
486+
django.forms.IntegerField.widget
487+
django.forms.JSONField.widget
488+
django.forms.ModelMultipleChoiceField.widget
489+
django.forms.models.InlineForeignKeyField.widget
490+
django.forms.models.ModelMultipleChoiceField.widget
491+
django.forms.MultipleChoiceField.widget
492+
django.forms.NullBooleanField.widget
493+
django.forms.SplitDateTimeField.widget
494+
django.forms.TimeField.widget
495+
django.forms.URLField.widget
496+
437497
# Custom __str__ that we don't want to overcomplicate:
438498
django.forms.utils.RenderableMixin.__str__
439499
django.forms.utils.RenderableMixin.__html__
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
from django.contrib.auth.forms import ReadOnlyPasswordHashField, UsernameField
2-
from django.forms.widgets import Widget
1+
from django.contrib.auth.forms import ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget, UsernameField
2+
from django.forms.widgets import TextInput
33
from typing_extensions import assert_type
44

5-
assert_type(ReadOnlyPasswordHashField.widget, type[Widget] | Widget)
6-
assert_type(ReadOnlyPasswordHashField().widget, Widget)
5+
assert_type(
6+
ReadOnlyPasswordHashField.widget,
7+
type[ReadOnlyPasswordHashWidget] | ReadOnlyPasswordHashWidget,
8+
)
9+
assert_type(ReadOnlyPasswordHashField().widget, ReadOnlyPasswordHashWidget)
710

8-
assert_type(UsernameField.widget, type[Widget] | Widget)
9-
assert_type(UsernameField().widget, Widget)
11+
assert_type(UsernameField.widget, type[TextInput] | TextInput)
12+
assert_type(UsernameField().widget, TextInput)

tests/assert_type/contrib/gis/forms/test_fields.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,29 @@
88
PointField,
99
PolygonField,
1010
)
11-
from django.forms.widgets import Widget
11+
from django.contrib.gis.forms.widgets import OpenLayersWidget
1212
from typing_extensions import assert_type
1313

14-
assert_type(GeometryField.widget, type[Widget] | Widget)
15-
assert_type(GeometryField().widget, Widget)
14+
assert_type(GeometryField.widget, type[OpenLayersWidget] | OpenLayersWidget)
15+
assert_type(GeometryField().widget, OpenLayersWidget)
1616

17-
assert_type(GeometryCollectionField.widget, type[Widget] | Widget)
18-
assert_type(GeometryCollectionField().widget, Widget)
17+
assert_type(GeometryCollectionField.widget, type[OpenLayersWidget] | OpenLayersWidget)
18+
assert_type(GeometryCollectionField().widget, OpenLayersWidget)
1919

20-
assert_type(PointField.widget, type[Widget] | Widget)
21-
assert_type(PointField().widget, Widget)
20+
assert_type(PointField.widget, type[OpenLayersWidget] | OpenLayersWidget)
21+
assert_type(PointField().widget, OpenLayersWidget)
2222

23-
assert_type(MultiPointField.widget, type[Widget] | Widget)
24-
assert_type(MultiPointField().widget, Widget)
23+
assert_type(MultiPointField.widget, type[OpenLayersWidget] | OpenLayersWidget)
24+
assert_type(MultiPointField().widget, OpenLayersWidget)
2525

26-
assert_type(LineStringField.widget, type[Widget] | Widget)
27-
assert_type(LineStringField().widget, Widget)
26+
assert_type(LineStringField.widget, type[OpenLayersWidget] | OpenLayersWidget)
27+
assert_type(LineStringField().widget, OpenLayersWidget)
2828

29-
assert_type(MultiLineStringField.widget, type[Widget] | Widget)
30-
assert_type(MultiLineStringField().widget, Widget)
29+
assert_type(MultiLineStringField.widget, type[OpenLayersWidget] | OpenLayersWidget)
30+
assert_type(MultiLineStringField().widget, OpenLayersWidget)
3131

32-
assert_type(PolygonField.widget, type[Widget] | Widget)
33-
assert_type(PolygonField().widget, Widget)
32+
assert_type(PolygonField.widget, type[OpenLayersWidget] | OpenLayersWidget)
33+
assert_type(PolygonField().widget, OpenLayersWidget)
3434

35-
assert_type(MultiPolygonField.widget, type[Widget] | Widget)
36-
assert_type(MultiPolygonField().widget, Widget)
35+
assert_type(MultiPolygonField.widget, type[OpenLayersWidget] | OpenLayersWidget)
36+
assert_type(MultiPolygonField().widget, OpenLayersWidget)

0 commit comments

Comments
 (0)