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 354fcbc33..af284babf 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] @@ -319,7 +321,6 @@ class ChoiceField(Field): _ChoicesInput | _ChoicesCallable | CallableChoiceIterator, _ChoicesInput | CallableChoiceIterator, ] - widget: _ClassLevelWidgetT def __init__( self, *, @@ -547,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] 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)