Skip to content

Replace TypeAlias _ClassLevelWidgetT with descriptor _WidgetTypeOrInstance #2615

New issue

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

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

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions django-stubs/contrib/auth/forms.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
4 changes: 3 additions & 1 deletion django-stubs/contrib/gis/forms/fields.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Any

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

class GeometryField(forms.Field):
widget: Any
widget: _WidgetTypeOrInstance[OpenLayersWidget] # type: ignore[assignment]
geom_type: str
srid: Any
def __init__(self, *, srid: Any | None = ..., geom_type: Any | None = ..., **kwargs: Any) -> None: ...
Expand Down
4 changes: 2 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,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

Expand Down Expand Up @@ -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
Expand All @@ -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
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
4 changes: 3 additions & 1 deletion django-stubs/contrib/postgres/forms/ranges.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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: ...
Expand Down
58 changes: 45 additions & 13 deletions django-stubs/forms/fields.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,54 @@ import datetime
from collections.abc import Collection, Iterator, Sequence
from decimal import Decimal
from re import Pattern
from typing import Any, ClassVar, Protocol, TypeAlias, type_check_only
from typing import Any, ClassVar, Generic, Protocol, TypeVar, overload, type_check_only
from uuid import UUID

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

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

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

class Field:
initial: Any
label: _StrOrPromise | None
required: bool
widget: _ClassLevelWidgetT
widget: _WidgetTypeOrInstance[TextInput]
hidden_widget: type[Widget]
default_validators: list[_ValidatorCallable]
default_error_messages: ClassVar[_ErrorMessagesDict]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -193,17 +215,20 @@ class BaseTemporalField(Field):
def strptime(self, value: str, format: str) -> Any: ...

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

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

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

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

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

class EmailField(CharField):
widget: _WidgetTypeOrInstance[EmailInput] # type: ignore[assignment]
def __init__(
self,
*,
Expand All @@ -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__(
Expand Down Expand Up @@ -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,
*,
Expand All @@ -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,
*,
Expand Down Expand Up @@ -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: ...
Expand Down Expand Up @@ -475,6 +506,7 @@ class FilePathField(ChoiceField):
) -> None: ...

class SplitDateTimeField(MultiValueField):
widget: _WidgetTypeOrInstance[SplitDateTimeWidget] # type: ignore[assignment]
def __init__(
self,
*,
Expand Down Expand Up @@ -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: ...
Expand Down
7 changes: 3 additions & 4 deletions django-stubs/forms/models.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
57 changes: 57 additions & 0 deletions scripts/stubtest/allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down
12 changes: 12 additions & 0 deletions tests/assert_type/contrib/auth/test_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.contrib.auth.forms import ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget, UsernameField
from django.forms.widgets import TextInput
from typing_extensions import assert_type

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

assert_type(UsernameField.widget, type[TextInput] | TextInput)
assert_type(UsernameField().widget, TextInput)
Empty file.
Loading
Loading