diff --git a/django-stubs/contrib/gis/db/models/fields.pyi b/django-stubs/contrib/gis/db/models/fields.pyi index 08f542006..7d501b017 100644 --- a/django-stubs/contrib/gis/db/models/fields.pyi +++ b/django-stubs/contrib/gis/db/models/fields.pyi @@ -16,6 +16,7 @@ from django.core.validators import _ValidatorCallable from django.db.models import Model from django.db.models.expressions import Combinable, Expression from django.db.models.fields import NOT_PROVIDED, Field, _ErrorMessagesMapping +from django.forms.widgets import Widget from django.utils.choices import _Choices from django.utils.functional import _StrOrPromise from typing_extensions import TypeVar, override @@ -125,10 +126,23 @@ class GeometryField(BaseSpatialField[_ST, _GT]): self, *, form_class: type[forms.GeometryField] | None = ..., + choices_form_class: type[forms.GeometryField] | None = ..., + required: bool = ..., + widget: Widget | type[Widget] | None = ..., + label: _StrOrPromise | None = ..., + initial: Any | None = ..., + help_text: _StrOrPromise = ..., + error_messages: _ErrorMessagesMapping | None = ..., + show_hidden_initial: bool = ..., + validators: Iterable[_ValidatorCallable] = ..., + localize: bool = ..., + disabled: bool = ..., + label_suffix: str | None = ..., + # GeometryField adds `geom_type` and `srid` geom_type: str = ..., srid: Any = ..., **kwargs: Any, - ) -> forms.GeometryField: ... + ) -> forms.GeometryField | None: ... class PointField(GeometryField[_ST, _GT]): _pyi_private_set_type: Point | Combinable diff --git a/django-stubs/contrib/postgres/fields/array.pyi b/django-stubs/contrib/postgres/fields/array.pyi index b0ed107c5..8dd3f0fee 100644 --- a/django-stubs/contrib/postgres/fields/array.pyi +++ b/django-stubs/contrib/postgres/fields/array.pyi @@ -69,7 +69,5 @@ class ArrayField(CheckPostgresInstalledMixin, CheckFieldDefaultMixin, Field[_ST, def get_placeholder(self, value: Unused, compiler: Unused, connection: BaseDatabaseWrapper) -> str: ... @override def get_transform(self, name: str) -> type[Transform] | None: ... - @override - def formfield(self, **kwargs: Any) -> Any: ... # type: ignore[override] __all__ = ["ArrayField"] diff --git a/django-stubs/contrib/postgres/fields/hstore.pyi b/django-stubs/contrib/postgres/fields/hstore.pyi index 340389951..7548f172b 100644 --- a/django-stubs/contrib/postgres/fields/hstore.pyi +++ b/django-stubs/contrib/postgres/fields/hstore.pyi @@ -11,8 +11,6 @@ from typing_extensions import override class HStoreField(CheckPostgresInstalledMixin, CheckFieldDefaultMixin, Field): @override def get_transform(self, name: str) -> Any: ... - @override - def formfield(self, **kwargs: Any) -> Any: ... # type: ignore[override] class KeyTransform(Transform): output_field: ClassVar[TextField] diff --git a/django-stubs/contrib/postgres/fields/ranges.pyi b/django-stubs/contrib/postgres/fields/ranges.pyi index ab553aa54..88194bdeb 100644 --- a/django-stubs/contrib/postgres/fields/ranges.pyi +++ b/django-stubs/contrib/postgres/fields/ranges.pyi @@ -40,8 +40,6 @@ class RangeField(CheckPostgresInstalledMixin, models.Field[Any, _RangeT]): def to_python(self, value: Any) -> Any: ... @override def value_to_string(self, obj: models.Model) -> str | None: ... # type: ignore[override] - @override - def formfield(self, **kwargs: Any) -> Any: ... # type: ignore[override] class ContinuousRangeField(RangeField[_RangeT]): default_bounds: str diff --git a/django-stubs/db/models/fields/__init__.pyi b/django-stubs/db/models/fields/__init__.pyi index 60e64c472..f0f8c7165 100644 --- a/django-stubs/db/models/fields/__init__.pyi +++ b/django-stubs/db/models/fields/__init__.pyi @@ -15,6 +15,7 @@ from django.db.models.fields.reverse_related import ForeignObjectRel from django.db.models.query import _OrderByFieldName from django.db.models.query_utils import Q, RegisterLookupMixin from django.db.models.sql.compiler import SQLCompiler, _AsSqlType, _ParamsT +from django.forms.widgets import Widget from django.utils.choices import BlankChoiceIterator, _Choice, _ChoiceNamedGroup, _ChoicesCallable, _ChoicesInput from django.utils.datastructures import DictWrapper from django.utils.functional import _Getter, _StrOrPromise, cached_property @@ -250,9 +251,22 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]): def save_form_data(self, instance: Model, data: Any) -> None: ... def formfield( self, - form_class: type[forms.Field] | None = None, - choices_form_class: type[forms.ChoiceField] | None = None, + *, + form_class: type[forms.Field] | None = ..., + choices_form_class: type[forms.ChoiceField] | None = ..., + required: bool = ..., + widget: Widget | type[Widget] | None = ..., + label: _StrOrPromise | None = ..., + initial: Any | None = ..., + help_text: _StrOrPromise = ..., + error_messages: _ErrorMessagesMapping | None = ..., + show_hidden_initial: bool = ..., + validators: Iterable[_ValidatorCallable] = ..., + localize: bool = ..., + disabled: bool = ..., + label_suffix: str | None = ..., **kwargs: Any, + # Subclasses are allowed to return None ) -> forms.Field | None: ... def value_from_object(self, obj: Model) -> _GT: ... def slice_expression(self, expression: Expression, start: int, length: int | None) -> Func: ... @@ -261,8 +275,6 @@ class IntegerField(Field[_ST, _GT]): _pyi_private_set_type: float | int | str | Combinable _pyi_private_get_type: int _pyi_lookup_exact_type: str | int - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class PositiveIntegerRelDbTypeMixin: def rel_db_type(self, connection: BaseDatabaseWrapper) -> str: ... @@ -271,30 +283,20 @@ class SmallIntegerField(IntegerField[_ST, _GT]): ... class BigIntegerField(IntegerField[_ST, _GT]): MAX_BIGINT: ClassVar[int] - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class PositiveIntegerField(PositiveIntegerRelDbTypeMixin, IntegerField[_ST, _GT]): integer_field_class: type[IntegerField] - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class PositiveSmallIntegerField(PositiveIntegerRelDbTypeMixin, SmallIntegerField[_ST, _GT]): integer_field_class: type[SmallIntegerField] - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class PositiveBigIntegerField(PositiveIntegerRelDbTypeMixin, BigIntegerField[_ST, _GT]): integer_field_class: type[BigIntegerField] - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class FloatField(Field[_ST, _GT]): _pyi_private_set_type: float | int | str | Combinable _pyi_private_get_type: float _pyi_lookup_exact_type: float - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class DecimalField(Field[_ST, _GT]): _pyi_private_set_type: str | float | decimal.Decimal | Combinable @@ -330,8 +332,6 @@ class DecimalField(Field[_ST, _GT]): ) -> None: ... @cached_property def context(self) -> decimal.Context: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class CharField(Field[_ST, _GT]): _pyi_private_set_type: str | int | Combinable @@ -366,8 +366,6 @@ class CharField(Field[_ST, _GT]): *, db_collation: str | None = None, ) -> None: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class CommaSeparatedIntegerField(CharField[_ST, _GT]): ... @@ -400,13 +398,9 @@ class SlugField(CharField[_ST, _GT]): db_index: bool = True, allow_unicode: bool = False, ) -> None: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class EmailField(CharField[_ST, _GT]): _pyi_private_set_type: str | Combinable - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class URLField(CharField[_ST, _GT]): def __init__( @@ -437,8 +431,6 @@ class URLField(CharField[_ST, _GT]): validators: Iterable[_ValidatorCallable] = ..., error_messages: _ErrorMessagesMapping | None = ..., ) -> None: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class TextField(Field[_ST, _GT]): _pyi_private_set_type: str | Combinable @@ -473,15 +465,11 @@ class TextField(Field[_ST, _GT]): *, db_collation: str | None = None, ) -> None: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class BooleanField(Field[_ST, _GT]): _pyi_private_set_type: bool | Combinable _pyi_private_get_type: bool _pyi_lookup_exact_type: bool - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class NullBooleanField(BooleanField[_ST, _GT]): _pyi_private_set_type: bool | Combinable | None # type: ignore[assignment] @@ -523,8 +511,6 @@ class GenericIPAddressField(Field[_ST, _GT]): validators: Iterable[_ValidatorCallable] = ..., error_messages: _ErrorMessagesMapping | None = ..., ) -> None: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class DateTimeCheckMixin: def check(self, **kwargs: Any) -> list[CheckMessage]: ... @@ -563,8 +549,6 @@ class DateField(DateTimeCheckMixin, Field[_ST, _GT]): ) -> None: ... @override def contribute_to_class(self, cls: type[Model], name: str, **kwargs: Any) -> None: ... # type: ignore[override] - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class TimeField(DateTimeCheckMixin, Field[_ST, _GT]): _pyi_private_set_type: str | time | real_datetime | Combinable @@ -596,15 +580,11 @@ class TimeField(DateTimeCheckMixin, Field[_ST, _GT]): validators: Iterable[_ValidatorCallable] = ..., error_messages: _ErrorMessagesMapping | None = ..., ) -> None: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class DateTimeField(DateField[_ST, _GT]): _pyi_private_set_type: str | real_datetime | date | Combinable _pyi_private_get_type: real_datetime _pyi_lookup_exact_type: str | real_datetime - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class UUIDField(Field[_ST, _GT]): _pyi_private_set_type: str | uuid.UUID @@ -638,8 +618,6 @@ class UUIDField(Field[_ST, _GT]): validators: Iterable[_ValidatorCallable] = ..., error_messages: _ErrorMessagesMapping | None = ..., ) -> None: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class FilePathField(Field[_ST, _GT]): path: Any @@ -676,8 +654,6 @@ class FilePathField(Field[_ST, _GT]): validators: Iterable[_ValidatorCallable] = ..., error_messages: _ErrorMessagesMapping | None = ..., ) -> None: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class BinaryField(Field[_ST, _GT]): _pyi_private_get_type: bytes | memoryview @@ -685,8 +661,6 @@ class BinaryField(Field[_ST, _GT]): class DurationField(Field[_ST, _GT]): _pyi_private_get_type: timedelta - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] class AutoFieldMixin: db_returning: bool diff --git a/django-stubs/db/models/fields/files.pyi b/django-stubs/db/models/fields/files.pyi index 59a258a58..08a106fd9 100644 --- a/django-stubs/db/models/fields/files.pyi +++ b/django-stubs/db/models/fields/files.pyi @@ -108,8 +108,6 @@ class FileField(Field[Any, Any]): @override def contribute_to_class(self, cls: type[Model], name: str, **kwargs: Any) -> None: ... # type: ignore[override] def generate_filename(self, instance: Model | None, filename: _PathCompatible) -> str: ... - @override - def formfield(self, **kwargs: Any) -> Any: ... # type: ignore[override] class ImageFileDescriptor(FileDescriptor): field: ImageField @@ -145,5 +143,3 @@ class ImageField(FileField): @override def __get__(self, instance: Any, owner: Any) -> Self: ... def update_dimension_fields(self, instance: Model, force: bool = False, *args: Any, **kwargs: Any) -> None: ... - @override - def formfield(self, **kwargs: Any) -> Any: ... # type: ignore[override] diff --git a/django-stubs/db/models/fields/json.pyi b/django-stubs/db/models/fields/json.pyi index ab4ff1b3c..89bc78a75 100644 --- a/django-stubs/db/models/fields/json.pyi +++ b/django-stubs/db/models/fields/json.pyi @@ -35,8 +35,6 @@ class JSONField(CheckFieldDefaultMixin, Field[_ST, _GT]): def get_transform(self, name: str) -> type[Transform] | KeyTransformFactory: ... # type: ignore[override] @override def value_to_string(self, obj: Model) -> Any: ... - @override - def formfield(self, **kwargs: Any) -> Any: ... # type: ignore[override] class DataContains(FieldGetDbPrepValueMixin, PostgresOperatorLookup): ... class ContainedBy(FieldGetDbPrepValueMixin, PostgresOperatorLookup): ... diff --git a/django-stubs/db/models/fields/related.pyi b/django-stubs/db/models/fields/related.pyi index bfedf4e63..2caa440f0 100644 --- a/django-stubs/db/models/fields/related.pyi +++ b/django-stubs/db/models/fields/related.pyi @@ -21,6 +21,7 @@ from django.db.models.fields.reverse_related import ManyToOneRel as ManyToOneRel from django.db.models.fields.reverse_related import OneToOneRel as OneToOneRel from django.db.models.query_utils import FilteredRelation, PathInfo, Q from django.db.models.sql.where import WhereNode +from django.forms.widgets import Widget from django.utils.choices import _Choices from django.utils.functional import _StrOrPromise, cached_property from typing_extensions import Self, TypeVar, override @@ -89,8 +90,6 @@ class RelatedField(FieldCacheMixin, Field[_ST, _GT]): def set_attributes_from_rel(self) -> None: ... def do_related_class(self, other: type[Model], cls: type[Model]) -> None: ... def get_limit_choices_to(self) -> _LimitChoicesTo: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] def related_query_name(self) -> str: ... @property def target_field(self) -> Field: ... @@ -225,7 +224,26 @@ class ForeignKey(ForeignObject[_ST, _GT]): @override def contribute_to_related_class(self, cls: type[Model], related: RelatedField) -> None: ... @override - def formfield(self, *, using: str | None = None, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] + def formfield( + self, + *, + form_class: type[forms.Field] | None = ..., + choices_form_class: type[forms.ChoiceField] | None = ..., + required: bool = ..., + widget: Widget | type[Widget] | None = ..., + label: _StrOrPromise | None = ..., + initial: Any | None = ..., + help_text: _StrOrPromise = ..., + error_messages: _ErrorMessagesMapping | None = ..., + show_hidden_initial: bool = ..., + validators: Iterable[_ValidatorCallable] = ..., + localize: bool = ..., + disabled: bool = ..., + label_suffix: str | None = ..., + # ForeignKey adds `using` + using: str | None = ..., + **kwargs: Any, + ) -> forms.Field | None: ... @override def cast_db_type(self, connection: BaseDatabaseWrapper) -> str | None: ... def convert_empty_strings(self, value: Any, expression: Expression, connection: BaseDatabaseWrapper) -> Any: ... @@ -286,8 +304,6 @@ class OneToOneField(ForeignKey[_ST, _GT]): @overload @override def __get__(self, instance: Any, owner: Any) -> Self: ... - @override - def formfield(self, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] forward_related_accessor_class: type[ForwardOneToOneDescriptor] related_accessor_class: type[ReverseOneToOneDescriptor] # type: ignore[assignment] @@ -368,7 +384,26 @@ class ManyToManyField(RelatedField[Any, Any], Generic[_To, _Through]): def m2m_target_field_name(self) -> str: ... def m2m_reverse_target_field_name(self) -> str: ... @override - def formfield(self, *, using: str | None = None, **kwargs: Any) -> forms.Field | None: ... # type: ignore[override] + def formfield( + self, + *, + form_class: type[forms.Field] | None = ..., + choices_form_class: type[forms.ChoiceField] | None = ..., + required: bool = ..., + widget: Widget | type[Widget] | None = ..., + label: _StrOrPromise | None = ..., + initial: Any | None = ..., + help_text: _StrOrPromise = ..., + error_messages: _ErrorMessagesMapping | None = ..., + show_hidden_initial: bool = ..., + validators: Iterable[_ValidatorCallable] = ..., + localize: bool = ..., + disabled: bool = ..., + label_suffix: str | None = ..., + # ManyToManyField adds `using` + using: str | None = ..., + **kwargs: Any, + ) -> forms.Field | None: ... @cached_property def path_infos(self) -> list[PathInfo]: ... @cached_property diff --git a/scripts/stubtest/allowlist.txt b/scripts/stubtest/allowlist.txt index 5a9ea0b09..9ef2561a8 100644 --- a/scripts/stubtest/allowlist.txt +++ b/scripts/stubtest/allowlist.txt @@ -237,6 +237,11 @@ django.urls.resolvers.ResolverMatch.__iter__ django.template.smartif.key django.template.smartif.op +# The parameters form_class / choices_form_class are positional at runtime, but +# usage and docs always pass them as keyword args; additionally, all subclasses +# require them to be keyword-only, so require all usage to be keyword-only. +django.db.models.fields.Field.formfield + # Field.__get__/__set__ are stub-only for the mypy plugin, they don't exist at runtime django.db.models.fields.Field.__get__ django.db.models.fields.Field.__set__