From efe39a53abe2d9bdbebb114467d78a430e7f4e25 Mon Sep 17 00:00:00 2001 From: Daverball Date: Sat, 24 Oct 2026 00:06:51 +0200 Subject: [PATCH 1/5] [WTForms] Update stubs for 3.3.0 --- stubs/WTForms/@tests/stubtest_allowlist.txt | 10 ++ stubs/WTForms/METADATA.toml | 3 +- stubs/WTForms/wtforms/__init__.pyi | 12 +- stubs/WTForms/wtforms/_compat.pyi | 5 + stubs/WTForms/wtforms/datalist.pyi | 34 +++++ stubs/WTForms/wtforms/fields/__init__.pyi | 12 +- stubs/WTForms/wtforms/fields/choices.pyi | 143 ++++++++++++++++++-- stubs/WTForms/wtforms/fields/core.pyi | 10 +- stubs/WTForms/wtforms/fields/datetime.pyi | 19 ++- stubs/WTForms/wtforms/fields/form.pyi | 3 + stubs/WTForms/wtforms/fields/list.pyi | 6 +- stubs/WTForms/wtforms/fields/numeric.pyi | 11 ++ stubs/WTForms/wtforms/fields/simple.pyi | 7 + stubs/WTForms/wtforms/form.pyi | 1 + stubs/WTForms/wtforms/validators.pyi | 52 ++++++- stubs/WTForms/wtforms/widgets/__init__.pyi | 4 + stubs/WTForms/wtforms/widgets/core.pyi | 24 +++- 17 files changed, 321 insertions(+), 35 deletions(-) create mode 100644 stubs/WTForms/wtforms/_compat.pyi create mode 100644 stubs/WTForms/wtforms/datalist.pyi diff --git a/stubs/WTForms/@tests/stubtest_allowlist.txt b/stubs/WTForms/@tests/stubtest_allowlist.txt index 3d8e733ca9c7..377e71cea47e 100644 --- a/stubs/WTForms/@tests/stubtest_allowlist.txt +++ b/stubs/WTForms/@tests/stubtest_allowlist.txt @@ -10,3 +10,13 @@ wtforms.meta.DefaultMeta.__getattr__ # Should allow setting and deleting any attribute wtforms.fields.core.Flags.__delattr__ wtforms.fields.core.Flags.__setattr__ + +# Error: is inconsistent +# ============================= +# __post_init__ ensures label never is None +wtforms.fields.choices.SelectChoice.__init__ + +# Error: variable differs from runtime type +# ============================= +# __post_init__ ensures label never is None +wtforms.fields.choices.SelectChoice.label diff --git a/stubs/WTForms/METADATA.toml b/stubs/WTForms/METADATA.toml index 62e6a60d86ce..a6854ee7d251 100644 --- a/stubs/WTForms/METADATA.toml +++ b/stubs/WTForms/METADATA.toml @@ -1,3 +1,4 @@ -version = "~= 3.2.1" +version = "3.3.*" upstream-repository = "https://github.com/pallets-eco/wtforms" dependencies = ["MarkupSafe"] +requires-python = ">=3.11" diff --git a/stubs/WTForms/wtforms/__init__.pyi b/stubs/WTForms/wtforms/__init__.pyi index 1bdf94499a55..506d1da60172 100644 --- a/stubs/WTForms/wtforms/__init__.pyi +++ b/stubs/WTForms/wtforms/__init__.pyi @@ -1,8 +1,11 @@ from typing import Final from wtforms import validators as validators, widgets as widgets +from wtforms.datalist import DataList as DataList, DataListChoice as DataListChoice, enum_datalist as enum_datalist from wtforms.fields.choices import ( + Choice as Choice, RadioField as RadioField, + SelectChoice as SelectChoice, SelectField as SelectField, SelectFieldBase as SelectFieldBase, SelectMultipleField as SelectMultipleField, @@ -27,6 +30,7 @@ from wtforms.fields.numeric import ( ) from wtforms.fields.simple import ( BooleanField as BooleanField, + ButtonField as ButtonField, ColorField as ColorField, EmailField as EmailField, FileField as FileField, @@ -50,8 +54,8 @@ __all__ = [ "Form", "ValidationError", "SelectField", - "SelectFieldBase", "SelectMultipleField", + "SelectFieldBase", "RadioField", "Field", "Flags", @@ -69,7 +73,9 @@ __all__ = [ "FloatField", "IntegerRangeField", "DecimalRangeField", + "ButtonField", "BooleanField", + "DataList", "TextAreaField", "PasswordField", "FileField", @@ -82,4 +88,8 @@ __all__ = [ "URLField", "EmailField", "ColorField", + "Choice", + "SelectChoice", + "DataListChoice", + "enum_datalist", ] diff --git a/stubs/WTForms/wtforms/_compat.pyi b/stubs/WTForms/wtforms/_compat.pyi new file mode 100644 index 000000000000..367feef1aee1 --- /dev/null +++ b/stubs/WTForms/wtforms/_compat.pyi @@ -0,0 +1,5 @@ +from collections.abc import Callable +from inspect import Signature +from typing import Any + +def get_signature(callable: Callable[..., Any]) -> Signature: ... diff --git a/stubs/WTForms/wtforms/datalist.pyi b/stubs/WTForms/wtforms/datalist.pyi new file mode 100644 index 000000000000..ad7297d95d2f --- /dev/null +++ b/stubs/WTForms/wtforms/datalist.pyi @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from enum import EnumMeta +from typing import Any, Protocol, type_check_only + +from markupsafe import Markup +from wtforms.fields import Field + +@type_check_only +class _DataListWidget(Protocol): + def __call__(self, datalist: DataList, /, *, field: Field | None = None, **kwargs: Any) -> Markup: ... + +__all__ = ("DataList", "DataListChoice", "enum_datalist") + +@dataclass +class DataListChoice: + value: str + label: str | None = ... + render_kw: dict[str, Any] = ... + def __post_init__(self) -> None: ... + def __iter__(self): ... + @classmethod + def from_input(cls, input): ... + +def enum_datalist(enum_cls: EnumMeta, *, by: str = "value", label: str | None = None) -> DataList: ... + +class DataList: + widget: _DataListWidget + render_kw: dict[str, Any] + id: str | None + def __init__( + self, choices=None, *, render_kw: dict[str, Any] | None = None, widget: _DataListWidget | None = None + ) -> None: ... + def iter_choices(self, field: Field | None = None) -> list[DataListChoice]: ... + def __call__(self, field: Field | None = None, **kwargs: Any) -> Markup: ... diff --git a/stubs/WTForms/wtforms/fields/__init__.pyi b/stubs/WTForms/wtforms/fields/__init__.pyi index b88edf9d427b..1ffdb69f2d29 100644 --- a/stubs/WTForms/wtforms/fields/__init__.pyi +++ b/stubs/WTForms/wtforms/fields/__init__.pyi @@ -1,8 +1,12 @@ from wtforms.fields.choices import ( + Choice as Choice, RadioField as RadioField, + SelectChoice as SelectChoice, SelectField as SelectField, SelectFieldBase as SelectFieldBase, SelectMultipleField as SelectMultipleField, + enum_choices as enum_choices, + enum_coerce as enum_coerce, ) from wtforms.fields.core import Field as Field, Flags as Flags, Label as Label from wtforms.fields.datetime import ( @@ -24,6 +28,7 @@ from wtforms.fields.numeric import ( ) from wtforms.fields.simple import ( BooleanField as BooleanField, + ButtonField as ButtonField, ColorField as ColorField, EmailField as EmailField, FileField as FileField, @@ -43,10 +48,14 @@ __all__ = [ "Field", "Flags", "Label", + "Choice", + "SelectChoice", "SelectField", - "SelectFieldBase", "SelectMultipleField", + "SelectFieldBase", "RadioField", + "enum_choices", + "enum_coerce", "DateTimeField", "DateField", "TimeField", @@ -59,6 +68,7 @@ __all__ = [ "FloatField", "IntegerRangeField", "DecimalRangeField", + "ButtonField", "BooleanField", "TextAreaField", "PasswordField", diff --git a/stubs/WTForms/wtforms/fields/choices.pyi b/stubs/WTForms/wtforms/fields/choices.pyi index 6ba8d1b40f09..3c6e1b65c6de 100644 --- a/stubs/WTForms/wtforms/fields/choices.pyi +++ b/stubs/WTForms/wtforms/fields/choices.pyi @@ -1,21 +1,51 @@ from collections.abc import Callable, Iterable, Iterator, Sequence -from typing import Any, TypeAlias -from typing_extensions import Self +from dataclasses import dataclass +from enum import Enum, EnumMeta +from typing import Any, Literal, NamedTuple, TypeAlias, TypeVar, overload +from typing_extensions import Self, deprecated +from wtforms.datalist import DataList from wtforms.fields.core import Field, _Filter, _FormT, _Validator, _Widget from wtforms.form import BaseForm from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext -__all__ = ("SelectField", "SelectMultipleField", "RadioField") +__all__ = ("SelectField", "Choice", "SelectChoice", "SelectMultipleField", "RadioField") -# technically this allows a list, but we're more strict for type safety -_Choice: TypeAlias = tuple[Any, str] | tuple[Any, str, dict[str, Any]] +_FieldT = TypeVar("_FieldT", bound=Field) +_LegacyChoice: TypeAlias = tuple[str, str] | tuple[str, str, dict[str, Any]] +_Choice: TypeAlias = SelectChoice | _LegacyChoice # it's too difficult to get type safety here due to to nested partially invariant collections _GroupedChoices: TypeAlias = dict[str, Any] # Any should be Collection[_Choice] -_FullChoice: TypeAlias = tuple[Any, str, bool, dict[str, Any]] # value, label, selected, render_kw +_LegacyChoices: TypeAlias = Iterable[_LegacyChoice] | _GroupedChoices +_LegacyChoicesCallback: TypeAlias = Callable[[], Sequence[_Choice] | _GroupedChoices] +_SelectChoicesCallback: TypeAlias = Callable[[Any, _FieldT], Sequence[SelectChoice]] | Callable[[], Sequence[SelectChoice]] +_FullChoice: TypeAlias = Choice | tuple[Any, str, bool, dict[str, Any]] # value, label, selected, render_kw _FullGroupedChoices: TypeAlias = tuple[str, Iterable[_FullChoice]] _Option: TypeAlias = SelectFieldBase._Option +class Choice(NamedTuple): + value: str + label: str + selected: bool + render_kw: dict[str, Any] + +@dataclass +class SelectChoice: + value: str + label: str = ... + render_kw: dict[str, Any] = ... + optgroup: str | None = ... + def __post_init__(self) -> None: ... + def __iter__(self) -> Iterator[tuple[str, str, dict[str, Any], str | None]]: ... + @classmethod + def from_input(cls, input: SelectChoice | _LegacyChoice | str, optgroup: str | None = None) -> Self: ... + +# we could make these generic, but it's not really worth it +def enum_choices( + enum_cls: EnumMeta, *, by: Literal["name", "value"] = "value", label: Callable[[Any], str] | None = None +) -> list[SelectChoice]: ... +def enum_coerce(enum_cls: EnumMeta, *, by: Literal["name", "value"] = "value") -> Callable[[Any], Enum]: ... + class SelectFieldBase(Field): option_widget: _Widget[_Option] def __init__( @@ -28,17 +58,21 @@ class SelectFieldBase(Field): description: str = "", id: str | None = None, default: object | None = None, + invalid_value_message: str | None = None, widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, _meta: DefaultMeta | None = None, ) -> None: ... def iter_choices(self) -> Iterator[_FullChoice]: ... + def _iter_choices_normalized(self) -> Iterator[Choice]: ... def has_groups(self) -> bool: ... def iter_groups(self) -> Iterator[_FullGroupedChoices]: ... + def _iter_groups_normalized(self) -> Iterator[tuple[str, list[Choice]]]: ... def __iter__(self) -> Iterator[_Option]: ... class _Option(Field): @@ -46,34 +80,117 @@ class SelectFieldBase(Field): class SelectField(SelectFieldBase): coerce: Callable[[Any], Any] - choices: Sequence[_Choice] | _GroupedChoices | None validate_choice: bool + invalid_choice_message: str + + @overload + @deprecated("Starting with WTForms 3.4 only SelectChoice will be supported as choices inputs") def __init__( self, label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, coerce: Callable[[Any], Any] = ..., - choices: Iterable[_Choice] | _GroupedChoices | Callable[[], Iterable[_Choice] | _GroupedChoices] | None = None, + choices: _LegacyChoices | _LegacyChoicesCallback | None = None, validate_choice: bool = True, + invalid_value_message: str | None = None, + invalid_choice_message: str | None = None, *, filters: Sequence[_Filter] = (), description: str = "", id: str | None = None, default: object | None = None, widget: _Widget[Self] | None = None, - option_widget: _Widget[_Option] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, _meta: DefaultMeta | None = None, ) -> None: ... - def iter_choices(self) -> Iterator[_FullChoice]: ... - def has_groups(self) -> bool: ... - def iter_groups(self) -> Iterator[_FullGroupedChoices]: ... + @overload + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + coerce: Callable[[Any], Any] = ..., + choices: Iterable[SelectChoice] | _SelectChoicesCallback[Self] | None = None, + validate_choice: bool = True, + invalid_value_message: str | None = None, + invalid_choice_message: str | None = None, + *, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: object | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + datalist: DataList | str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + + @property + def choices(self) -> Sequence[SelectChoice] | _LegacyChoices: ... + # we would like to have a deprecated overload here as well, but that's not supported + @choices.setter + def choices( + self, value: Iterable[SelectChoice] | _LegacyChoices | _SelectChoicesCallback[Self] | _LegacyChoicesCallback + ) -> None: ... class SelectMultipleField(SelectField): data: list[Any] | None -class RadioField(SelectField): ... +class RadioField(SelectField): + @overload + @deprecated("Starting with WTForms 3.4 only SelectChoice will be supported as choices inputs") + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + *, + coerce: Callable[[Any], Any] = ..., + choices: _LegacyChoices | _LegacyChoicesCallback | None = None, + validate_choice: bool = True, + invalid_value_message: str | None = None, + invalid_choice_message: str | None = None, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: object | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + datalist: DataList | str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + @overload + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + *, + coerce: Callable[[Any], Any] = ..., + choices: Iterable[SelectChoice] | _SelectChoicesCallback[Self] | None = None, + validate_choice: bool = True, + invalid_value_message: str | None = None, + invalid_choice_message: str | None = None, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: object | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + datalist: DataList | str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... diff --git a/stubs/WTForms/wtforms/fields/core.pyi b/stubs/WTForms/wtforms/fields/core.pyi index 101f31fc03c6..44c30572df8d 100644 --- a/stubs/WTForms/wtforms/fields/core.pyi +++ b/stubs/WTForms/wtforms/fields/core.pyi @@ -4,6 +4,7 @@ from typing import Any, Generic, Protocol, TypeAlias, TypeVar, overload, type_ch from typing_extensions import Self from markupsafe import Markup +from wtforms.datalist import DataList from wtforms.form import BaseForm from wtforms.meta import DefaultMeta, _MultiDictLikeWithGetlist, _SupportsGettextAndNgettext @@ -46,6 +47,7 @@ class Field: id: str type: str label: Label + invalid_value_message: str | None # technically this can return UnboundField, but that is not allowed # by type checkers, so we use a descriptor hack to get around this # limitation instead @@ -63,16 +65,19 @@ class Field: description: str = "", id: str | None = None, default: object | None = None, + invalid_value_message: str | None = None, widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, _meta: DefaultMeta | None = None, ) -> None: ... - def __html__(self) -> str: ... - def __call__(self, **kwargs: object) -> Markup: ... + def __html__(self) -> Markup: ... + def __call__(self, **kwargs: Any) -> Markup: ... + def datalist(self, **kwargs: Any) -> Markup: ... @classmethod def check_validators(cls, validators: Iterable[_Validator[_FormT, Self]] | None) -> None: ... def gettext(self, string: str) -> str: ... @@ -83,6 +88,7 @@ class Field: def process( self, formdata: _MultiDictLikeWithGetlist | None, data: Any = ..., extra_filters: Sequence[_Filter] | None = None ) -> None: ... + def post_process(self, formdata: _MultiDictLikeWithGetlist | None = None) -> None: ... def process_data(self, value: Any) -> None: ... def process_formdata(self, valuelist: list[Any]) -> None: ... def populate_obj(self, obj: object, name: str) -> None: ... diff --git a/stubs/WTForms/wtforms/fields/datetime.pyi b/stubs/WTForms/wtforms/fields/datetime.pyi index 426dbfaec91f..95004a228352 100644 --- a/stubs/WTForms/wtforms/fields/datetime.pyi +++ b/stubs/WTForms/wtforms/fields/datetime.pyi @@ -1,14 +1,16 @@ from collections.abc import Callable, Sequence -from datetime import date, datetime, time +from datetime import date, datetime, time, tzinfo from typing import Any -from typing_extensions import Self +from typing_extensions import Self, deprecated +from wtforms.datalist import DataList from wtforms.fields.core import Field, _Filter, _FormT, _Validator, _Widget from wtforms.form import BaseForm from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext __all__ = ("DateTimeField", "DateField", "TimeField", "MonthField", "DateTimeLocalField", "WeekField") +@deprecated("Will be removed in WTForms 3.4 use DateTimeLocalField instead") class DateTimeField(Field): format: list[str] strptime_format: list[str] @@ -19,6 +21,7 @@ class DateTimeField(Field): label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, format: str | list[str] = "%Y-%m-%d %H:%M:%S", + invalid_value_message: str | None = None, *, filters: Sequence[_Filter] = (), description: str = "", @@ -27,6 +30,7 @@ class DateTimeField(Field): widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -41,6 +45,7 @@ class DateField(DateTimeField): label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, format: str | list[str] = "%Y-%m-%d", + invalid_value_message: str | None = None, *, filters: Sequence[_Filter] = (), description: str = "", @@ -49,6 +54,7 @@ class DateField(DateTimeField): widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -63,6 +69,7 @@ class TimeField(DateTimeField): label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, format: str | list[str] = "%H:%M", + invalid_value_message: str | None = None, *, filters: Sequence[_Filter] = (), description: str = "", @@ -71,6 +78,7 @@ class TimeField(DateTimeField): widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -88,9 +96,11 @@ class MonthField(DateField): description: str = "", id: str | None = None, default: time | Callable[[], time] | None = None, + invalid_value_message: str | None = None, widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -103,6 +113,7 @@ class WeekField(DateField): label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, format: str | list[str] = "%Y-W%W", # only difference is the default value + invalid_value_message: str | None = None, *, filters: Sequence[_Filter] = (), description: str = "", @@ -111,6 +122,7 @@ class WeekField(DateField): widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -123,7 +135,9 @@ class DateTimeLocalField(DateTimeField): label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, format: str | list[str] = ..., + invalid_value_message: str | None = None, *, + tz: tzinfo | Callable[[], tzinfo | None] | None = None, filters: Sequence[_Filter] = (), description: str = "", id: str | None = None, @@ -131,6 +145,7 @@ class DateTimeLocalField(DateTimeField): widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, diff --git a/stubs/WTForms/wtforms/fields/form.pyi b/stubs/WTForms/wtforms/fields/form.pyi index 16b7e2016eed..e47a04eaca3d 100644 --- a/stubs/WTForms/wtforms/fields/form.pyi +++ b/stubs/WTForms/wtforms/fields/form.pyi @@ -1,6 +1,7 @@ from collections.abc import Iterator from typing import Any, Generic, TypeVar +from wtforms.datalist import DataList from wtforms.fields.core import Field, _Widget from wtforms.form import BaseForm, _FormErrors from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext @@ -23,9 +24,11 @@ class FormField(Field, Generic[_BoundFormT]): description: str = "", id: str | None = None, default: object | None = None, + invalid_value_message: str | None = None, widget: _Widget[FormField[_BoundFormT]] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, diff --git a/stubs/WTForms/wtforms/fields/list.pyi b/stubs/WTForms/wtforms/fields/list.pyi index 2142009c810d..48b66b20b03c 100644 --- a/stubs/WTForms/wtforms/fields/list.pyi +++ b/stubs/WTForms/wtforms/fields/list.pyi @@ -1,6 +1,7 @@ from collections.abc import Callable, Iterable, Iterator, Sequence from typing import Any, Generic, TypeVar +from wtforms.datalist import DataList from wtforms.fields.core import Field, UnboundField, _FormT, _Validator, _Widget from wtforms.form import BaseForm from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext @@ -34,16 +35,19 @@ class FieldList(Field, Generic[_BoundFieldT]): *, description: str = "", id: str | None = None, + invalid_value_message: str | None = None, widget: _Widget[FieldList[Any]] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, _meta: DefaultMeta | None = None, ) -> None: ... def append_entry(self, data: Any = ...) -> _BoundFieldT: ... - def pop_entry(self) -> _BoundFieldT: ... + def insert_entry(self, index: int, data: Any = ...) -> _BoundFieldT: ... + def pop_entry(self, index: int = -1) -> _BoundFieldT: ... def __iter__(self) -> Iterator[_BoundFieldT]: ... def __len__(self) -> int: ... def __getitem__(self, index: int) -> _BoundFieldT: ... diff --git a/stubs/WTForms/wtforms/fields/numeric.pyi b/stubs/WTForms/wtforms/fields/numeric.pyi index e88aad5a7c92..a8ae1ef3af2c 100644 --- a/stubs/WTForms/wtforms/fields/numeric.pyi +++ b/stubs/WTForms/wtforms/fields/numeric.pyi @@ -3,6 +3,7 @@ from decimal import Decimal from typing import Any, Literal, overload from typing_extensions import Self +from wtforms.datalist import DataList from wtforms.fields.core import Field, _Filter, _FormT, _Validator, _Widget from wtforms.form import BaseForm from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext @@ -28,9 +29,11 @@ class LocaleAwareNumberField(Field): description: str = "", id: str | None = None, default: object | None = None, + invalid_value_message: str | None = None, widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -46,6 +49,7 @@ class IntegerField(Field): self, label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + invalid_value_message: str | None = None, *, filters: Sequence[_Filter] = (), description: str = "", @@ -54,6 +58,7 @@ class IntegerField(Field): widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -76,6 +81,7 @@ class DecimalField(LocaleAwareNumberField): *, places: UnsetValue = ..., rounding: None = None, + invalid_value_message: str | None = None, use_locale: Literal[True], # this accepts a babel.numbers.NumberPattern, but since it # is an optional dependency we don't want to depend on it @@ -88,6 +94,7 @@ class DecimalField(LocaleAwareNumberField): widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -100,6 +107,7 @@ class DecimalField(LocaleAwareNumberField): validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, places: int | UnsetValue | None = ..., rounding: str | None = None, + invalid_value_message: str | None = None, *, use_locale: Literal[False] = False, # this accepts a babel.numbers.NumberPattern, but since it @@ -113,6 +121,7 @@ class DecimalField(LocaleAwareNumberField): widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -128,6 +137,7 @@ class FloatField(Field): self, label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + invalid_value_message: str | None = None, *, filters: Sequence[_Filter] = (), description: str = "", @@ -136,6 +146,7 @@ class FloatField(Field): widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, diff --git a/stubs/WTForms/wtforms/fields/simple.pyi b/stubs/WTForms/wtforms/fields/simple.pyi index 13bc3f4ed45d..4278b0572878 100644 --- a/stubs/WTForms/wtforms/fields/simple.pyi +++ b/stubs/WTForms/wtforms/fields/simple.pyi @@ -2,11 +2,13 @@ from collections.abc import Callable, Collection, Sequence from typing import Any from typing_extensions import Self +from wtforms.datalist import DataList from wtforms.fields.core import Field, _Filter, _FormT, _Validator, _Widget from wtforms.form import BaseForm from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext __all__ = ( + "ButtonField", "BooleanField", "TextAreaField", "PasswordField", @@ -36,9 +38,11 @@ class BooleanField(Field): description: str = "", id: str | None = None, default: bool | Callable[[], bool] | None = None, + invalid_value_message: str | None = None, widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, @@ -56,15 +60,18 @@ class StringField(Field): description: str = "", id: str | None = None, default: str | Callable[[], str] | None = None, + invalid_value_message: str | None = None, widget: _Widget[Self] | None = None, render_kw: dict[str, Any] | None = None, name: str | None = None, + datalist: DataList | str | None = None, _form: BaseForm | None = None, _prefix: str = "", _translations: _SupportsGettextAndNgettext | None = None, _meta: DefaultMeta | None = None, ) -> None: ... +class ButtonField(StringField): ... class TextAreaField(StringField): ... class PasswordField(StringField): ... class FileField(Field): ... diff --git a/stubs/WTForms/wtforms/form.pyi b/stubs/WTForms/wtforms/form.pyi index a8337d394696..986472a280d1 100644 --- a/stubs/WTForms/wtforms/form.pyi +++ b/stubs/WTForms/wtforms/form.pyi @@ -46,6 +46,7 @@ class BaseForm: extra_filters: Mapping[str, Sequence[Any]] | None = None, **kwargs: object, ) -> None: ... + def post_process(self, formdata: _MultiDictLike | None = None) -> None: ... # same thing here with extra_validators def validate(self, extra_validators: Mapping[str, Sequence[Any]] | None = None) -> bool: ... @property diff --git a/stubs/WTForms/wtforms/validators.pyi b/stubs/WTForms/wtforms/validators.pyi index 0389fcb38779..6a8304641222 100644 --- a/stubs/WTForms/wtforms/validators.pyi +++ b/stubs/WTForms/wtforms/validators.pyi @@ -1,4 +1,5 @@ from collections.abc import Callable, Collection, Iterable +from datetime import date, datetime from decimal import Decimal from re import Match, Pattern from typing import Any, TypeVar, overload @@ -20,7 +21,9 @@ __all__ = ( "Length", "length", "NumberRange", + "DateRange", "number_range", + "date_range", "Optional", "optional", "Regexp", @@ -78,6 +81,19 @@ class NumberRange: # numeric fields def __call__(self, form: BaseForm, field: Field) -> None: ... +class DateRange: + min: date | datetime | Callable[[], date | datetime] | None + max: date | datetime | Callable[[], date | datetime] | None + message: str + field_flags: dict[str, Any] + def __init__( + self, + min: date | datetime | Callable[[], date | datetime] | None = None, + max: date | datetime | Callable[[], date | datetime] | None = None, + message: str | None = None, + ) -> None: ... + def __call__(self, form: BaseForm, field: Field) -> None: ... + class Optional: string_check: Callable[[str], bool] field_flags: dict[str, Any] @@ -99,22 +115,33 @@ class InputRequired: class Regexp: regex: Pattern[str] message: str | None - def __init__(self, regex: str | Pattern[str], flags: int = 0, message: str | None = None) -> None: ... + matcher: Callable[[Pattern[str], str], Match[str]] + field_flags: dict[str, Any] + def __init__( + self, + regex: str | Pattern[str], + flags: int = 0, + message: str | None = None, + matcher: Callable[[Pattern[str], str], Match[str]] = ..., + html_pattern: bool | str | Callable[[Pattern[str]], bool | str] = False, + ) -> None: ... def __call__(self, form: BaseForm, field: StringField, message: str | None = None) -> Match[str]: ... class Email: message: str | None granular_message: bool check_deliverability: bool - allow_smtputf8: bool - allow_empty_local: bool + test_environment: bool | None + allow_smtputf8: bool | None + allow_empty_local: bool | None def __init__( self, message: str | None = None, granular_message: bool = False, check_deliverability: bool = False, - allow_smtputf8: bool = True, - allow_empty_local: bool = False, + test_environment: bool | None = None, + allow_smtputf8: bool | None = None, + allow_empty_local: bool | None = None, ) -> None: ... def __call__(self, form: BaseForm, field: StringField) -> None: ... @@ -133,9 +160,19 @@ class MacAddress(Regexp): def __init__(self, message: str | None = None) -> None: ... def __call__(self, form: BaseForm, field: StringField) -> None: ... # type: ignore[override] -class URL(Regexp): +class URL: + allow_userinfo: bool + schemes: Collection[str] + message: str | None validate_hostname: HostnameValidation - def __init__(self, require_tld: bool = True, allow_ip: bool = True, message: str | None = None) -> None: ... + def __init__( + self, + require_tld: bool = True, + allow_ip: bool = True, + allow_userinfo: bool = False, + schemes: Collection[str] = ..., + message: str | None = None, + ) -> None: ... def __call__(self, form: BaseForm, field: StringField) -> None: ... # type: ignore[override] class UUID: @@ -203,6 +240,7 @@ ip_address = IPAddress mac_address = MacAddress length = Length number_range = NumberRange +date_range = DateRange optional = Optional input_required = InputRequired data_required = DataRequired diff --git a/stubs/WTForms/wtforms/widgets/__init__.pyi b/stubs/WTForms/wtforms/widgets/__init__.pyi index 919a43b3e49a..9641efbafa0c 100644 --- a/stubs/WTForms/wtforms/widgets/__init__.pyi +++ b/stubs/WTForms/wtforms/widgets/__init__.pyi @@ -1,6 +1,8 @@ from wtforms.widgets.core import ( + Button as Button, CheckboxInput as CheckboxInput, ColorInput as ColorInput, + DataListWidget as DataListWidget, DateInput as DateInput, DateTimeInput as DateTimeInput, DateTimeLocalInput as DateTimeLocalInput, @@ -29,8 +31,10 @@ from wtforms.widgets.core import ( ) __all__ = [ + "Button", "CheckboxInput", "ColorInput", + "DataListWidget", "DateInput", "DateTimeInput", "DateTimeLocalInput", diff --git a/stubs/WTForms/wtforms/widgets/core.pyi b/stubs/WTForms/wtforms/widgets/core.pyi index 62861c43cb66..248f1fed57ff 100644 --- a/stubs/WTForms/wtforms/widgets/core.pyi +++ b/stubs/WTForms/wtforms/widgets/core.pyi @@ -2,12 +2,15 @@ from decimal import Decimal from typing import Any, Literal from markupsafe import Markup +from wtforms.datalist import DataList from wtforms.fields import Field, FormField, StringField -from wtforms.fields.choices import SelectFieldBase, _Option +from wtforms.fields.choices import Choice, SelectFieldBase, _Option __all__ = ( + "Button", "CheckboxInput", "ColorInput", + "DataListWidget", "DateInput", "DateTimeInput", "DateTimeLocalInput", @@ -49,6 +52,9 @@ class TableWidget: def __init__(self, with_table_tag: bool = True) -> None: ... def __call__(self, field: FormField[Any], **kwargs: object) -> Markup: ... +class DataListWidget: + def __call__(self, datalist: DataList, field: Field | None = None, **kwargs: Any) -> Markup: ... + class Input: validation_attrs: list[str] input_type: str @@ -57,6 +63,14 @@ class Input: @staticmethod def html_params(**kwargs: object) -> str: ... +class Button: + input_type: str + validation_attrs: list[str] + def __init__(self, input_type=None) -> None: ... + def __call__(self, field: Field, **kwargs: Any) -> Markup: ... + @staticmethod + def html_params(**kwargs: object) -> str: ... + class TextInput(Input): ... class PasswordInput(Input): @@ -85,7 +99,7 @@ class Select: def __init__(self, multiple: bool = False) -> None: ... def __call__(self, field: SelectFieldBase, **kwargs: object) -> Markup: ... @classmethod - def render_option(cls, value: object, label: str, selected: bool, **kwargs: object) -> Markup: ... + def render_option(cls, choice: Choice, **kwargs: object) -> Markup: ... class Option: def __call__(self, field: _Option, **kwargs: object) -> Markup: ... @@ -112,9 +126,5 @@ class NumberInput(Input): max: Decimal | float | str | None = None, ) -> None: ... -class RangeInput(Input): - # maybe we should allow any str for this - step: Decimal | float | str | None - def __init__(self, step: Decimal | float | str | None = None) -> None: ... - +class RangeInput(Input): ... class ColorInput(Input): ... From 2ff3e9f8f256731a25f272a4a1f67e560ea08781 Mon Sep 17 00:00:00 2001 From: Daverball Date: Sat, 24 Oct 2026 00:21:19 +0200 Subject: [PATCH 2/5] Fixes regressions --- stubs/WTForms/wtforms/datalist.pyi | 8 ++++++-- stubs/WTForms/wtforms/fields/choices.pyi | 6 +++++- stubs/WTForms/wtforms/widgets/core.pyi | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/stubs/WTForms/wtforms/datalist.pyi b/stubs/WTForms/wtforms/datalist.pyi index ad7297d95d2f..333b2114fc5b 100644 --- a/stubs/WTForms/wtforms/datalist.pyi +++ b/stubs/WTForms/wtforms/datalist.pyi @@ -1,6 +1,8 @@ +from collections.abc import Iterator from dataclasses import dataclass from enum import EnumMeta from typing import Any, Protocol, type_check_only +from typing_extensions import Self from markupsafe import Markup from wtforms.fields import Field @@ -17,9 +19,11 @@ class DataListChoice: label: str | None = ... render_kw: dict[str, Any] = ... def __post_init__(self) -> None: ... - def __iter__(self): ... + def __iter__(self) -> Iterator[tuple[str, str, dict[str, Any]]]: ... @classmethod - def from_input(cls, input): ... + def from_input( + cls, input: DataListChoice | str | tuple[str] | tuple[str, str | None] | tuple[str, str | None, dict[str, Any]] + ) -> Self: ... def enum_datalist(enum_cls: EnumMeta, *, by: str = "value", label: str | None = None) -> DataList: ... diff --git a/stubs/WTForms/wtforms/fields/choices.pyi b/stubs/WTForms/wtforms/fields/choices.pyi index 3c6e1b65c6de..4cf200a08d05 100644 --- a/stubs/WTForms/wtforms/fields/choices.pyi +++ b/stubs/WTForms/wtforms/fields/choices.pyi @@ -12,7 +12,7 @@ from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext __all__ = ("SelectField", "Choice", "SelectChoice", "SelectMultipleField", "RadioField") _FieldT = TypeVar("_FieldT", bound=Field) -_LegacyChoice: TypeAlias = tuple[str, str] | tuple[str, str, dict[str, Any]] +_LegacyChoice: TypeAlias = tuple[Any, str] | tuple[Any, str, dict[str, Any]] _Choice: TypeAlias = SelectChoice | _LegacyChoice # it's too difficult to get type safety here due to to nested partially invariant collections _GroupedChoices: TypeAlias = dict[str, Any] # Any should be Collection[_Choice] @@ -95,6 +95,7 @@ class SelectField(SelectFieldBase): invalid_value_message: str | None = None, invalid_choice_message: str | None = None, *, + option_widget: _Widget[_Option] | None = None, filters: Sequence[_Filter] = (), description: str = "", id: str | None = None, @@ -119,6 +120,7 @@ class SelectField(SelectFieldBase): invalid_value_message: str | None = None, invalid_choice_message: str | None = None, *, + option_widget: _Widget[_Option] | None = None, filters: Sequence[_Filter] = (), description: str = "", id: str | None = None, @@ -152,6 +154,7 @@ class RadioField(SelectField): label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, *, + option_widget: _Widget[_Option] | None = None, coerce: Callable[[Any], Any] = ..., choices: _LegacyChoices | _LegacyChoicesCallback | None = None, validate_choice: bool = True, @@ -176,6 +179,7 @@ class RadioField(SelectField): label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, *, + option_widget: _Widget[_Option] | None = None, coerce: Callable[[Any], Any] = ..., choices: Iterable[SelectChoice] | _SelectChoicesCallback[Self] | None = None, validate_choice: bool = True, diff --git a/stubs/WTForms/wtforms/widgets/core.pyi b/stubs/WTForms/wtforms/widgets/core.pyi index 248f1fed57ff..e3ef12674d49 100644 --- a/stubs/WTForms/wtforms/widgets/core.pyi +++ b/stubs/WTForms/wtforms/widgets/core.pyi @@ -66,7 +66,7 @@ class Input: class Button: input_type: str validation_attrs: list[str] - def __init__(self, input_type=None) -> None: ... + def __init__(self, input_type: str | None = None) -> None: ... def __call__(self, field: Field, **kwargs: Any) -> Markup: ... @staticmethod def html_params(**kwargs: object) -> str: ... From ec18d85cbc5d36047780be7202a459873c16a1ef Mon Sep 17 00:00:00 2001 From: Daverball Date: Sat, 24 Oct 2026 00:33:00 +0200 Subject: [PATCH 3/5] Adds final missing type hint --- stubs/WTForms/wtforms/datalist.pyi | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/stubs/WTForms/wtforms/datalist.pyi b/stubs/WTForms/wtforms/datalist.pyi index 333b2114fc5b..4e87b34b81df 100644 --- a/stubs/WTForms/wtforms/datalist.pyi +++ b/stubs/WTForms/wtforms/datalist.pyi @@ -1,12 +1,15 @@ -from collections.abc import Iterator +from collections.abc import Callable, Iterator, Sequence from dataclasses import dataclass from enum import EnumMeta -from typing import Any, Protocol, type_check_only +from typing import Any, Protocol, TypeAlias, type_check_only from typing_extensions import Self from markupsafe import Markup from wtforms.fields import Field +_RawDataListChoice: TypeAlias = str | tuple[str] | tuple[str, str | None] | tuple[str, str | None, dict[str, Any]] +_DataListChoices: TypeAlias = Sequence[DataListChoice | _RawDataListChoice] | dict[str, str] + @type_check_only class _DataListWidget(Protocol): def __call__(self, datalist: DataList, /, *, field: Field | None = None, **kwargs: Any) -> Markup: ... @@ -21,9 +24,7 @@ class DataListChoice: def __post_init__(self) -> None: ... def __iter__(self) -> Iterator[tuple[str, str, dict[str, Any]]]: ... @classmethod - def from_input( - cls, input: DataListChoice | str | tuple[str] | tuple[str, str | None] | tuple[str, str | None, dict[str, Any]] - ) -> Self: ... + def from_input(cls, input: DataListChoice | _RawDataListChoice) -> Self: ... def enum_datalist(enum_cls: EnumMeta, *, by: str = "value", label: str | None = None) -> DataList: ... @@ -32,7 +33,11 @@ class DataList: render_kw: dict[str, Any] id: str | None def __init__( - self, choices=None, *, render_kw: dict[str, Any] | None = None, widget: _DataListWidget | None = None + self, + choices: _DataListChoices | Callable[[Any, Any], _DataListChoices] | Callable[[], _DataListChoices] | None = None, + *, + render_kw: dict[str, Any] | None = None, + widget: _DataListWidget | None = None, ) -> None: ... def iter_choices(self, field: Field | None = None) -> list[DataListChoice]: ... def __call__(self, field: Field | None = None, **kwargs: Any) -> Markup: ... From 52ed7f813ac9c2ea1451e312a9798af1a7922cd5 Mon Sep 17 00:00:00 2001 From: Daverball Date: Sat, 24 Oct 2026 08:18:23 +0200 Subject: [PATCH 4/5] Updates choices regression tests --- .../@tests/test_cases/check_choices.py | 145 ++++++++++++------ stubs/WTForms/wtforms/fields/choices.pyi | 36 ++++- 2 files changed, 133 insertions(+), 48 deletions(-) diff --git a/stubs/WTForms/@tests/test_cases/check_choices.py b/stubs/WTForms/@tests/test_cases/check_choices.py index 41204023ad25..53955ceb2f94 100644 --- a/stubs/WTForms/@tests/test_cases/check_choices.py +++ b/stubs/WTForms/@tests/test_cases/check_choices.py @@ -1,67 +1,122 @@ from __future__ import annotations -from wtforms import SelectField +from enum import Enum, auto +from wtforms import Field, Form, SelectChoice, SelectField, StringField +from wtforms.fields import enum_choices, enum_coerce + +# construct SelectChoice from old inputs +SelectChoice.from_input("") +SelectChoice.from_input(("", "")) +SelectChoice.from_input(("", "", {})) +# non-str values are no longer allowed/encouraged +# technically they never were, it just never was enforced +# coerce(value) is supposed to yield the object that gets stored in data +# so values in choices are intended to be pre-serialized to str in the +# format coerce expects +SelectChoice.from_input((1, "1")) # type: ignore[arg-type] +SelectChoice.from_input((True, "t", {})) # type: ignore[arg-type] +SelectChoice.from_input((False, "f", {})) # type: ignore[arg-type] + +# provide choices as any iterable of SelectChoice +SelectField(choices=[SelectChoice("")]) + +# new style of grouped choices +SelectField(choices=[SelectChoice("A", optgroup="a"), SelectChoice("B", optgroup="b")]) + +# provide a callback without arguments that generates a sequence of SelectChoice +SelectField(choices=lambda: [SelectChoice("")]) + + +# or a callback that receives the form as its first and the field as its second argument +def generate_choices(form: Form, field: SelectField) -> list[SelectChoice]: + return [] + + +def generate_choices_generous_field_type(form: Form, field: Field) -> list[SelectChoice]: + return [] + + +def generate_choices_wrong_field_type(form: Form, field: StringField) -> list[SelectChoice]: + return [] + + +SelectField(choices=lambda form, field: [SelectChoice("")]) +SelectField(choices=generate_choices) +SelectField(choices=generate_choices_generous_field_type) +SelectField(choices=generate_choices_wrong_field_type) # type: ignore + + +# provide choices based on enum +class TestEnum(Enum): + foo = auto() + bar = auto() + baz = auto() + + +SelectField(choices=enum_choices(TestEnum), coerce=enum_coerce(TestEnum)) + +# deprecated ways of providing choices: # any way we can specify the choices inline with a literal should work # tuple of tuples -SelectField(choices=(("", ""),)) -SelectField(choices=((1, "1"),)) -SelectField(choices=(("", "", {}),)) -SelectField(choices=((True, "t", {}),)) -SelectField(choices=((True, "t"), (False, "f", {}))) +SelectField(choices=(("", ""),)) # pyright: ignore[reportDeprecated] +SelectField(choices=((1, "1"),)) # pyright: ignore[reportDeprecated] +SelectField(choices=(("", "", {}),)) # pyright: ignore[reportDeprecated] +SelectField(choices=((True, "t", {}),)) # pyright: ignore[reportDeprecated] +SelectField(choices=((True, "t"), (False, "f", {}))) # pyright: ignore[reportDeprecated] # list of tuples -SelectField(choices=[("", "")]) -SelectField(choices=[(1, "1")]) -SelectField(choices=[("", "", {})]) -SelectField(choices=[(True, "t", {})]) -SelectField(choices=[(True, "t"), (False, "f", {})]) +SelectField(choices=[("", "")]) # pyright: ignore[reportDeprecated] +SelectField(choices=[(1, "1")]) # pyright: ignore[reportDeprecated] +SelectField(choices=[("", "", {})]) # pyright: ignore[reportDeprecated] +SelectField(choices=[(True, "t", {})]) # pyright: ignore[reportDeprecated] +SelectField(choices=[(True, "t"), (False, "f", {})]) # pyright: ignore[reportDeprecated] # dict of tuple of tuples -SelectField(choices={"a": (("", ""),)}) -SelectField(choices={"a": ((1, "1"),)}) -SelectField(choices={"a": (("", "", {}),)}) -SelectField(choices={"a": ((True, "t", {}),)}) -SelectField(choices={"a": ((True, "t"), (False, "f", {}))}) -SelectField(choices={"a": ((True, "", {}),), "b": ((False, "f"),)}) +SelectField(choices={"a": (("", ""),)}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": ((1, "1"),)}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": (("", "", {}),)}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": ((True, "t", {}),)}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": ((True, "t"), (False, "f", {}))}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": ((True, "", {}),), "b": ((False, "f"),)}) # pyright: ignore[reportDeprecated] # dict of list of tuples -SelectField(choices={"a": [("", "")]}) -SelectField(choices={"a": [(1, "1")]}) -SelectField(choices={"a": [("", "", {})]}) -SelectField(choices={"a": [(True, "t", {})]}) -SelectField(choices={"a": [(True, "t"), (False, "f", {})]}) -SelectField(choices={"a": [(True, "", {})], "b": [(False, "f")]}) +SelectField(choices={"a": [("", "")]}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": [(1, "1")]}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": [("", "", {})]}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": [(True, "t", {})]}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": [(True, "t"), (False, "f", {})]}) # pyright: ignore[reportDeprecated] +SelectField(choices={"a": [(True, "", {})], "b": [(False, "f")]}) # pyright: ignore[reportDeprecated] # the same should be true for lambdas # tuple of tuples -SelectField(choices=lambda: (("", ""),)) -SelectField(choices=lambda: ((1, "1"),)) -SelectField(choices=lambda: (("", "", {}),)) -SelectField(choices=lambda: ((True, "t", {}),)) -SelectField(choices=lambda: ((True, "t"), (False, "f", {}))) +SelectField(choices=lambda: (("", ""),)) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: ((1, "1"),)) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: (("", "", {}),)) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: ((True, "t", {}),)) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: ((True, "t"), (False, "f", {}))) # pyright: ignore[reportDeprecated] # list of tuples -SelectField(choices=lambda: [("", "")]) -SelectField(choices=lambda: [(1, "1")]) -SelectField(choices=lambda: [("", "", {})]) -SelectField(choices=lambda: [(True, "t", {})]) -SelectField(choices=lambda: [(True, "t"), (False, "f", {})]) +SelectField(choices=lambda: [("", "")]) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: [(1, "1")]) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: [("", "", {})]) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: [(True, "t", {})]) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: [(True, "t"), (False, "f", {})]) # pyright: ignore[reportDeprecated] # dict of tuple of tuples -SelectField(choices=lambda: {"a": (("", ""),)}) -SelectField(choices=lambda: {"a": ((1, "1"),)}) -SelectField(choices=lambda: {"a": (("", "", {}),)}) -SelectField(choices=lambda: {"a": ((True, "t", {}),)}) -SelectField(choices=lambda: {"a": ((True, "t"), (False, "f", {}))}) -SelectField(choices=lambda: {"a": ((True, "", {}),), "b": ((False, "f"),)}) +SelectField(choices=lambda: {"a": (("", ""),)}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": ((1, "1"),)}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": (("", "", {}),)}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": ((True, "t", {}),)}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": ((True, "t"), (False, "f", {}))}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": ((True, "", {}),), "b": ((False, "f"),)}) # pyright: ignore[reportDeprecated] # dict of list of tuples -SelectField(choices=lambda: {"a": [("", "")]}) -SelectField(choices=lambda: {"a": [(1, "1")]}) -SelectField(choices=lambda: {"a": [("", "", {})]}) -SelectField(choices=lambda: {"a": [(True, "t", {})]}) -SelectField(choices=lambda: {"a": [(True, "t"), (False, "f", {})]}) -SelectField(choices=lambda: {"a": [(True, "", {})], "b": [(False, "f")]}) +SelectField(choices=lambda: {"a": [("", "")]}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": [(1, "1")]}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": [("", "", {})]}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": [(True, "t", {})]}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": [(True, "t"), (False, "f", {})]}) # pyright: ignore[reportDeprecated] +SelectField(choices=lambda: {"a": [(True, "", {})], "b": [(False, "f")]}) # pyright: ignore[reportDeprecated] diff --git a/stubs/WTForms/wtforms/fields/choices.pyi b/stubs/WTForms/wtforms/fields/choices.pyi index 4cf200a08d05..26b293b7483e 100644 --- a/stubs/WTForms/wtforms/fields/choices.pyi +++ b/stubs/WTForms/wtforms/fields/choices.pyi @@ -38,7 +38,11 @@ class SelectChoice: def __post_init__(self) -> None: ... def __iter__(self) -> Iterator[tuple[str, str, dict[str, Any], str | None]]: ... @classmethod - def from_input(cls, input: SelectChoice | _LegacyChoice | str, optgroup: str | None = None) -> Self: ... + def from_input( + cls, + input: SelectChoice | tuple[str, str | None] | tuple[str, str | None, dict[str, Any]] | str, + optgroup: str | None = None, + ) -> Self: ... # we could make these generic, but it's not really worth it def enum_choices( @@ -83,6 +87,32 @@ class SelectField(SelectFieldBase): validate_choice: bool invalid_choice_message: str + @overload + @deprecated("Starting with WTForms 3.4 only SelectChoice will be supported as choices inputs") + def __init__( + self, + label: str | None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None, + coerce: Callable[[Any], Any], + choices: _LegacyChoices | _LegacyChoicesCallback, + validate_choice: bool = True, + invalid_value_message: str | None = None, + invalid_choice_message: str | None = None, + *, + option_widget: _Widget[_Option] | None = None, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: object | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + datalist: DataList | str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... @overload @deprecated("Starting with WTForms 3.4 only SelectChoice will be supported as choices inputs") def __init__( @@ -90,11 +120,11 @@ class SelectField(SelectFieldBase): label: str | None = None, validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, coerce: Callable[[Any], Any] = ..., - choices: _LegacyChoices | _LegacyChoicesCallback | None = None, + *, + choices: _LegacyChoices | _LegacyChoicesCallback, validate_choice: bool = True, invalid_value_message: str | None = None, invalid_choice_message: str | None = None, - *, option_widget: _Widget[_Option] | None = None, filters: Sequence[_Filter] = (), description: str = "", From 9de75f76483d549ce36585c4cf112d9838a264f3 Mon Sep 17 00:00:00 2001 From: Daverball Date: Sat, 24 Oct 2026 08:23:33 +0200 Subject: [PATCH 5/5] Fixes overly-broad `_LegacyCallback` return type --- stubs/WTForms/wtforms/fields/choices.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stubs/WTForms/wtforms/fields/choices.pyi b/stubs/WTForms/wtforms/fields/choices.pyi index 26b293b7483e..3765842abd92 100644 --- a/stubs/WTForms/wtforms/fields/choices.pyi +++ b/stubs/WTForms/wtforms/fields/choices.pyi @@ -17,7 +17,7 @@ _Choice: TypeAlias = SelectChoice | _LegacyChoice # it's too difficult to get type safety here due to to nested partially invariant collections _GroupedChoices: TypeAlias = dict[str, Any] # Any should be Collection[_Choice] _LegacyChoices: TypeAlias = Iterable[_LegacyChoice] | _GroupedChoices -_LegacyChoicesCallback: TypeAlias = Callable[[], Sequence[_Choice] | _GroupedChoices] +_LegacyChoicesCallback: TypeAlias = Callable[[], Sequence[_LegacyChoice] | _GroupedChoices] _SelectChoicesCallback: TypeAlias = Callable[[Any, _FieldT], Sequence[SelectChoice]] | Callable[[], Sequence[SelectChoice]] _FullChoice: TypeAlias = Choice | tuple[Any, str, bool, dict[str, Any]] # value, label, selected, render_kw _FullGroupedChoices: TypeAlias = tuple[str, Iterable[_FullChoice]] @@ -166,7 +166,7 @@ class SelectField(SelectFieldBase): ) -> None: ... @property - def choices(self) -> Sequence[SelectChoice] | _LegacyChoices: ... + def choices(self) -> Sequence[_Choice]: ... # we would like to have a deprecated overload here as well, but that's not supported @choices.setter def choices(