Skip to content

Commit cdd1d73

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

File tree

14 files changed

+242
-114
lines changed

14 files changed

+242
-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: 40 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]
@@ -96,6 +115,7 @@ class CharField(Field):
96115
def widget_attrs(self, widget: Widget) -> dict[str, Any]: ...
97116

98117
class IntegerField(Field):
118+
widget: _WidgetTypeOrInstance[NumberInput] # type: ignore[assignment]
99119
max_value: int | None
100120
min_value: int | None
101121
step_size: int | None
@@ -193,17 +213,20 @@ class BaseTemporalField(Field):
193213
def strptime(self, value: str, format: str) -> Any: ...
194214

195215
class DateField(BaseTemporalField):
216+
widget: _WidgetTypeOrInstance[DateInput] # type: ignore[assignment]
196217
def to_python(self, value: None | str | datetime.datetime | datetime.date) -> datetime.date | None: ...
197218
def strptime(self, value: str, format: str) -> datetime.date: ...
198219

199220
class TimeField(BaseTemporalField):
221+
widget: _WidgetTypeOrInstance[TimeInput] # type: ignore[assignment]
200222
def to_python(self, value: None | str | datetime.time) -> datetime.time | None: ...
201223
def strptime(self, value: str, format: str) -> datetime.time: ...
202224

203225
class DateTimeFormatsIterator:
204226
def __iter__(self) -> Iterator[str]: ...
205227

206228
class DateTimeField(BaseTemporalField):
229+
widget: _WidgetTypeOrInstance[DateTimeInput] # type: ignore[assignment]
207230
def to_python(self, value: None | str | datetime.datetime | datetime.date) -> datetime.datetime | None: ...
208231
def strptime(self, value: str, format: str) -> datetime.datetime: ...
209232

@@ -235,6 +258,7 @@ class RegexField(CharField):
235258
) -> None: ...
236259

237260
class EmailField(CharField):
261+
widget: _WidgetTypeOrInstance[EmailInput] # type: ignore[assignment]
238262
def __init__(
239263
self,
240264
*,
@@ -256,6 +280,7 @@ class EmailField(CharField):
256280
) -> None: ...
257281

258282
class FileField(Field):
283+
widget: _WidgetTypeOrInstance[ClearableFileInput] # type: ignore[assignment]
259284
allow_empty_file: bool
260285
max_length: int | None
261286
def __init__(
@@ -285,6 +310,7 @@ class ImageField(FileField):
285310
def widget_attrs(self, widget: Widget) -> dict[str, Any]: ...
286311

287312
class URLField(CharField):
313+
widget: _WidgetTypeOrInstance[URLInput] # type: ignore[assignment]
288314
def __init__(
289315
self,
290316
*,
@@ -308,15 +334,18 @@ class URLField(CharField):
308334
def to_python(self, value: Any | None) -> str | None: ...
309335

310336
class BooleanField(Field):
337+
widget: _WidgetTypeOrInstance[CheckboxInput] # type: ignore[assignment]
311338
def to_python(self, value: Any | None) -> bool: ...
312339
def validate(self, value: Any) -> None: ...
313340
def has_changed(self, initial: Any | None, data: Any | None) -> bool: ...
314341

315342
class NullBooleanField(BooleanField):
343+
widget: _WidgetTypeOrInstance[NullBooleanSelect] # type: ignore[assignment]
316344
def to_python(self, value: Any | None) -> bool | None: ... # type: ignore[override]
317345
def validate(self, value: Any) -> None: ...
318346

319347
class ChoiceField(Field):
348+
widget: _WidgetTypeOrInstance[Select] # type: ignore[assignment]
320349
choices: _PropertyDescriptor[
321350
_ChoicesInput | _ChoicesCallable | CallableChoiceIterator,
322351
_ChoicesInput | CallableChoiceIterator,
@@ -371,6 +400,7 @@ class TypedChoiceField(ChoiceField):
371400
def clean(self, value: Any) -> Any: ...
372401

373402
class MultipleChoiceField(ChoiceField):
403+
widget: _WidgetTypeOrInstance[SelectMultiple] # type: ignore[assignment]
374404
def to_python(self, value: Any | None) -> list[str]: ...
375405
def validate(self, value: Any) -> None: ...
376406
def has_changed(self, initial: Collection[Any] | None, data: Collection[Any] | None) -> bool: ...
@@ -474,6 +504,7 @@ class FilePathField(ChoiceField):
474504
) -> None: ...
475505

476506
class SplitDateTimeField(MultiValueField):
507+
widget: _WidgetTypeOrInstance[SplitDateTimeWidget] # type: ignore[assignment]
477508
def __init__(
478509
self,
479510
*,
@@ -548,6 +579,7 @@ class JSONString(str): ...
548579

549580
class JSONField(CharField):
550581
default_error_messages: ClassVar[_ErrorMessagesDict]
582+
widget: _WidgetTypeOrInstance[Textarea] # type: ignore[assignment]
551583
encoder: Any
552584
decoder: Any
553585
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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,63 @@ 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.ChoiceField.widget
441+
django.contrib.gis.forms.DateField.widget
442+
django.contrib.gis.forms.DateTimeField.widget
443+
django.contrib.gis.forms.EmailField.widget
444+
django.contrib.gis.forms.Field.widget
445+
django.contrib.gis.forms.fields.GeometryField.widget
446+
django.contrib.gis.forms.FileField.widget
447+
django.contrib.gis.forms.GeometryField.widget
448+
django.contrib.gis.forms.IntegerField.widget
449+
django.contrib.gis.forms.JSONField.widget
450+
django.contrib.gis.forms.ModelMultipleChoiceField.widget
451+
django.contrib.gis.forms.MultipleChoiceField.widget
452+
django.contrib.gis.forms.NullBooleanField.widget
453+
django.contrib.gis.forms.SplitDateTimeField.widget
454+
django.contrib.gis.forms.TimeField.widget
455+
django.contrib.gis.forms.URLField.widget
456+
django.contrib.postgres.forms.array.SplitArrayField.widget
457+
django.contrib.postgres.forms.BaseRangeField.widget
458+
django.contrib.postgres.forms.hstore.HStoreField.widget
459+
django.contrib.postgres.forms.HStoreField.widget
460+
django.contrib.postgres.forms.ranges.BaseRangeField.widget
461+
django.contrib.postgres.forms.SplitArrayField.widget
462+
django.forms.BooleanField.widget
463+
django.forms.ChoiceField.widget
464+
django.forms.DateField.widget
465+
django.forms.DateTimeField.widget
466+
django.forms.EmailField.widget
467+
django.forms.Field.widget
468+
django.forms.fields.BooleanField.widget
469+
django.forms.fields.ChoiceField.widget
470+
django.forms.fields.DateField.widget
471+
django.forms.fields.DateTimeField.widget
472+
django.forms.fields.EmailField.widget
473+
django.forms.fields.Field.widget
474+
django.forms.fields.FileField.widget
475+
django.forms.fields.IntegerField.widget
476+
django.forms.fields.JSONField.widget
477+
django.forms.fields.MultipleChoiceField.widget
478+
django.forms.fields.NullBooleanField.widget
479+
django.forms.fields.SplitDateTimeField.widget
480+
django.forms.fields.TimeField.widget
481+
django.forms.fields.URLField.widget
482+
django.forms.FileField.widget
483+
django.forms.IntegerField.widget
484+
django.forms.JSONField.widget
485+
django.forms.ModelMultipleChoiceField.widget
486+
django.forms.models.InlineForeignKeyField.widget
487+
django.forms.models.ModelMultipleChoiceField.widget
488+
django.forms.MultipleChoiceField.widget
489+
django.forms.NullBooleanField.widget
490+
django.forms.SplitDateTimeField.widget
491+
django.forms.TimeField.widget
492+
django.forms.URLField.widget
493+
437494
# Custom __str__ that we don't want to overcomplicate:
438495
django.forms.utils.RenderableMixin.__str__
439496
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)
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from typing import cast
22

3-
from django.contrib.postgres.forms.array import SimpleArrayField, SplitArrayField
3+
from django.contrib.postgres.forms.array import SimpleArrayField, SplitArrayField, SplitArrayWidget
44
from django.forms.fields import Field
5-
from django.forms.widgets import Widget
5+
from django.forms.widgets import TextInput
66
from typing_extensions import assert_type
77

88
base_field = cast(Field, ...)
99
size = cast(int, ...)
1010

11-
assert_type(SimpleArrayField.widget, type[Widget] | Widget)
12-
assert_type(SimpleArrayField(base_field).widget, Widget)
11+
assert_type(SimpleArrayField.widget, type[TextInput] | TextInput)
12+
assert_type(SimpleArrayField(base_field).widget, TextInput)
1313

14-
assert_type(SplitArrayField.widget, type[Widget] | Widget)
15-
assert_type(SplitArrayField(base_field, size).widget, Widget)
14+
assert_type(SplitArrayField.widget, type[TextInput] | TextInput)
15+
assert_type(SplitArrayField(base_field, size).widget, SplitArrayWidget)

0 commit comments

Comments
 (0)