Skip to content

Replace TypeAlias _ClassLevelWidgetT with descriptor _WidgetTypeOrInstance #2615

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions django-stubs/contrib/auth/forms.pyi
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.fields import _ErrorMessagesDict
from django.forms.fields import _ClassLevelWidgetT
from django.forms.fields import _WidgetTypeOrInstance
from django.forms.widgets import Widget
from django.http.request import HttpRequest
from django.utils.functional import _StrOrPromise
@@ -22,7 +22,7 @@ class ReadOnlyPasswordHashWidget(forms.Widget):
def get_context(self, name: str, value: Any, attrs: dict[str, Any] | None) -> dict[str, Any]: ...

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

class UsernameField(forms.CharField):
4 changes: 3 additions & 1 deletion django-stubs/contrib/gis/forms/fields.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Any

from django import forms
from django.contrib.gis.forms.widgets import OpenLayersWidget
from django.forms.fields import _WidgetTypeOrInstance

class GeometryField(forms.Field):
widget: Any
widget: _WidgetTypeOrInstance[OpenLayersWidget] # type: ignore[assignment]
geom_type: str
srid: Any
def __init__(self, *, srid: Any | None = ..., geom_type: Any | None = ..., **kwargs: Any) -> None: ...
4 changes: 2 additions & 2 deletions django-stubs/contrib/postgres/forms/array.pyi
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ from typing import Any, ClassVar

from django import forms
from django.db.models.fields import _ErrorMessagesDict
from django.forms.fields import _ClassLevelWidgetT
from django.forms.fields import _WidgetTypeOrInstance
from django.forms.utils import _DataT, _FilesT
from django.forms.widgets import _OptAttrs

@@ -33,7 +33,6 @@ class SimpleArrayField(forms.CharField):

class SplitArrayWidget(forms.Widget):
template_name: str
widget: _ClassLevelWidgetT
size: int
def __init__(self, widget: forms.Widget | type[forms.Widget], size: int, **kwargs: Any) -> None: ...
@property
@@ -46,6 +45,7 @@ class SplitArrayWidget(forms.Widget):
def needs_multipart_form(self) -> bool: ... # type: ignore[override]

class SplitArrayField(forms.Field):
widget: _WidgetTypeOrInstance[forms.TextInput, SplitArrayWidget] # type: ignore[assignment]
default_error_messages: ClassVar[_ErrorMessagesDict]
base_field: forms.Field
size: int
2 changes: 0 additions & 2 deletions django-stubs/contrib/postgres/forms/hstore.pyi
Original file line number Diff line number Diff line change
@@ -2,10 +2,8 @@ from typing import Any, ClassVar

from django import forms
from django.db.models.fields import _ErrorMessagesDict
from django.forms.fields import _ClassLevelWidgetT

class HStoreField(forms.CharField):
widget: _ClassLevelWidgetT
default_error_messages: ClassVar[_ErrorMessagesDict]
def prepare_value(self, value: Any) -> Any: ...
def to_python(self, value: Any) -> dict[str, str | None]: ... # type: ignore[override]
4 changes: 3 additions & 1 deletion django-stubs/contrib/postgres/forms/ranges.pyi
Original file line number Diff line number Diff line change
@@ -2,7 +2,8 @@ from typing import Any, ClassVar

from django import forms
from django.db.models.fields import _ErrorMessagesDict
from django.forms.widgets import MultiWidget, _OptAttrs
from django.forms.fields import _WidgetTypeOrInstance
from django.forms.widgets import MultiWidget, TextInput, _OptAttrs
from psycopg2.extras import Range # type: ignore [import-untyped]

class RangeWidget(MultiWidget):
@@ -17,6 +18,7 @@ class BaseRangeField(forms.MultiValueField):
base_field: type[forms.Field]
range_type: type[Range]
hidden_widget: type[forms.Widget]
widget: _WidgetTypeOrInstance[TextInput, RangeWidget] # type: ignore[assignment]
def __init__(self, **kwargs: Any) -> None: ...
def prepare_value(self, value: Any) -> Any: ...
def compress(self, values: tuple[Any | None, Any | None]) -> Range | None: ...
58 changes: 45 additions & 13 deletions django-stubs/forms/fields.pyi
Original file line number Diff line number Diff line change
@@ -2,33 +2,54 @@ import datetime
from collections.abc import Collection, Iterator, Sequence
from decimal import Decimal
from re import Pattern
from typing import Any, ClassVar, Protocol, TypeAlias, type_check_only
from typing import Any, ClassVar, Generic, Protocol, TypeVar, overload, type_check_only
from uuid import UUID

from django.core.files import File
from django.core.validators import _ValidatorCallable
from django.db.models.fields import _ErrorMessagesDict, _ErrorMessagesMapping
from django.forms.boundfield import BoundField
from django.forms.forms import BaseForm
from django.forms.widgets import Widget
from django.forms.widgets import (
CheckboxInput,
ClearableFileInput,
DateInput,
DateTimeInput,
EmailInput,
NullBooleanSelect,
NumberInput,
Select,
SelectMultiple,
SplitDateTimeWidget,
Textarea,
TextInput,
TimeInput,
URLInput,
Widget,
)
from django.utils.choices import CallableChoiceIterator, _ChoicesCallable, _ChoicesInput
from django.utils.datastructures import _PropertyDescriptor
from django.utils.functional import _StrOrPromise

# Problem: attribute `widget` is always of type `Widget` after field instantiation.
# However, on class level it can be set to `Type[Widget]` too.
# If we annotate it as `Union[Widget, Type[Widget]]`, every code that uses field
# instances will not typecheck.
# If we annotate it as `Widget`, any widget subclasses that do e.g.
# `widget = Select` will not typecheck.
# `Any` gives too much freedom, but does not create false positives.
_ClassLevelWidgetT: TypeAlias = Any
_ClassWidget = TypeVar("_ClassWidget", bound=Widget)
_InstanceWidget = TypeVar("_InstanceWidget", bound=Widget, default=_ClassWidget)

@type_check_only
class _WidgetTypeOrInstance(Generic[_ClassWidget, _InstanceWidget]):
@overload
def __get__(self, instance: None, owner: type[Field]) -> type[_ClassWidget] | _ClassWidget: ...
@overload
def __get__(self, instance: Field, owner: type[Field]) -> _InstanceWidget: ...
@overload
def __set__(self, instance: None, value: type[_ClassWidget] | _ClassWidget) -> None: ...
@overload
def __set__(self, instance: Field, value: _InstanceWidget) -> None: ...

class Field:
initial: Any
label: _StrOrPromise | None
required: bool
widget: _ClassLevelWidgetT
widget: _WidgetTypeOrInstance[TextInput]
hidden_widget: type[Widget]
default_validators: list[_ValidatorCallable]
default_error_messages: ClassVar[_ErrorMessagesDict]
@@ -96,6 +117,7 @@ class CharField(Field):
def widget_attrs(self, widget: Widget) -> dict[str, Any]: ...

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

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

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

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

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

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

class EmailField(CharField):
widget: _WidgetTypeOrInstance[EmailInput] # type: ignore[assignment]
def __init__(
self,
*,
@@ -256,6 +282,7 @@ class EmailField(CharField):
) -> None: ...

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

class URLField(CharField):
widget: _WidgetTypeOrInstance[URLInput] # type: ignore[assignment]
def __init__(
self,
*,
@@ -308,20 +336,22 @@ class URLField(CharField):
def to_python(self, value: Any | None) -> str | None: ...

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

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

class ChoiceField(Field):
widget: _WidgetTypeOrInstance[Select] # type: ignore[assignment]
choices: _PropertyDescriptor[
_ChoicesInput | _ChoicesCallable | CallableChoiceIterator,
_ChoicesInput | CallableChoiceIterator,
]
widget: _ClassLevelWidgetT
def __init__(
self,
*,
@@ -372,6 +402,7 @@ class TypedChoiceField(ChoiceField):
def clean(self, value: Any) -> Any: ...

class MultipleChoiceField(ChoiceField):
widget: _WidgetTypeOrInstance[SelectMultiple] # type: ignore[assignment]
def to_python(self, value: Any | None) -> list[str]: ...
def validate(self, value: Any) -> None: ...
def has_changed(self, initial: Collection[Any] | None, data: Collection[Any] | None) -> bool: ...
@@ -475,6 +506,7 @@ class FilePathField(ChoiceField):
) -> None: ...

class SplitDateTimeField(MultiValueField):
widget: _WidgetTypeOrInstance[SplitDateTimeWidget] # type: ignore[assignment]
def __init__(
self,
*,
@@ -549,7 +581,7 @@ class JSONString(str): ...

class JSONField(CharField):
default_error_messages: ClassVar[_ErrorMessagesDict]
widget: _ClassLevelWidgetT
widget: _WidgetTypeOrInstance[Textarea] # type: ignore[assignment]
encoder: Any
decoder: Any
def __init__(self, encoder: Any | None = None, decoder: Any | None = None, **kwargs: Any) -> None: ...
7 changes: 3 additions & 4 deletions django-stubs/forms/models.pyi
Original file line number Diff line number Diff line change
@@ -8,12 +8,12 @@ from django.db.models.base import Model
from django.db.models.fields import _AllLimitChoicesTo, _LimitChoicesTo
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
from django.forms.fields import ChoiceField, Field, _ClassLevelWidgetT
from django.forms.fields import ChoiceField, Field, _WidgetTypeOrInstance
from django.forms.forms import BaseForm, DeclarativeFieldsMetaclass
from django.forms.formsets import BaseFormSet
from django.forms.renderers import BaseRenderer
from django.forms.utils import ErrorList, _DataT, _FilesT
from django.forms.widgets import Widget
from django.forms.widgets import HiddenInput, Widget
from django.utils.choices import BaseChoiceIterator, CallableChoiceIterator, _ChoicesCallable, _ChoicesInput
from django.utils.datastructures import _PropertyDescriptor
from django.utils.functional import _StrOrPromise
@@ -222,11 +222,11 @@ def inlineformset_factory(
) -> type[BaseInlineFormSet[_M, _ParentM, _ModelFormT]]: ...

class InlineForeignKeyField(Field):
widget: _WidgetTypeOrInstance[HiddenInput] # type: ignore[assignment]
disabled: bool
help_text: _StrOrPromise
required: bool
show_hidden_initial: bool
widget: _ClassLevelWidgetT
parent_instance: Model
pk_field: bool
to_field: str | None
@@ -297,7 +297,6 @@ class ModelMultipleChoiceField(ModelChoiceField[_M]):
help_text: _StrOrPromise
required: bool
show_hidden_initial: bool
widget: _ClassLevelWidgetT
hidden_widget: type[Widget]
def __init__(self, queryset: Manager[_M] | QuerySet[_M] | None, **kwargs: Any) -> None: ...
def to_python(self, value: Any) -> list[_M]: ... # type: ignore[override]
57 changes: 57 additions & 0 deletions scripts/stubtest/allowlist.txt
Original file line number Diff line number Diff line change
@@ -455,6 +455,63 @@ django.contrib.auth.models.PermissionsMixin.Meta
django.contrib.flatpages.forms.FlatpageForm.Meta
django.contrib.sessions.base_session.AbstractBaseSession.Meta

# Ignore `widget` for `Field` subclasses, see PR #2615 for the related discussion
django.contrib.auth.forms.ReadOnlyPasswordHashField.widget
django.contrib.gis.forms.BooleanField.widget
django.contrib.gis.forms.ChoiceField.widget
django.contrib.gis.forms.DateField.widget
django.contrib.gis.forms.DateTimeField.widget
django.contrib.gis.forms.EmailField.widget
django.contrib.gis.forms.Field.widget
django.contrib.gis.forms.fields.GeometryField.widget
django.contrib.gis.forms.FileField.widget
django.contrib.gis.forms.GeometryField.widget
django.contrib.gis.forms.IntegerField.widget
django.contrib.gis.forms.JSONField.widget
django.contrib.gis.forms.ModelMultipleChoiceField.widget
django.contrib.gis.forms.MultipleChoiceField.widget
django.contrib.gis.forms.NullBooleanField.widget
django.contrib.gis.forms.SplitDateTimeField.widget
django.contrib.gis.forms.TimeField.widget
django.contrib.gis.forms.URLField.widget
django.contrib.postgres.forms.array.SplitArrayField.widget
django.contrib.postgres.forms.BaseRangeField.widget
django.contrib.postgres.forms.hstore.HStoreField.widget
django.contrib.postgres.forms.HStoreField.widget
django.contrib.postgres.forms.ranges.BaseRangeField.widget
django.contrib.postgres.forms.SplitArrayField.widget
django.forms.BooleanField.widget
django.forms.ChoiceField.widget
django.forms.DateField.widget
django.forms.DateTimeField.widget
django.forms.EmailField.widget
django.forms.Field.widget
django.forms.fields.BooleanField.widget
django.forms.fields.ChoiceField.widget
django.forms.fields.DateField.widget
django.forms.fields.DateTimeField.widget
django.forms.fields.EmailField.widget
django.forms.fields.Field.widget
django.forms.fields.FileField.widget
django.forms.fields.IntegerField.widget
django.forms.fields.JSONField.widget
django.forms.fields.MultipleChoiceField.widget
django.forms.fields.NullBooleanField.widget
django.forms.fields.SplitDateTimeField.widget
django.forms.fields.TimeField.widget
django.forms.fields.URLField.widget
django.forms.FileField.widget
django.forms.IntegerField.widget
django.forms.JSONField.widget
django.forms.ModelMultipleChoiceField.widget
django.forms.models.InlineForeignKeyField.widget
django.forms.models.ModelMultipleChoiceField.widget
django.forms.MultipleChoiceField.widget
django.forms.NullBooleanField.widget
django.forms.SplitDateTimeField.widget
django.forms.TimeField.widget
django.forms.URLField.widget

# Custom __str__ that we don't want to overcomplicate:
django.forms.utils.RenderableMixin.__str__
django.forms.utils.RenderableMixin.__html__
12 changes: 12 additions & 0 deletions tests/assert_type/contrib/auth/test_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.contrib.auth.forms import ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget, UsernameField
from django.forms.widgets import TextInput
from typing_extensions import assert_type

assert_type(
ReadOnlyPasswordHashField.widget,
type[ReadOnlyPasswordHashWidget] | ReadOnlyPasswordHashWidget,
)
assert_type(ReadOnlyPasswordHashField().widget, ReadOnlyPasswordHashWidget)

assert_type(UsernameField.widget, type[TextInput] | TextInput)
assert_type(UsernameField().widget, TextInput)
Empty file.
36 changes: 36 additions & 0 deletions tests/assert_type/contrib/gis/forms/test_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from django.contrib.gis.forms import (
GeometryCollectionField,
GeometryField,
LineStringField,
MultiLineStringField,
MultiPointField,
MultiPolygonField,
PointField,
PolygonField,
)
from django.contrib.gis.forms.widgets import OpenLayersWidget
from typing_extensions import assert_type

assert_type(GeometryField.widget, type[OpenLayersWidget] | OpenLayersWidget)
assert_type(GeometryField().widget, OpenLayersWidget)

assert_type(GeometryCollectionField.widget, type[OpenLayersWidget] | OpenLayersWidget)
assert_type(GeometryCollectionField().widget, OpenLayersWidget)

assert_type(PointField.widget, type[OpenLayersWidget] | OpenLayersWidget)
assert_type(PointField().widget, OpenLayersWidget)

assert_type(MultiPointField.widget, type[OpenLayersWidget] | OpenLayersWidget)
assert_type(MultiPointField().widget, OpenLayersWidget)

assert_type(LineStringField.widget, type[OpenLayersWidget] | OpenLayersWidget)
assert_type(LineStringField().widget, OpenLayersWidget)

assert_type(MultiLineStringField.widget, type[OpenLayersWidget] | OpenLayersWidget)
assert_type(MultiLineStringField().widget, OpenLayersWidget)

assert_type(PolygonField.widget, type[OpenLayersWidget] | OpenLayersWidget)
assert_type(PolygonField().widget, OpenLayersWidget)

assert_type(MultiPolygonField.widget, type[OpenLayersWidget] | OpenLayersWidget)
assert_type(MultiPolygonField().widget, OpenLayersWidget)
15 changes: 15 additions & 0 deletions tests/assert_type/contrib/postgres/forms/test_array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import cast

from django.contrib.postgres.forms.array import SimpleArrayField, SplitArrayField, SplitArrayWidget
from django.forms.fields import Field
from django.forms.widgets import TextInput
from typing_extensions import assert_type

base_field = cast(Field, ...)
size = cast(int, ...)

assert_type(SimpleArrayField.widget, type[TextInput] | TextInput)
assert_type(SimpleArrayField(base_field).widget, TextInput)

assert_type(SplitArrayField.widget, type[TextInput] | TextInput)
assert_type(SplitArrayField(base_field, size).widget, SplitArrayWidget)
6 changes: 6 additions & 0 deletions tests/assert_type/contrib/postgres/forms/test_hstore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.contrib.postgres.forms.hstore import HStoreField
from django.forms.widgets import TextInput
from typing_extensions import assert_type

assert_type(HStoreField.widget, type[TextInput] | TextInput)
assert_type(HStoreField().widget, TextInput)
21 changes: 21 additions & 0 deletions tests/assert_type/contrib/postgres/forms/test_ranges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.contrib.postgres.forms.ranges import (
DateRangeField,
DateTimeRangeField,
DecimalRangeField,
IntegerRangeField,
RangeWidget,
)
from django.forms.widgets import TextInput
from typing_extensions import assert_type

assert_type(IntegerRangeField.widget, type[TextInput] | TextInput)
assert_type(IntegerRangeField().widget, RangeWidget)

assert_type(DecimalRangeField.widget, type[TextInput] | TextInput)
assert_type(DecimalRangeField().widget, RangeWidget)

assert_type(DateTimeRangeField.widget, type[TextInput] | TextInput)
assert_type(DateTimeRangeField().widget, RangeWidget)

assert_type(DateRangeField.widget, type[TextInput] | TextInput)
assert_type(DateRangeField().widget, RangeWidget)
146 changes: 146 additions & 0 deletions tests/assert_type/forms/test_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
from typing import cast

from django.forms.fields import (
BooleanField,
CharField,
ChoiceField,
ComboField,
DateField,
DateTimeField,
DecimalField,
DurationField,
EmailField,
Field,
FileField,
FilePathField,
FloatField,
GenericIPAddressField,
ImageField,
IntegerField,
JSONField,
MultipleChoiceField,
MultiValueField,
NullBooleanField,
RegexField,
SlugField,
SplitDateTimeField,
TimeField,
TypedChoiceField,
TypedMultipleChoiceField,
URLField,
UUIDField,
)
from django.forms.widgets import (
CheckboxInput,
ClearableFileInput,
DateInput,
DateTimeInput,
EmailInput,
NullBooleanSelect,
NumberInput,
Select,
SelectMultiple,
SplitDateTimeWidget,
Textarea,
TextInput,
TimeInput,
URLInput,
)
from typing_extensions import assert_type

assert_type(Field.widget, type[TextInput] | TextInput)
assert_type(Field().widget, TextInput)

assert_type(CharField.widget, type[TextInput] | TextInput)
assert_type(CharField().widget, TextInput)

assert_type(IntegerField.widget, type[NumberInput] | NumberInput)
assert_type(IntegerField().widget, NumberInput)

assert_type(FloatField.widget, type[NumberInput] | NumberInput)
assert_type(FloatField().widget, NumberInput)

assert_type(DecimalField.widget, type[NumberInput] | NumberInput)
assert_type(DecimalField().widget, NumberInput)

assert_type(DateField.widget, type[DateInput] | DateInput)
assert_type(DateField().widget, DateInput)

assert_type(TimeField.widget, type[TimeInput] | TimeInput)
assert_type(TimeField().widget, TimeInput)

assert_type(DateTimeField.widget, type[DateTimeInput] | DateTimeInput)
assert_type(DateTimeField().widget, DateTimeInput)

assert_type(DurationField.widget, type[TextInput] | TextInput)
assert_type(DurationField().widget, TextInput)

regex = cast(str, ...)

assert_type(RegexField.widget, type[TextInput] | TextInput)
assert_type(RegexField(regex).widget, TextInput)

assert_type(EmailField.widget, type[EmailInput] | EmailInput)
assert_type(EmailField().widget, EmailInput)

assert_type(FileField.widget, type[ClearableFileInput] | ClearableFileInput)
assert_type(FileField().widget, ClearableFileInput)

assert_type(ImageField.widget, type[ClearableFileInput] | ClearableFileInput)
assert_type(ImageField().widget, ClearableFileInput)

assert_type(URLField.widget, type[URLInput] | URLInput)
assert_type(URLField().widget, URLInput)

assert_type(BooleanField.widget, type[CheckboxInput] | CheckboxInput)
assert_type(BooleanField().widget, CheckboxInput)

assert_type(NullBooleanField.widget, type[NullBooleanSelect] | NullBooleanSelect)
assert_type(NullBooleanField().widget, NullBooleanSelect)

assert_type(ChoiceField.widget, type[Select] | Select)
assert_type(ChoiceField().widget, Select)

assert_type(TypedChoiceField.widget, type[Select] | Select)
assert_type(TypedChoiceField().widget, Select)

assert_type(MultipleChoiceField.widget, type[SelectMultiple] | SelectMultiple)
assert_type(MultipleChoiceField().widget, SelectMultiple)

assert_type(TypedMultipleChoiceField.widget, type[SelectMultiple] | SelectMultiple)
assert_type(TypedMultipleChoiceField().widget, SelectMultiple)

fields = cast(list[Field], ...)

assert_type(ComboField.widget, type[TextInput] | TextInput)
assert_type(ComboField(fields).widget, TextInput)

assert_type(MultiValueField.widget, type[TextInput] | TextInput)
assert_type(MultiValueField(fields).widget, TextInput)

path = cast(str, ...)

assert_type(FilePathField.widget, type[Select] | Select)
assert_type(FilePathField(path).widget, Select)

assert_type(SplitDateTimeField.widget, type[SplitDateTimeWidget] | SplitDateTimeWidget)
assert_type(SplitDateTimeField().widget, SplitDateTimeWidget)

assert_type(GenericIPAddressField.widget, type[TextInput] | TextInput)
assert_type(GenericIPAddressField().widget, TextInput)

assert_type(SlugField.widget, type[TextInput] | TextInput)
assert_type(SlugField().widget, TextInput)

assert_type(UUIDField.widget, type[TextInput] | TextInput)
assert_type(UUIDField().widget, TextInput)

assert_type(JSONField.widget, type[Textarea] | Textarea)
assert_type(JSONField().widget, Textarea)


class CustomIntegerField(IntegerField): ...


assert_type(CustomIntegerField.widget, type[NumberInput] | NumberInput)
assert_type(CustomIntegerField().widget, NumberInput)
23 changes: 23 additions & 0 deletions tests/assert_type/forms/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import cast

from django.db.models import Model, QuerySet
from django.forms.models import InlineForeignKeyField, ModelChoiceField, ModelMultipleChoiceField
from django.forms.widgets import HiddenInput, Select
from typing_extensions import assert_type


class TestModel(Model): ...


testmodel_instance = cast(TestModel, ...)

assert_type(InlineForeignKeyField.widget, type[HiddenInput] | HiddenInput)
assert_type(InlineForeignKeyField(testmodel_instance).widget, HiddenInput)

testmodel_queryset = cast(QuerySet[TestModel], ...)

assert_type(ModelChoiceField.widget, type[Select] | Select)
assert_type(ModelChoiceField(testmodel_queryset).widget, Select)

assert_type(ModelMultipleChoiceField.widget, type[Select] | Select)
assert_type(ModelMultipleChoiceField(testmodel_queryset).widget, Select)