From 4c4e7952ee00eff22f258cacb18ed9d6f8b7b2c4 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:57:02 +0200 Subject: [PATCH 1/4] Replaced TypeAlias `_ClassLevelWidgetT` with descriptor `_WidgetTypeOrInstance` for `Field.widget` --- django-stubs/forms/fields.pyi | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/django-stubs/forms/fields.pyi b/django-stubs/forms/fields.pyi index 354fcbc33..a22abcc88 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, Protocol, overload, type_check_only from uuid import UUID from django.core.files import File @@ -15,20 +15,22 @@ from django.utils.choices import CallableChoiceIterator, _ChoicesCallable, _Choi 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 +@type_check_only +class _WidgetTypeOrInstance: + @overload + def __get__(self, instance: None, owner: type[Field]) -> type[Widget] | Widget: ... + @overload + def __get__(self, instance: Field, owner: type[Field]) -> Widget: ... + @overload + def __set__(self, instance: None, value: type[Widget] | Widget) -> None: ... + @overload + def __set__(self, instance: Field, value: Widget) -> None: ... class Field: initial: Any label: _StrOrPromise | None required: bool - widget: _ClassLevelWidgetT + widget: _WidgetTypeOrInstance hidden_widget: type[Widget] default_validators: list[_ValidatorCallable] default_error_messages: ClassVar[_ErrorMessagesDict] From f7144b11b30750ca6e393ac22eb316d21e132369 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:35:23 +0200 Subject: [PATCH 2/4] Removed leftover widget typings --- django-stubs/contrib/auth/forms.pyi | 2 -- django-stubs/contrib/gis/forms/fields.pyi | 1 - django-stubs/contrib/postgres/forms/array.pyi | 2 -- django-stubs/contrib/postgres/forms/hstore.pyi | 2 -- django-stubs/forms/fields.pyi | 2 -- django-stubs/forms/models.pyi | 4 +--- 6 files changed, 1 insertion(+), 12 deletions(-) diff --git a/django-stubs/contrib/auth/forms.pyi b/django-stubs/contrib/auth/forms.pyi index 2054b5400..0cec639d5 100644 --- a/django-stubs/contrib/auth/forms.pyi +++ b/django-stubs/contrib/auth/forms.pyi @@ -8,7 +8,6 @@ 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.widgets import Widget from django.http.request import HttpRequest from django.utils.functional import _StrOrPromise @@ -22,7 +21,6 @@ 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 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 ac9c7f011..cd616cc37 100644 --- a/django-stubs/contrib/gis/forms/fields.pyi +++ b/django-stubs/contrib/gis/forms/fields.pyi @@ -3,7 +3,6 @@ from typing import Any from django import forms class GeometryField(forms.Field): - widget: Any 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 0c4ff4a72..a9db2932b 100644 --- a/django-stubs/contrib/postgres/forms/array.pyi +++ b/django-stubs/contrib/postgres/forms/array.pyi @@ -3,7 +3,6 @@ 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.utils import _DataT, _FilesT from django.forms.widgets import _OptAttrs @@ -33,7 +32,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 diff --git a/django-stubs/contrib/postgres/forms/hstore.pyi b/django-stubs/contrib/postgres/forms/hstore.pyi index 32c874731..79026fa32 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/forms/fields.pyi b/django-stubs/forms/fields.pyi index a22abcc88..af284babf 100644 --- a/django-stubs/forms/fields.pyi +++ b/django-stubs/forms/fields.pyi @@ -321,7 +321,6 @@ class ChoiceField(Field): _ChoicesInput | _ChoicesCallable | CallableChoiceIterator, _ChoicesInput | CallableChoiceIterator, ] - widget: _ClassLevelWidgetT def __init__( self, *, @@ -549,7 +548,6 @@ class JSONString(str): ... class JSONField(CharField): default_error_messages: ClassVar[_ErrorMessagesDict] - widget: _ClassLevelWidgetT 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 1a25213fa..402b39be2 100644 --- a/django-stubs/forms/models.pyi +++ b/django-stubs/forms/models.pyi @@ -8,7 +8,7 @@ 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 from django.forms.forms import BaseForm, DeclarativeFieldsMetaclass from django.forms.formsets import BaseFormSet from django.forms.renderers import BaseRenderer @@ -226,7 +226,6 @@ class InlineForeignKeyField(Field): help_text: _StrOrPromise required: bool show_hidden_initial: bool - widget: _ClassLevelWidgetT parent_instance: Model pk_field: bool to_field: str | None @@ -296,7 +295,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] From 6c0e4a03f9f3d285dec989624150951235aa8dce Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Wed, 23 Apr 2025 20:19:34 +0200 Subject: [PATCH 3/4] Added assert type tests --- tests/assert_type/contrib/auth/test_forms.py | 9 ++ .../assert_type/contrib/gis/forms/__init__.py | 0 .../contrib/gis/forms/test_fields.py | 36 ++++++ .../contrib/postgres/forms/test_array.py | 15 +++ .../contrib/postgres/forms/test_hstore.py | 6 + .../contrib/postgres/forms/test_ranges.py | 20 +++ tests/assert_type/forms/test_fields.py | 121 ++++++++++++++++++ tests/assert_type/forms/test_models.py | 23 ++++ 8 files changed, 230 insertions(+) create mode 100644 tests/assert_type/contrib/auth/test_forms.py create mode 100644 tests/assert_type/contrib/gis/forms/__init__.py create mode 100644 tests/assert_type/contrib/gis/forms/test_fields.py create mode 100644 tests/assert_type/contrib/postgres/forms/test_array.py create mode 100644 tests/assert_type/contrib/postgres/forms/test_hstore.py create mode 100644 tests/assert_type/contrib/postgres/forms/test_ranges.py create mode 100644 tests/assert_type/forms/test_fields.py create mode 100644 tests/assert_type/forms/test_models.py 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 000000000..7f386255c --- /dev/null +++ b/tests/assert_type/contrib/auth/test_forms.py @@ -0,0 +1,9 @@ +from django.contrib.auth.forms import ReadOnlyPasswordHashField, UsernameField +from django.forms.widgets import Widget +from typing_extensions import assert_type + +assert_type(ReadOnlyPasswordHashField.widget, type[Widget] | Widget) +assert_type(ReadOnlyPasswordHashField().widget, Widget) + +assert_type(UsernameField.widget, type[Widget] | Widget) +assert_type(UsernameField().widget, Widget) 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 000000000..e69de29bb 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 000000000..c6dc18382 --- /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.forms.widgets import Widget +from typing_extensions import assert_type + +assert_type(GeometryField.widget, type[Widget] | Widget) +assert_type(GeometryField().widget, Widget) + +assert_type(GeometryCollectionField.widget, type[Widget] | Widget) +assert_type(GeometryCollectionField().widget, Widget) + +assert_type(PointField.widget, type[Widget] | Widget) +assert_type(PointField().widget, Widget) + +assert_type(MultiPointField.widget, type[Widget] | Widget) +assert_type(MultiPointField().widget, Widget) + +assert_type(LineStringField.widget, type[Widget] | Widget) +assert_type(LineStringField().widget, Widget) + +assert_type(MultiLineStringField.widget, type[Widget] | Widget) +assert_type(MultiLineStringField().widget, Widget) + +assert_type(PolygonField.widget, type[Widget] | Widget) +assert_type(PolygonField().widget, Widget) + +assert_type(MultiPolygonField.widget, type[Widget] | Widget) +assert_type(MultiPolygonField().widget, Widget) 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 000000000..07cef081c --- /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 +from django.forms.fields import Field +from django.forms.widgets import Widget +from typing_extensions import assert_type + +base_field = cast(Field, ...) +size = cast(int, ...) + +assert_type(SimpleArrayField.widget, type[Widget] | Widget) +assert_type(SimpleArrayField(base_field).widget, Widget) + +assert_type(SplitArrayField.widget, type[Widget] | Widget) +assert_type(SplitArrayField(base_field, size).widget, Widget) 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 000000000..077cec0fd --- /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 Widget +from typing_extensions import assert_type + +assert_type(HStoreField.widget, type[Widget] | Widget) +assert_type(HStoreField().widget, Widget) 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 000000000..1937fa4e5 --- /dev/null +++ b/tests/assert_type/contrib/postgres/forms/test_ranges.py @@ -0,0 +1,20 @@ +from django.contrib.postgres.forms.ranges import ( + DateRangeField, + DateTimeRangeField, + DecimalRangeField, + IntegerRangeField, +) +from django.forms.widgets import Widget +from typing_extensions import assert_type + +assert_type(IntegerRangeField.widget, type[Widget] | Widget) +assert_type(IntegerRangeField().widget, Widget) + +assert_type(DecimalRangeField.widget, type[Widget] | Widget) +assert_type(DecimalRangeField().widget, Widget) + +assert_type(DateTimeRangeField.widget, type[Widget] | Widget) +assert_type(DateTimeRangeField().widget, Widget) + +assert_type(DateRangeField.widget, type[Widget] | Widget) +assert_type(DateRangeField().widget, Widget) diff --git a/tests/assert_type/forms/test_fields.py b/tests/assert_type/forms/test_fields.py new file mode 100644 index 000000000..f67afc45d --- /dev/null +++ b/tests/assert_type/forms/test_fields.py @@ -0,0 +1,121 @@ +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 Widget +from typing_extensions import assert_type + +assert_type(CharField.widget, type[Widget] | Widget) +assert_type(CharField().widget, Widget) + +assert_type(IntegerField.widget, type[Widget] | Widget) +assert_type(IntegerField().widget, Widget) + +assert_type(FloatField.widget, type[Widget] | Widget) +assert_type(FloatField().widget, Widget) + +assert_type(DecimalField.widget, type[Widget] | Widget) +assert_type(DecimalField().widget, Widget) + +assert_type(DateField.widget, type[Widget] | Widget) +assert_type(DateField().widget, Widget) + +assert_type(TimeField.widget, type[Widget] | Widget) +assert_type(TimeField().widget, Widget) + +assert_type(DateTimeField.widget, type[Widget] | Widget) +assert_type(DateTimeField().widget, Widget) + +assert_type(DurationField.widget, type[Widget] | Widget) +assert_type(DurationField().widget, Widget) + +regex = cast(str, ...) + +assert_type(RegexField.widget, type[Widget] | Widget) +assert_type(RegexField(regex).widget, Widget) + +assert_type(EmailField.widget, type[Widget] | Widget) +assert_type(EmailField().widget, Widget) + +assert_type(FileField.widget, type[Widget] | Widget) +assert_type(FileField().widget, Widget) + +assert_type(ImageField.widget, type[Widget] | Widget) +assert_type(ImageField().widget, Widget) + +assert_type(URLField.widget, type[Widget] | Widget) +assert_type(URLField().widget, Widget) + +assert_type(BooleanField.widget, type[Widget] | Widget) +assert_type(BooleanField().widget, Widget) + +assert_type(NullBooleanField.widget, type[Widget] | Widget) +assert_type(NullBooleanField().widget, Widget) + +assert_type(ChoiceField.widget, type[Widget] | Widget) +assert_type(ChoiceField().widget, Widget) + +assert_type(TypedChoiceField.widget, type[Widget] | Widget) +assert_type(TypedChoiceField().widget, Widget) + +assert_type(MultipleChoiceField.widget, type[Widget] | Widget) +assert_type(MultipleChoiceField().widget, Widget) + +assert_type(TypedMultipleChoiceField.widget, type[Widget] | Widget) +assert_type(TypedMultipleChoiceField().widget, Widget) + +fields = cast(list[Field], ...) + +assert_type(ComboField.widget, type[Widget] | Widget) +assert_type(ComboField(fields).widget, Widget) + +assert_type(MultiValueField.widget, type[Widget] | Widget) +assert_type(MultiValueField(fields).widget, Widget) + +path = cast(str, ...) + +assert_type(FilePathField.widget, type[Widget] | Widget) +assert_type(FilePathField(path).widget, Widget) + +assert_type(SplitDateTimeField.widget, type[Widget] | Widget) +assert_type(SplitDateTimeField().widget, Widget) + +assert_type(GenericIPAddressField.widget, type[Widget] | Widget) +assert_type(GenericIPAddressField().widget, Widget) + +assert_type(SlugField.widget, type[Widget] | Widget) +assert_type(SlugField().widget, Widget) + +assert_type(UUIDField.widget, type[Widget] | Widget) +assert_type(UUIDField().widget, Widget) + +assert_type(JSONField.widget, type[Widget] | Widget) +assert_type(JSONField().widget, Widget) diff --git a/tests/assert_type/forms/test_models.py b/tests/assert_type/forms/test_models.py new file mode 100644 index 000000000..e5d4c9402 --- /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 Widget +from typing_extensions import assert_type + + +class TestModel(Model): ... + + +testmodel_instance = cast(TestModel, ...) + +assert_type(InlineForeignKeyField.widget, type[Widget] | Widget) +assert_type(InlineForeignKeyField(testmodel_instance).widget, Widget) + +testmodel_queryset = cast(QuerySet[TestModel], ...) + +assert_type(ModelChoiceField.widget, type[Widget] | Widget) +assert_type(ModelChoiceField(testmodel_queryset).widget, Widget) + +assert_type(ModelMultipleChoiceField.widget, type[Widget] | Widget) +assert_type(ModelMultipleChoiceField(testmodel_queryset).widget, Widget) From cdd1d73a3cfdd6ed693b0c3eb7dc6e742e62de0c Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 12 May 2025 03:48:46 +0200 Subject: [PATCH 4/4] Refactor `_WidgetTypeOrInstance` to generic descriptor, updated tests and added widgets to allowlist.txt --- django-stubs/contrib/auth/forms.pyi | 2 + django-stubs/contrib/gis/forms/fields.pyi | 3 + django-stubs/contrib/postgres/forms/array.pyi | 2 + .../contrib/postgres/forms/ranges.pyi | 4 +- django-stubs/forms/fields.pyi | 48 +++++-- django-stubs/forms/models.pyi | 5 +- scripts/stubtest/allowlist.txt | 57 ++++++++ tests/assert_type/contrib/auth/test_forms.py | 15 +- .../contrib/gis/forms/test_fields.py | 34 ++--- .../contrib/postgres/forms/test_array.py | 12 +- .../contrib/postgres/forms/test_hstore.py | 6 +- .../contrib/postgres/forms/test_ranges.py | 19 +-- tests/assert_type/forms/test_fields.py | 135 +++++++++++------- tests/assert_type/forms/test_models.py | 14 +- 14 files changed, 242 insertions(+), 114 deletions(-) diff --git a/django-stubs/contrib/auth/forms.pyi b/django-stubs/contrib/auth/forms.pyi index 0cec639d5..b37c357e0 100644 --- a/django-stubs/contrib/auth/forms.pyi +++ b/django-stubs/contrib/auth/forms.pyi @@ -8,6 +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 _WidgetTypeOrInstance from django.forms.widgets import Widget from django.http.request import HttpRequest from django.utils.functional import _StrOrPromise @@ -21,6 +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: _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 cd616cc37..e1c9b930d 100644 --- a/django-stubs/contrib/gis/forms/fields.pyi +++ b/django-stubs/contrib/gis/forms/fields.pyi @@ -1,8 +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: _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 a9db2932b..1ae969f2a 100644 --- a/django-stubs/contrib/postgres/forms/array.pyi +++ b/django-stubs/contrib/postgres/forms/array.pyi @@ -3,6 +3,7 @@ from typing import Any, ClassVar from django import forms from django.db.models.fields import _ErrorMessagesDict +from django.forms.fields import _WidgetTypeOrInstance from django.forms.utils import _DataT, _FilesT from django.forms.widgets import _OptAttrs @@ -44,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/ranges.pyi b/django-stubs/contrib/postgres/forms/ranges.pyi index a93cb8496..f6f27daec 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 af284babf..413042497 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, overload, 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,27 +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 +_ClassWidget = TypeVar("_ClassWidget", bound=Widget) +_InstanceWidget = TypeVar("_InstanceWidget", bound=Widget, default=_ClassWidget) + @type_check_only -class _WidgetTypeOrInstance: +class _WidgetTypeOrInstance(Generic[_ClassWidget, _InstanceWidget]): @overload - def __get__(self, instance: None, owner: type[Field]) -> type[Widget] | Widget: ... + def __get__(self, instance: None, owner: type[Field]) -> type[_ClassWidget] | _ClassWidget: ... @overload - def __get__(self, instance: Field, owner: type[Field]) -> Widget: ... + def __get__(self, instance: Field, owner: type[Field]) -> _InstanceWidget: ... @overload - def __set__(self, instance: None, value: type[Widget] | Widget) -> None: ... + def __set__(self, instance: None, value: type[_ClassWidget] | _ClassWidget) -> None: ... @overload - def __set__(self, instance: Field, value: Widget) -> None: ... + def __set__(self, instance: Field, value: _InstanceWidget) -> None: ... class Field: initial: Any label: _StrOrPromise | None required: bool - widget: _WidgetTypeOrInstance + widget: _WidgetTypeOrInstance[TextInput] hidden_widget: type[Widget] default_validators: list[_ValidatorCallable] default_error_messages: ClassVar[_ErrorMessagesDict] @@ -96,6 +115,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 +213,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 +226,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 +258,7 @@ class RegexField(CharField): ) -> None: ... class EmailField(CharField): + widget: _WidgetTypeOrInstance[EmailInput] # type: ignore[assignment] def __init__( self, *, @@ -256,6 +280,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 +310,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,15 +334,18 @@ 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, @@ -371,6 +400,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: ... @@ -474,6 +504,7 @@ class FilePathField(ChoiceField): ) -> None: ... class SplitDateTimeField(MultiValueField): + widget: _WidgetTypeOrInstance[SplitDateTimeWidget] # type: ignore[assignment] def __init__( self, *, @@ -548,6 +579,7 @@ class JSONString(str): ... class JSONField(CharField): default_error_messages: ClassVar[_ErrorMessagesDict] + 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 402b39be2..a4d9fc10d 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 +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,6 +222,7 @@ def inlineformset_factory( ) -> type[BaseInlineFormSet[_M, _ParentM, _ModelFormT]]: ... class InlineForeignKeyField(Field): + widget: _WidgetTypeOrInstance[HiddenInput] # type: ignore[assignment] disabled: bool help_text: _StrOrPromise required: bool diff --git a/scripts/stubtest/allowlist.txt b/scripts/stubtest/allowlist.txt index 4be63fbf2..500060f35 100644 --- a/scripts/stubtest/allowlist.txt +++ b/scripts/stubtest/allowlist.txt @@ -434,6 +434,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 index 7f386255c..6d5684da3 100644 --- a/tests/assert_type/contrib/auth/test_forms.py +++ b/tests/assert_type/contrib/auth/test_forms.py @@ -1,9 +1,12 @@ -from django.contrib.auth.forms import ReadOnlyPasswordHashField, UsernameField -from django.forms.widgets import Widget +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[Widget] | Widget) -assert_type(ReadOnlyPasswordHashField().widget, Widget) +assert_type( + ReadOnlyPasswordHashField.widget, + type[ReadOnlyPasswordHashWidget] | ReadOnlyPasswordHashWidget, +) +assert_type(ReadOnlyPasswordHashField().widget, ReadOnlyPasswordHashWidget) -assert_type(UsernameField.widget, type[Widget] | Widget) -assert_type(UsernameField().widget, Widget) +assert_type(UsernameField.widget, type[TextInput] | TextInput) +assert_type(UsernameField().widget, TextInput) diff --git a/tests/assert_type/contrib/gis/forms/test_fields.py b/tests/assert_type/contrib/gis/forms/test_fields.py index c6dc18382..d168a39ea 100644 --- a/tests/assert_type/contrib/gis/forms/test_fields.py +++ b/tests/assert_type/contrib/gis/forms/test_fields.py @@ -8,29 +8,29 @@ PointField, PolygonField, ) -from django.forms.widgets import Widget +from django.contrib.gis.forms.widgets import OpenLayersWidget from typing_extensions import assert_type -assert_type(GeometryField.widget, type[Widget] | Widget) -assert_type(GeometryField().widget, Widget) +assert_type(GeometryField.widget, type[OpenLayersWidget] | OpenLayersWidget) +assert_type(GeometryField().widget, OpenLayersWidget) -assert_type(GeometryCollectionField.widget, type[Widget] | Widget) -assert_type(GeometryCollectionField().widget, Widget) +assert_type(GeometryCollectionField.widget, type[OpenLayersWidget] | OpenLayersWidget) +assert_type(GeometryCollectionField().widget, OpenLayersWidget) -assert_type(PointField.widget, type[Widget] | Widget) -assert_type(PointField().widget, Widget) +assert_type(PointField.widget, type[OpenLayersWidget] | OpenLayersWidget) +assert_type(PointField().widget, OpenLayersWidget) -assert_type(MultiPointField.widget, type[Widget] | Widget) -assert_type(MultiPointField().widget, Widget) +assert_type(MultiPointField.widget, type[OpenLayersWidget] | OpenLayersWidget) +assert_type(MultiPointField().widget, OpenLayersWidget) -assert_type(LineStringField.widget, type[Widget] | Widget) -assert_type(LineStringField().widget, Widget) +assert_type(LineStringField.widget, type[OpenLayersWidget] | OpenLayersWidget) +assert_type(LineStringField().widget, OpenLayersWidget) -assert_type(MultiLineStringField.widget, type[Widget] | Widget) -assert_type(MultiLineStringField().widget, Widget) +assert_type(MultiLineStringField.widget, type[OpenLayersWidget] | OpenLayersWidget) +assert_type(MultiLineStringField().widget, OpenLayersWidget) -assert_type(PolygonField.widget, type[Widget] | Widget) -assert_type(PolygonField().widget, Widget) +assert_type(PolygonField.widget, type[OpenLayersWidget] | OpenLayersWidget) +assert_type(PolygonField().widget, OpenLayersWidget) -assert_type(MultiPolygonField.widget, type[Widget] | Widget) -assert_type(MultiPolygonField().widget, Widget) +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 index 07cef081c..714f4b7db 100644 --- a/tests/assert_type/contrib/postgres/forms/test_array.py +++ b/tests/assert_type/contrib/postgres/forms/test_array.py @@ -1,15 +1,15 @@ from typing import cast -from django.contrib.postgres.forms.array import SimpleArrayField, SplitArrayField +from django.contrib.postgres.forms.array import SimpleArrayField, SplitArrayField, SplitArrayWidget from django.forms.fields import Field -from django.forms.widgets import Widget +from django.forms.widgets import TextInput from typing_extensions import assert_type base_field = cast(Field, ...) size = cast(int, ...) -assert_type(SimpleArrayField.widget, type[Widget] | Widget) -assert_type(SimpleArrayField(base_field).widget, Widget) +assert_type(SimpleArrayField.widget, type[TextInput] | TextInput) +assert_type(SimpleArrayField(base_field).widget, TextInput) -assert_type(SplitArrayField.widget, type[Widget] | Widget) -assert_type(SplitArrayField(base_field, size).widget, Widget) +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 index 077cec0fd..c50b85d1e 100644 --- a/tests/assert_type/contrib/postgres/forms/test_hstore.py +++ b/tests/assert_type/contrib/postgres/forms/test_hstore.py @@ -1,6 +1,6 @@ from django.contrib.postgres.forms.hstore import HStoreField -from django.forms.widgets import Widget +from django.forms.widgets import TextInput from typing_extensions import assert_type -assert_type(HStoreField.widget, type[Widget] | Widget) -assert_type(HStoreField().widget, Widget) +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 index 1937fa4e5..22528e659 100644 --- a/tests/assert_type/contrib/postgres/forms/test_ranges.py +++ b/tests/assert_type/contrib/postgres/forms/test_ranges.py @@ -3,18 +3,19 @@ DateTimeRangeField, DecimalRangeField, IntegerRangeField, + RangeWidget, ) -from django.forms.widgets import Widget +from django.forms.widgets import TextInput from typing_extensions import assert_type -assert_type(IntegerRangeField.widget, type[Widget] | Widget) -assert_type(IntegerRangeField().widget, Widget) +assert_type(IntegerRangeField.widget, type[TextInput] | TextInput) +assert_type(IntegerRangeField().widget, RangeWidget) -assert_type(DecimalRangeField.widget, type[Widget] | Widget) -assert_type(DecimalRangeField().widget, Widget) +assert_type(DecimalRangeField.widget, type[TextInput] | TextInput) +assert_type(DecimalRangeField().widget, RangeWidget) -assert_type(DateTimeRangeField.widget, type[Widget] | Widget) -assert_type(DateTimeRangeField().widget, Widget) +assert_type(DateTimeRangeField.widget, type[TextInput] | TextInput) +assert_type(DateTimeRangeField().widget, RangeWidget) -assert_type(DateRangeField.widget, type[Widget] | Widget) -assert_type(DateRangeField().widget, Widget) +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 index f67afc45d..8efd93faa 100644 --- a/tests/assert_type/forms/test_fields.py +++ b/tests/assert_type/forms/test_fields.py @@ -30,92 +30,117 @@ URLField, UUIDField, ) -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, +) from typing_extensions import assert_type -assert_type(CharField.widget, type[Widget] | Widget) -assert_type(CharField().widget, Widget) +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[Widget] | Widget) -assert_type(IntegerField().widget, Widget) +assert_type(IntegerField.widget, type[NumberInput] | NumberInput) +assert_type(IntegerField().widget, NumberInput) -assert_type(FloatField.widget, type[Widget] | Widget) -assert_type(FloatField().widget, Widget) +assert_type(FloatField.widget, type[NumberInput] | NumberInput) +assert_type(FloatField().widget, NumberInput) -assert_type(DecimalField.widget, type[Widget] | Widget) -assert_type(DecimalField().widget, Widget) +assert_type(DecimalField.widget, type[NumberInput] | NumberInput) +assert_type(DecimalField().widget, NumberInput) -assert_type(DateField.widget, type[Widget] | Widget) -assert_type(DateField().widget, Widget) +assert_type(DateField.widget, type[DateInput] | DateInput) +assert_type(DateField().widget, DateInput) -assert_type(TimeField.widget, type[Widget] | Widget) -assert_type(TimeField().widget, Widget) +assert_type(TimeField.widget, type[TimeInput] | TimeInput) +assert_type(TimeField().widget, TimeInput) -assert_type(DateTimeField.widget, type[Widget] | Widget) -assert_type(DateTimeField().widget, Widget) +assert_type(DateTimeField.widget, type[DateTimeInput] | DateTimeInput) +assert_type(DateTimeField().widget, DateTimeInput) -assert_type(DurationField.widget, type[Widget] | Widget) -assert_type(DurationField().widget, Widget) +assert_type(DurationField.widget, type[TextInput] | TextInput) +assert_type(DurationField().widget, TextInput) regex = cast(str, ...) -assert_type(RegexField.widget, type[Widget] | Widget) -assert_type(RegexField(regex).widget, Widget) +assert_type(RegexField.widget, type[TextInput] | TextInput) +assert_type(RegexField(regex).widget, TextInput) -assert_type(EmailField.widget, type[Widget] | Widget) -assert_type(EmailField().widget, Widget) +assert_type(EmailField.widget, type[EmailInput] | EmailInput) +assert_type(EmailField().widget, EmailInput) -assert_type(FileField.widget, type[Widget] | Widget) -assert_type(FileField().widget, Widget) +assert_type(FileField.widget, type[ClearableFileInput] | ClearableFileInput) +assert_type(FileField().widget, ClearableFileInput) -assert_type(ImageField.widget, type[Widget] | Widget) -assert_type(ImageField().widget, Widget) +assert_type(ImageField.widget, type[ClearableFileInput] | ClearableFileInput) +assert_type(ImageField().widget, ClearableFileInput) -assert_type(URLField.widget, type[Widget] | Widget) -assert_type(URLField().widget, Widget) +assert_type(URLField.widget, type[URLInput] | URLInput) +assert_type(URLField().widget, URLInput) -assert_type(BooleanField.widget, type[Widget] | Widget) -assert_type(BooleanField().widget, Widget) +assert_type(BooleanField.widget, type[CheckboxInput] | CheckboxInput) +assert_type(BooleanField().widget, CheckboxInput) -assert_type(NullBooleanField.widget, type[Widget] | Widget) -assert_type(NullBooleanField().widget, Widget) +assert_type(NullBooleanField.widget, type[NullBooleanSelect] | NullBooleanSelect) +assert_type(NullBooleanField().widget, NullBooleanSelect) -assert_type(ChoiceField.widget, type[Widget] | Widget) -assert_type(ChoiceField().widget, Widget) +assert_type(ChoiceField.widget, type[Select] | Select) +assert_type(ChoiceField().widget, Select) -assert_type(TypedChoiceField.widget, type[Widget] | Widget) -assert_type(TypedChoiceField().widget, Widget) +assert_type(TypedChoiceField.widget, type[Select] | Select) +assert_type(TypedChoiceField().widget, Select) -assert_type(MultipleChoiceField.widget, type[Widget] | Widget) -assert_type(MultipleChoiceField().widget, Widget) +assert_type(MultipleChoiceField.widget, type[SelectMultiple] | SelectMultiple) +assert_type(MultipleChoiceField().widget, SelectMultiple) -assert_type(TypedMultipleChoiceField.widget, type[Widget] | Widget) -assert_type(TypedMultipleChoiceField().widget, Widget) +assert_type(TypedMultipleChoiceField.widget, type[SelectMultiple] | SelectMultiple) +assert_type(TypedMultipleChoiceField().widget, SelectMultiple) fields = cast(list[Field], ...) -assert_type(ComboField.widget, type[Widget] | Widget) -assert_type(ComboField(fields).widget, Widget) +assert_type(ComboField.widget, type[TextInput] | TextInput) +assert_type(ComboField(fields).widget, TextInput) -assert_type(MultiValueField.widget, type[Widget] | Widget) -assert_type(MultiValueField(fields).widget, Widget) +assert_type(MultiValueField.widget, type[TextInput] | TextInput) +assert_type(MultiValueField(fields).widget, TextInput) path = cast(str, ...) -assert_type(FilePathField.widget, type[Widget] | Widget) -assert_type(FilePathField(path).widget, Widget) +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(SplitDateTimeField.widget, type[Widget] | Widget) -assert_type(SplitDateTimeField().widget, Widget) +assert_type(JSONField.widget, type[Textarea] | Textarea) +assert_type(JSONField().widget, Textarea) -assert_type(GenericIPAddressField.widget, type[Widget] | Widget) -assert_type(GenericIPAddressField().widget, Widget) -assert_type(SlugField.widget, type[Widget] | Widget) -assert_type(SlugField().widget, Widget) +class CustomIntegerField(IntegerField): ... -assert_type(UUIDField.widget, type[Widget] | Widget) -assert_type(UUIDField().widget, Widget) -assert_type(JSONField.widget, type[Widget] | Widget) -assert_type(JSONField().widget, Widget) +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 index e5d4c9402..13839d8cb 100644 --- a/tests/assert_type/forms/test_models.py +++ b/tests/assert_type/forms/test_models.py @@ -2,7 +2,7 @@ from django.db.models import Model, QuerySet from django.forms.models import InlineForeignKeyField, ModelChoiceField, ModelMultipleChoiceField -from django.forms.widgets import Widget +from django.forms.widgets import HiddenInput, Select from typing_extensions import assert_type @@ -11,13 +11,13 @@ class TestModel(Model): ... testmodel_instance = cast(TestModel, ...) -assert_type(InlineForeignKeyField.widget, type[Widget] | Widget) -assert_type(InlineForeignKeyField(testmodel_instance).widget, Widget) +assert_type(InlineForeignKeyField.widget, type[HiddenInput] | HiddenInput) +assert_type(InlineForeignKeyField(testmodel_instance).widget, HiddenInput) testmodel_queryset = cast(QuerySet[TestModel], ...) -assert_type(ModelChoiceField.widget, type[Widget] | Widget) -assert_type(ModelChoiceField(testmodel_queryset).widget, Widget) +assert_type(ModelChoiceField.widget, type[Select] | Select) +assert_type(ModelChoiceField(testmodel_queryset).widget, Select) -assert_type(ModelMultipleChoiceField.widget, type[Widget] | Widget) -assert_type(ModelMultipleChoiceField(testmodel_queryset).widget, Widget) +assert_type(ModelMultipleChoiceField.widget, type[Select] | Select) +assert_type(ModelMultipleChoiceField(testmodel_queryset).widget, Select)