diff --git a/django-stubs/contrib/auth/forms.pyi b/django-stubs/contrib/auth/forms.pyi index 2054b54001..b37c357e0d 100644 --- a/django-stubs/contrib/auth/forms.pyi +++ b/django-stubs/contrib/auth/forms.pyi @@ -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): diff --git a/django-stubs/contrib/gis/forms/fields.pyi b/django-stubs/contrib/gis/forms/fields.pyi index ac9c7f011d..e1c9b930d4 100644 --- a/django-stubs/contrib/gis/forms/fields.pyi +++ b/django-stubs/contrib/gis/forms/fields.pyi @@ -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: ... diff --git a/django-stubs/contrib/postgres/forms/array.pyi b/django-stubs/contrib/postgres/forms/array.pyi index 0c4ff4a727..1ae969f2ae 100644 --- a/django-stubs/contrib/postgres/forms/array.pyi +++ b/django-stubs/contrib/postgres/forms/array.pyi @@ -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 diff --git a/django-stubs/contrib/postgres/forms/hstore.pyi b/django-stubs/contrib/postgres/forms/hstore.pyi index 1f944fa080..e8f78506ff 100644 --- a/django-stubs/contrib/postgres/forms/hstore.pyi +++ b/django-stubs/contrib/postgres/forms/hstore.pyi @@ -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] diff --git a/django-stubs/contrib/postgres/forms/ranges.pyi b/django-stubs/contrib/postgres/forms/ranges.pyi index ea49e0d47c..ed1121e262 100644 --- a/django-stubs/contrib/postgres/forms/ranges.pyi +++ b/django-stubs/contrib/postgres/forms/ranges.pyi @@ -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: ... diff --git a/django-stubs/forms/fields.pyi b/django-stubs/forms/fields.pyi index c9d86d018d..71ffdf7aa3 100644 --- a/django-stubs/forms/fields.pyi +++ b/django-stubs/forms/fields.pyi @@ -2,7 +2,7 @@ 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 @@ -10,25 +10,46 @@ 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,10 +215,12 @@ 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: ... @@ -204,6 +228,7 @@ 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: ... diff --git a/django-stubs/forms/models.pyi b/django-stubs/forms/models.pyi index 1d243f60ea..0572d385ec 100644 --- a/django-stubs/forms/models.pyi +++ b/django-stubs/forms/models.pyi @@ -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] diff --git a/scripts/stubtest/allowlist.txt b/scripts/stubtest/allowlist.txt index 20065f28f7..3e7243517c 100644 --- a/scripts/stubtest/allowlist.txt +++ b/scripts/stubtest/allowlist.txt @@ -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__ diff --git a/tests/assert_type/contrib/auth/test_forms.py b/tests/assert_type/contrib/auth/test_forms.py new file mode 100644 index 0000000000..6d5684da3e --- /dev/null +++ b/tests/assert_type/contrib/auth/test_forms.py @@ -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) diff --git a/tests/assert_type/contrib/gis/forms/__init__.py b/tests/assert_type/contrib/gis/forms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/assert_type/contrib/gis/forms/test_fields.py b/tests/assert_type/contrib/gis/forms/test_fields.py new file mode 100644 index 0000000000..d168a39ea3 --- /dev/null +++ b/tests/assert_type/contrib/gis/forms/test_fields.py @@ -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) diff --git a/tests/assert_type/contrib/postgres/forms/test_array.py b/tests/assert_type/contrib/postgres/forms/test_array.py new file mode 100644 index 0000000000..714f4b7db9 --- /dev/null +++ b/tests/assert_type/contrib/postgres/forms/test_array.py @@ -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) diff --git a/tests/assert_type/contrib/postgres/forms/test_hstore.py b/tests/assert_type/contrib/postgres/forms/test_hstore.py new file mode 100644 index 0000000000..c50b85d1e3 --- /dev/null +++ b/tests/assert_type/contrib/postgres/forms/test_hstore.py @@ -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) diff --git a/tests/assert_type/contrib/postgres/forms/test_ranges.py b/tests/assert_type/contrib/postgres/forms/test_ranges.py new file mode 100644 index 0000000000..22528e659a --- /dev/null +++ b/tests/assert_type/contrib/postgres/forms/test_ranges.py @@ -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) diff --git a/tests/assert_type/forms/test_fields.py b/tests/assert_type/forms/test_fields.py new file mode 100644 index 0000000000..8efd93faa2 --- /dev/null +++ b/tests/assert_type/forms/test_fields.py @@ -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) diff --git a/tests/assert_type/forms/test_models.py b/tests/assert_type/forms/test_models.py new file mode 100644 index 0000000000..13839d8cbe --- /dev/null +++ b/tests/assert_type/forms/test_models.py @@ -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)