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 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
2 changes: 0 additions & 2 deletions django-stubs/contrib/auth/forms.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
1 change: 0 additions & 1 deletion django-stubs/contrib/gis/forms/fields.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down
2 changes: 0 additions & 2 deletions django-stubs/contrib/postgres/forms/array.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions django-stubs/contrib/postgres/forms/hstore.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
24 changes: 12 additions & 12 deletions django-stubs/forms/fields.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -319,7 +321,6 @@ class ChoiceField(Field):
_ChoicesInput | _ChoicesCallable | CallableChoiceIterator,
_ChoicesInput | CallableChoiceIterator,
]
widget: _ClassLevelWidgetT
def __init__(
self,
*,
Expand Down Expand Up @@ -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: ...
Expand Down
4 changes: 1 addition & 3 deletions django-stubs/forms/models.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
9 changes: 9 additions & 0 deletions tests/assert_type/contrib/auth/test_forms.py
Original file line number Diff line number Diff line change
@@ -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)
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.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)
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
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)
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 Widget
from typing_extensions import assert_type

assert_type(HStoreField.widget, type[Widget] | Widget)
assert_type(HStoreField().widget, Widget)
20 changes: 20 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,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)
121 changes: 121 additions & 0 deletions tests/assert_type/forms/test_fields.py
Original file line number Diff line number Diff line change
@@ -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)
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 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)
Loading