diff --git a/stubs/WTForms/@tests/stubtest_allowlist.txt b/stubs/WTForms/@tests/stubtest_allowlist.txt new file mode 100644 index 000000000000..18057b0c2330 --- /dev/null +++ b/stubs/WTForms/@tests/stubtest_allowlist.txt @@ -0,0 +1,41 @@ +# Error: is not present at runtime +# ============================= +# This is hack to get around Field.__new__ not being able to return +# UnboundField +wtforms.Field.__get__ +wtforms.fields.Field.__get__ +wtforms.fields.core.Field.__get__ +# Since DefaultMeta can contain arbitrary values we added __getattr__ +# to let mypy know that arbitrary attribute access is possible +wtforms.meta.DefaultMeta.__getattr__ + +# Error: variable differs from runtime +# ====================== +# _unbound_fields has some weird semantics: due to the metaclass it +# will be None until the form class has been instantiated at least +# once and then will stick around until someone adds a new field +# to the class, which clears it back to None. Which means on instances +# it will always be there and on the class it depends, so maybe this +# should use a dummy descriptor? For now we just pretend it's set. +# The behavior is documented in FormMeta, so I think it's fine. +wtforms.Form._unbound_fields +wtforms.form.Form._unbound_fields + +# widget is both used as a ClassVar and instance variable and does +# not necessarily reflect an upper bound on Widget, so we always use +# our Widget Protocol definition that's contravariant on Self +wtforms.Field.widget +wtforms.FormField.widget +wtforms.SelectField.widget +wtforms.SelectMultipleField.widget +wtforms.TextAreaField.widget +wtforms.fields.Field.widget +wtforms.fields.FormField.widget +wtforms.fields.SelectField.widget +wtforms.fields.SelectMultipleField.widget +wtforms.fields.TextAreaField.widget +wtforms.fields.choices.SelectField.widget +wtforms.fields.choices.SelectMultipleField.widget +wtforms.fields.core.Field.widget +wtforms.fields.form.FormField.widget +wtforms.fields.simple.TextAreaField.widget diff --git a/stubs/WTForms/@tests/test_cases/check_filters.py b/stubs/WTForms/@tests/test_cases/check_filters.py new file mode 100644 index 000000000000..2786e3b20f0b --- /dev/null +++ b/stubs/WTForms/@tests/test_cases/check_filters.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from wtforms import Field, Form + + +class Filter1: + def __call__(self, value: object) -> None: + ... + + +class Filter2: + def __call__(self, input: None) -> None: + ... + + +def not_a_filter(a: object, b: object) -> None: + ... + + +def also_not_a_filter() -> None: + ... + + +# we should accept any mapping of sequences, we can't really validate +# the filter functions when it's this nested +form = Form() +form.process(extra_filters={"foo": (str.upper, str.strip, int), "bar": (Filter1(), Filter2())}) +form.process(extra_filters={"foo": [str.upper, str.strip, int], "bar": [Filter1(), Filter2()]}) + +# regardless of how we pass the filters into Field it should work +field = Field(filters=(str.upper, str.lower, int)) +Field(filters=(Filter1(), Filter2())) +Field(filters=[str.upper, str.lower, int]) +Field(filters=[Filter1(), Filter2()]) +field.process(None, extra_filters=(str.upper, str.lower, int)) +field.process(None, extra_filters=(Filter1(), Filter2())) +field.process(None, extra_filters=[str.upper, str.lower, int]) +field.process(None, extra_filters=[Filter1(), Filter2()]) + +# but if we pass in some callables with an incompatible param spec +# then we should get type errors +Field(filters=(str.upper, str.lower, int, not_a_filter)) # type:ignore +Field(filters=(Filter1(), Filter2(), also_not_a_filter)) # type:ignore +Field(filters=[str.upper, str.lower, int, also_not_a_filter]) # type:ignore +Field(filters=[Filter1(), Filter2(), not_a_filter]) # type:ignore +field.process(None, extra_filters=(str.upper, str.lower, int, not_a_filter)) # type:ignore +field.process(None, extra_filters=(Filter1(), Filter2(), also_not_a_filter)) # type:ignore +field.process(None, extra_filters=[str.upper, str.lower, int, also_not_a_filter]) # type:ignore +field.process(None, extra_filters=[Filter1(), Filter2(), not_a_filter]) # type:ignore diff --git a/stubs/WTForms/@tests/test_cases/check_validators.py b/stubs/WTForms/@tests/test_cases/check_validators.py new file mode 100644 index 000000000000..ba89d64fdd2f --- /dev/null +++ b/stubs/WTForms/@tests/test_cases/check_validators.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from wtforms import DateField, Field, Form, StringField +from wtforms.validators import Email, Optional + +form = Form() +# on form we should accept any validator mapping +form.validate({"field": (Optional(),), "string_field": (Optional(), Email())}) +form.validate({"field": [Optional()], "string_field": [Optional(), Email()]}) + +# both StringField validators and Field validators should be valid +# as inputs on a StringField +string_field = StringField(validators=(Optional(), Email())) +string_field.validate(form, (Optional(), Email())) + +# but not on Field +field = Field(validators=(Optional(), Email())) # type:ignore +field.validate(form, (Optional(), Email())) # type:ignore + +# unless we only pass the Field validator +Field(validators=(Optional(),)) +field.validate(form, (Optional(),)) + +# DateField should accept Field validators but not StringField validators +date_field = DateField(validators=(Optional(), Email())) # type:ignore +date_field.validate(form, (Optional(), Email())) # type:ignore +DateField(validators=(Optional(),)) + +# for lists we can't be as strict so we won't get type errors here +Field(validators=[Optional(), Email()]) +field.validate(form, [Optional(), Email()]) +DateField(validators=[Optional(), Email()]) +date_field.validate(form, [Optional(), Email()]) diff --git a/stubs/WTForms/@tests/test_cases/check_widgets.py b/stubs/WTForms/@tests/test_cases/check_widgets.py new file mode 100644 index 000000000000..d5914ceff442 --- /dev/null +++ b/stubs/WTForms/@tests/test_cases/check_widgets.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from wtforms import Field, FieldList, Form, FormField, SelectField, StringField +from wtforms.widgets import Input, ListWidget, Option, Select, TableWidget, TextArea + +# more specific widgets should only work on more specific fields +Field(widget=Input()) +Field(widget=TextArea()) # type:ignore +Field(widget=Select()) # type:ignore + +# less specific widgets are fine, even if they're often not what you want +StringField(widget=Input()) +StringField(widget=TextArea()) + +SelectField(widget=Input(), option_widget=Input()) +SelectField(widget=Select(), option_widget=Option()) +# a more specific type other than Option widget is not allowed +SelectField(widget=Select(), option_widget=TextArea()) # type:ignore + +# we should be able to pass Field() even though it wants an unbound_field +# this gets around __new__ not working in type checking +FieldList(Field(), widget=Input()) +FieldList(Field(), widget=ListWidget()) + +FormField(Form, widget=Input()) +FormField(Form, widget=TableWidget()) diff --git a/stubs/WTForms/METADATA.toml b/stubs/WTForms/METADATA.toml new file mode 100644 index 000000000000..0a1f052cb831 --- /dev/null +++ b/stubs/WTForms/METADATA.toml @@ -0,0 +1,3 @@ +version = "3.0.*" +upstream_repository = "https://github.com/wtforms/wtforms" +requires = ["MarkupSafe"] diff --git a/stubs/WTForms/wtforms/__init__.pyi b/stubs/WTForms/wtforms/__init__.pyi new file mode 100644 index 000000000000..e440ec792e79 --- /dev/null +++ b/stubs/WTForms/wtforms/__init__.pyi @@ -0,0 +1,4 @@ +from wtforms import validators as validators, widgets as widgets +from wtforms.fields import * +from wtforms.form import Form as Form +from wtforms.validators import ValidationError as ValidationError diff --git a/stubs/WTForms/wtforms/csrf/__init__.pyi b/stubs/WTForms/wtforms/csrf/__init__.pyi new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/stubs/WTForms/wtforms/csrf/core.pyi b/stubs/WTForms/wtforms/csrf/core.pyi new file mode 100644 index 000000000000..785f34a985fe --- /dev/null +++ b/stubs/WTForms/wtforms/csrf/core.pyi @@ -0,0 +1,39 @@ +from abc import abstractmethod +from collections.abc import Callable, Sequence +from typing import Any +from typing_extensions import Self + +from wtforms.fields import HiddenField +from wtforms.fields.core import UnboundField, _Filter, _FormT, _Validator, _Widget +from wtforms.form import BaseForm +from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext + +class CSRFTokenField(HiddenField): + current_token: str | None + csrf_impl: CSRF + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: str | Callable[[], str] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + *, + csrf_impl: CSRF, + ) -> None: ... + +class CSRF: + field_class: type[CSRFTokenField] + def setup_form(self, form: BaseForm) -> list[tuple[str, UnboundField[Any]]]: ... + @abstractmethod + def generate_csrf_token(self, csrf_token_field: CSRFTokenField) -> str: ... + @abstractmethod + def validate_csrf_token(self, form: BaseForm, field: CSRFTokenField) -> None: ... diff --git a/stubs/WTForms/wtforms/csrf/session.pyi b/stubs/WTForms/wtforms/csrf/session.pyi new file mode 100644 index 000000000000..6b2ccd98e83c --- /dev/null +++ b/stubs/WTForms/wtforms/csrf/session.pyi @@ -0,0 +1,18 @@ +from _typeshed import SupportsItemAccess +from datetime import datetime, timedelta +from typing import Any + +from wtforms.csrf.core import CSRF, CSRFTokenField +from wtforms.form import BaseForm +from wtforms.meta import DefaultMeta + +class SessionCSRF(CSRF): + TIME_FORMAT: str + form_meta: DefaultMeta + def generate_csrf_token(self, csrf_token_field: CSRFTokenField) -> str: ... + def validate_csrf_token(self, form: BaseForm, field: CSRFTokenField) -> None: ... + def now(self) -> datetime: ... + @property + def time_limit(self) -> timedelta: ... + @property + def session(self) -> SupportsItemAccess[str, Any]: ... diff --git a/stubs/WTForms/wtforms/fields/__init__.pyi b/stubs/WTForms/wtforms/fields/__init__.pyi new file mode 100644 index 000000000000..d96ee0b4a7ff --- /dev/null +++ b/stubs/WTForms/wtforms/fields/__init__.pyi @@ -0,0 +1,8 @@ +from wtforms.fields.choices import * +from wtforms.fields.choices import SelectFieldBase as SelectFieldBase +from wtforms.fields.core import Field as Field, Flags as Flags, Label as Label +from wtforms.fields.datetime import * +from wtforms.fields.form import * +from wtforms.fields.list import * +from wtforms.fields.numeric import * +from wtforms.fields.simple import * diff --git a/stubs/WTForms/wtforms/fields/choices.pyi b/stubs/WTForms/wtforms/fields/choices.pyi new file mode 100644 index 000000000000..f5c076c49cc2 --- /dev/null +++ b/stubs/WTForms/wtforms/fields/choices.pyi @@ -0,0 +1,76 @@ +from collections.abc import Callable, Iterable, Iterator, Sequence +from typing import Any +from typing_extensions import Self, TypeAlias + +from wtforms.fields.core import Field, _Filter, _FormT, _Validator, _Widget +from wtforms.form import BaseForm +from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext + +# technically this allows a list, but we're more strict for type safety +_Choice: TypeAlias = tuple[Any, str] +_GroupedChoices: TypeAlias = dict[str, Iterable[_Choice]] +_FullChoice: TypeAlias = tuple[Any, str, bool] # value, label, selected +_FullGroupedChoices: TypeAlias = tuple[str, Iterable[_FullChoice]] +_Option: TypeAlias = SelectFieldBase._Option + +class SelectFieldBase(Field): + option_widget: _Widget[_Option] + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | 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, + _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]: ... + def __iter__(self) -> Iterator[_Option]: ... + + class _Option(Field): + checked: bool + +class SelectField(SelectFieldBase): + coerce: Callable[[Any], Any] + choices: list[_Choice] | _GroupedChoices + validate_choice: bool + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + coerce: Callable[[Any], Any] = ..., + choices: Iterable[_Choice] | _GroupedChoices | None = None, + validate_choice: bool = True, + *, + 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, + _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]: ... + +class SelectMultipleField(SelectField): + data: list[Any] | None + +class RadioField(SelectField): ... diff --git a/stubs/WTForms/wtforms/fields/core.pyi b/stubs/WTForms/wtforms/fields/core.pyi new file mode 100644 index 000000000000..614f93a9c5aa --- /dev/null +++ b/stubs/WTForms/wtforms/fields/core.pyi @@ -0,0 +1,132 @@ +from builtins import type as _type # type is being shadowed in Field +from collections.abc import Callable, Iterable, Sequence +from typing import Any, Generic, Protocol, TypeVar, overload +from typing_extensions import Self, TypeAlias + +from markupsafe import Markup +from wtforms.form import BaseForm +from wtforms.meta import DefaultMeta, _MultiDictLikeWithGetlist, _SupportsGettextAndNgettext + +_FormT = TypeVar("_FormT", bound=BaseForm) +_FieldT = TypeVar("_FieldT", bound=Field) +_FormT_contra = TypeVar("_FormT_contra", bound=BaseForm, contravariant=True) +_FieldT_contra = TypeVar("_FieldT_contra", bound=Field, contravariant=True) +# It would be nice to annotate this as invariant, i.e. input type and output type +# needs to be the same, but it will probably be too annoying to use, for now we +# trust, that people won't use it to change the type of data in a field... +_Filter: TypeAlias = Callable[[Any], Any] + +class _Validator(Protocol[_FormT_contra, _FieldT_contra]): + def __call__(self, __form: _FormT_contra, __field: _FieldT_contra) -> object: ... + +class _Widget(Protocol[_FieldT_contra]): + def __call__(self, field: _FieldT_contra, **kwargs: Any) -> Markup: ... + +class Field: + errors: Sequence[str] + process_errors: Sequence[str] + raw_data: list[Any] | None + object_data: Any + data: Any + validators: Sequence[_Validator[Any, Self]] + # even though this could be None on the base class, this should + # never actually be None in a real field + widget: _Widget[Self] + do_not_call_in_templates: bool + meta: DefaultMeta + default: Any | None + description: str + render_kw: dict[str, Any] + filters: Sequence[_Filter] + flags: Flags + name: str + short_name: str + id: str + type: str + label: Label + # 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 + def __new__(cls, *args: Any, **kwargs: Any) -> Self: ... + def __init__( + self, + label: str | None = None, + # for tuple we can be a bit more type safe and only accept validators + # that would work on this or a less specific field, but in general it + # would be too annoying to restrict to Sequence[_Validator], since mypy + # will infer a list of mixed validators as list[object], since that is + # the common base class between all validators + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | 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, + _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: ... + @classmethod + def check_validators(cls, validators: Iterable[_Validator[_FormT, Self]] | None) -> None: ... + def gettext(self, string: str) -> str: ... + def ngettext(self, singular: str, plural: str, n: int) -> str: ... + def validate(self, form: BaseForm, extra_validators: tuple[_Validator[_FormT, Self], ...] | list[Any] = ()) -> bool: ... + def pre_validate(self, form: BaseForm) -> None: ... + def post_validate(self, form: BaseForm, validation_stopped: bool) -> None: ... + def process( + self, formdata: _MultiDictLikeWithGetlist | None, data: Any = ..., extra_filters: Sequence[_Filter] | 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: ... + + # this is a workaround for what is essentialy illegal in static type checking + # Field.__new__ would return an UnboundField, unless the _form parameter is + # specified. We can't really work around it by making UnboundField a subclass + # of Field, since all subclasses of Field still need to return an UnboundField + # and we can't expect third parties to add a __new__ method to every field + # they define... + # This workaround only works for Form, not BaseForm, but we take what we can get + # BaseForm shouldn't really be used anyways + @overload + def __get__(self, obj: None, owner: _type[object] | None = None) -> UnboundField[Self]: ... + @overload + def __get__(self, obj: object, owner: _type[object] | None = None) -> Self: ... + +class UnboundField(Generic[_FieldT]): + creation_counter: int + field_class: type[_FieldT] + name: str | None + args: tuple[Any, ...] + kwargs: dict[str, Any] + def __init__(self, field_class: type[_FieldT], *args: object, name: str | None = None, **kwargs: object) -> None: ... + def bind( + self, + form: BaseForm, + name: str, + prefix: str = "", + translations: _SupportsGettextAndNgettext | None = None, + **kwargs: object, + ) -> _FieldT: ... + +class Flags: + # the API for this is a bit loosey goosey, the intention probably + # was that the values should always be boolean, but __contains__ + # just returns the same thing as __getattr__ and in the widgets + # there are fields that could accept numeric values from Flags + def __getattr__(self, name: str) -> Any | None: ... + def __setattr__(self, name: str, value: object) -> None: ... + def __delattr__(self, name: str) -> None: ... + def __contains__(self, name: str) -> Any | None: ... + +class Label: + field_id: str + text: str + def __init__(self, field_id: str, text: str) -> None: ... + def __html__(self) -> str: ... + def __call__(self, text: str | None = None, **kwargs: Any) -> Markup: ... diff --git a/stubs/WTForms/wtforms/fields/datetime.pyi b/stubs/WTForms/wtforms/fields/datetime.pyi new file mode 100644 index 000000000000..58d9135cd37f --- /dev/null +++ b/stubs/WTForms/wtforms/fields/datetime.pyi @@ -0,0 +1,116 @@ +from collections.abc import Callable, Sequence +from datetime import date, datetime, time +from typing import Any +from typing_extensions import Self + +from wtforms.fields.core import Field, _Filter, _FormT, _Validator, _Widget +from wtforms.form import BaseForm +from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext + +class DateTimeField(Field): + format: list[str] + strptime_format: list[str] + data: datetime | None + default: datetime | Callable[[], datetime] | None + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + format: str | list[str] = "%Y-%m-%d %H:%M:%S", + *, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: datetime | Callable[[], datetime] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class DateField(DateTimeField): + data: date | None # type: ignore[assignment] + default: date | Callable[[], date] | None # type: ignore[assignment] + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + format: str | list[str] = "%Y-%m-%d", + *, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: date | Callable[[], date] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class TimeField(DateTimeField): + data: time | None # type: ignore[assignment] + default: time | Callable[[], time] | None # type: ignore[assignment] + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + format: str | list[str] = "%H:%M", + *, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: time | Callable[[], time] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class MonthField(DateField): + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + format: str | list[str] = "%Y-%m", + *, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: time | Callable[[], time] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class DateTimeLocalField(DateTimeField): + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + format: str | list[str] = ..., + *, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: time | Callable[[], time] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: 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/form.pyi b/stubs/WTForms/wtforms/fields/form.pyi new file mode 100644 index 000000000000..fe9c694d187f --- /dev/null +++ b/stubs/WTForms/wtforms/fields/form.pyi @@ -0,0 +1,38 @@ +from collections.abc import Iterator, Sequence +from typing import Any, Generic, TypeVar + +from wtforms.fields.core import Field, _Widget +from wtforms.form import BaseForm +from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext + +_BoundFormT = TypeVar("_BoundFormT", bound=BaseForm) + +class FormField(Field, Generic[_BoundFormT]): + form_class: type[_BoundFormT] + form: _BoundFormT + separator: str + def __init__( + self: FormField[_BoundFormT], + form_class: type[_BoundFormT], + label: str | None = None, + validators: None = None, + separator: str = "-", + *, + description: str = "", + id: str | None = None, + default: object | None = None, + widget: _Widget[FormField[_BoundFormT]] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + def __iter__(self) -> Iterator[Field]: ... + def __getitem__(self, name: str) -> Field: ... + def __getattr__(self, name: str) -> Field: ... + @property + def data(self) -> dict[str, Any]: ... + @property + def errors(self) -> dict[str | None, Sequence[str]]: ... # type: ignore[override] diff --git a/stubs/WTForms/wtforms/fields/list.pyi b/stubs/WTForms/wtforms/fields/list.pyi new file mode 100644 index 000000000000..4ce857d676c2 --- /dev/null +++ b/stubs/WTForms/wtforms/fields/list.pyi @@ -0,0 +1,44 @@ +from collections.abc import Callable, Iterable, Iterator +from typing import Any, Generic, TypeVar + +from wtforms.fields.core import Field, UnboundField, _FormT, _Validator, _Widget +from wtforms.form import BaseForm +from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext + +_BoundFieldT = TypeVar("_BoundFieldT", bound=Field) + +class FieldList(Field, Generic[_BoundFieldT]): + unbound_field: UnboundField[_BoundFieldT] + min_entries: int + max_entries: int | None + last_index: int + entries: list[_BoundFieldT] + object_data: Iterable[Any] + def __init__( + self: FieldList[_BoundFieldT], + # because of our workaround we need to accept Field as well + unbound_field: UnboundField[_BoundFieldT] | _BoundFieldT, + label: str | None = None, + validators: tuple[_Validator[_FormT, _BoundFieldT], ...] | list[Any] | None = None, + min_entries: int = 0, + max_entries: int | None = None, + separator: str = "-", + default: Iterable[Any] | Callable[[], Iterable[Any]] = (), + *, + description: str = "", + id: str | None = None, + widget: _Widget[FieldList[Any]] | None = None, + render_kw: dict[str, Any] | None = None, + name: 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 __iter__(self) -> Iterator[_BoundFieldT]: ... + def __len__(self) -> int: ... + def __getitem__(self, index: int) -> _BoundFieldT: ... + @property + def data(self) -> list[Any]: ... diff --git a/stubs/WTForms/wtforms/fields/numeric.pyi b/stubs/WTForms/wtforms/fields/numeric.pyi new file mode 100644 index 000000000000..7288369c0dac --- /dev/null +++ b/stubs/WTForms/wtforms/fields/numeric.pyi @@ -0,0 +1,145 @@ +from collections.abc import Callable, Sequence +from decimal import Decimal +from typing import Any, overload +from typing_extensions import Literal, Self + +from wtforms.fields.core import Field, _Filter, _FormT, _Validator, _Widget +from wtforms.form import BaseForm +from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext +from wtforms.utils import UnsetValue + +__all__ = ("IntegerField", "DecimalField", "FloatField", "IntegerRangeField", "DecimalRangeField") + +class LocaleAwareNumberField(Field): + use_locale: bool + number_format: Any | None + locale: str + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + use_locale: bool = False, + # this accepts a babel.numbers.NumberPattern, but since it + # is an optional dependency we don't want to depend on it + # for annotating this one argument + number_format: str | Any | 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, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class IntegerField(Field): + data: int | None + # technically this is not as strict and will accept anything + # that can be passed into int(), but we might as well be + default: int | Callable[[], int] | None + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + *, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: int | Callable[[], int] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class DecimalField(LocaleAwareNumberField): + data: Decimal | None + # technically this is not as strict and will accept anything + # that can be passed into Decimal(), but we might as well be + default: Decimal | Callable[[], Decimal] | None + places: int | None + rounding: str | None + @overload + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + *, + places: UnsetValue = ..., + rounding: 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 + # for annotation this one argument + number_format: str | Any | None = None, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: Decimal | Callable[[], Decimal] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: 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, + places: int | UnsetValue | None = ..., + rounding: str | None = None, + *, + use_locale: Literal[False] = False, + # this accepts a babel.numbers.NumberPattern, but since it + # is an optional dependency we don't want to depend on it + # for annotation this one argument + number_format: str | Any | None = None, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: Decimal | Callable[[], Decimal] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class FloatField(Field): + data: float | None + # technically this is not as strict and will accept anything + # that can be passed into float(), but we might as well be + default: float | Callable[[], float] | None + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + *, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: float | Callable[[], float] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class IntegerRangeField(IntegerField): ... +class DecimalRangeField(DecimalField): ... diff --git a/stubs/WTForms/wtforms/fields/simple.pyi b/stubs/WTForms/wtforms/fields/simple.pyi new file mode 100644 index 000000000000..dce59077c225 --- /dev/null +++ b/stubs/WTForms/wtforms/fields/simple.pyi @@ -0,0 +1,64 @@ +from collections.abc import Callable, Collection, Sequence +from typing import Any +from typing_extensions import Self + +from wtforms.fields.core import Field, _Filter, _FormT, _Validator, _Widget +from wtforms.form import BaseForm +from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext + +class BooleanField(Field): + data: bool + default: bool | Callable[[], bool] | None + false_values: Collection[Any] + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + false_values: Collection[Any] | None = None, + *, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: bool | Callable[[], bool] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class StringField(Field): + data: str | None + default: str | Callable[[], str] | None + def __init__( + self, + label: str | None = None, + validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None, + filters: Sequence[_Filter] = (), + description: str = "", + id: str | None = None, + default: str | Callable[[], str] | None = None, + widget: _Widget[Self] | None = None, + render_kw: dict[str, Any] | None = None, + name: str | None = None, + _form: BaseForm | None = None, + _prefix: str = "", + _translations: _SupportsGettextAndNgettext | None = None, + _meta: DefaultMeta | None = None, + ) -> None: ... + +class TextAreaField(StringField): ... +class PasswordField(StringField): ... +class FileField(Field): ... + +class MultipleFileField(FileField): + data: list[Any] + +class HiddenField(StringField): ... +class SubmitField(BooleanField): ... +class SearchField(StringField): ... +class TelField(StringField): ... +class URLField(StringField): ... +class EmailField(StringField): ... diff --git a/stubs/WTForms/wtforms/form.pyi b/stubs/WTForms/wtforms/form.pyi new file mode 100644 index 000000000000..bfb81c72b901 --- /dev/null +++ b/stubs/WTForms/wtforms/form.pyi @@ -0,0 +1,84 @@ +from _typeshed import SupportsItems +from collections.abc import Iterable, Iterator, Mapping, Sequence +from typing import Any, ClassVar, Protocol, overload +from typing_extensions import TypeAlias + +from wtforms.fields.core import Field, UnboundField +from wtforms.meta import DefaultMeta, _MultiDictLike + +_FormErrors: TypeAlias = dict[str | None, Sequence[str] | _FormErrors] + +# _unbound_fields will always be a list on an instance, but on a +# class it might be None, if it never has been instantiated, or +# not instantianted after a new field had been added/removed +class _UnboundFields(Protocol): + @overload + def __get__(self, obj: None, owner: type[object] | None = None) -> list[tuple[str, UnboundField[Any]]] | None: ... + @overload + def __get__(self, obj: object, owner: type[object] | None = None) -> list[tuple[str, UnboundField[Any]]]: ... + +class BaseForm: + meta: DefaultMeta + form_errors: list[str] + # we document this, because it's the only efficient way to introspect + # the field names of the form, it also seems to be stable API-wise + _fields: dict[str, Field] + def __init__( + self, + fields: SupportsItems[str, UnboundField[Any]] | Iterable[tuple[str, UnboundField[Any]]], + prefix: str = "", + meta: DefaultMeta = ..., + ) -> None: ... + def __iter__(self) -> Iterator[Field]: ... + def __contains__(self, name: str) -> bool: ... + def __getitem__(self, name: str) -> Field: ... + def __setitem__(self, name: str, value: UnboundField[Any]) -> None: ... + def __delitem__(self, name: str) -> None: ... + def populate_obj(self, obj: object) -> None: ... + # while we would like to be more strict on extra_filters, we can't easily do that + # without it being annoying in most situations + def process( + self, + formdata: _MultiDictLike | None = None, + obj: object | None = None, + data: Mapping[str, Any] | None = None, + extra_filters: Mapping[str, Sequence[Any]] | None = None, + **kwargs: object, + ) -> None: ... + # same thing here with extra_validators + def validate(self, extra_validators: Mapping[str, Sequence[Any]] | None = None) -> bool: ... + @property + def data(self) -> dict[str, Any]: ... + # because of the Liskov violation in FormField.errors we need to make errors a recursive type + @property + def errors(self) -> _FormErrors: ... + +class FormMeta(type): + def __init__(cls, name: str, bases: Sequence[type[object]], attrs: Mapping[str, Any]) -> None: ... + def __call__(cls, *args: Any, **kwargs: Any) -> Any: ... + def __setattr__(cls, name: str, value: object) -> None: ... + def __delattr__(cls, name: str) -> None: ... + +class Form(BaseForm, metaclass=FormMeta): + # due to the metaclass this should always be a subclass of DefaultMeta + # but if we annotate this as such, then subclasses cannot use it in the + # intended way + Meta: ClassVar[type[Any]] + # this attribute is documented, so we annotate it + _unbound_fields: _UnboundFields + def __init__( + self, + formdata: _MultiDictLike | None = None, + obj: object | None = None, + prefix: str = "", + data: Mapping[str, Any] | None = None, + meta: Mapping[str, Any] | None = None, + *, + # same issue as with process + extra_filters: Mapping[str, Sequence[Any]] | None = None, + **kwargs: object, + ) -> None: ... + # this should emit a type_error, since it's not allowed to be called + def __setitem__(self, name: str, value: None) -> None: ... # type: ignore[override] + def __delitem__(self, name: str) -> None: ... + def __delattr__(self, name: str) -> None: ... diff --git a/stubs/WTForms/wtforms/i18n.pyi b/stubs/WTForms/wtforms/i18n.pyi new file mode 100644 index 000000000000..39cd5f697464 --- /dev/null +++ b/stubs/WTForms/wtforms/i18n.pyi @@ -0,0 +1,30 @@ +from collections.abc import Callable, Iterable +from gettext import GNUTranslations +from typing import Protocol, TypeVar, overload + +_T = TypeVar("_T") + +class _SupportsUgettextAndUngettext(Protocol): + def ugettext(self, __string: str) -> str: ... + def ungettext(self, __singular: str, __plural: str, __n: int) -> str: ... + +def messages_path() -> str: ... +def get_builtin_gnu_translations(languages: Iterable[str] | None = None) -> GNUTranslations: ... +@overload +def get_translations( + languages: Iterable[str] | None = None, getter: Callable[[Iterable[str]], GNUTranslations] = ... +) -> GNUTranslations: ... +@overload +def get_translations(languages: Iterable[str] | None = None, *, getter: Callable[[Iterable[str]], _T]) -> _T: ... +@overload +def get_translations(languages: Iterable[str] | None, getter: Callable[[Iterable[str]], _T]) -> _T: ... + +class DefaultTranslations: + translations: _SupportsUgettextAndUngettext + def __init__(self, translations: _SupportsUgettextAndUngettext) -> None: ... + def gettext(self, string: str) -> str: ... + def ngettext(self, singular: str, plural: str, n: int) -> str: ... + +class DummyTranslations: + def gettext(self, string: str) -> str: ... + def ngettext(self, singular: str, plural: str, n: int) -> str: ... diff --git a/stubs/WTForms/wtforms/meta.pyi b/stubs/WTForms/wtforms/meta.pyi new file mode 100644 index 000000000000..aa67be71cc1b --- /dev/null +++ b/stubs/WTForms/wtforms/meta.pyi @@ -0,0 +1,55 @@ +from _typeshed import SupportsItems +from collections.abc import Collection, Iterator, MutableMapping +from typing import Any, Protocol, TypeVar, overload +from typing_extensions import Literal, TypeAlias + +from markupsafe import Markup +from wtforms.fields.core import Field, UnboundField +from wtforms.form import BaseForm + +_FieldT = TypeVar("_FieldT", bound=Field) + +class _SupportsGettextAndNgettext(Protocol): + def gettext(self, __string: str) -> str: ... + def ngettext(self, __singular: str, __plural: str, __n: int) -> str: ... + +# these are the methods WTForms depends on, the dict can either provide +# a getlist or getall, if it only provies getall, it will wrapped, to +# provide getlist instead +class _MultiDictLikeBase(Protocol): + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + def __contains__(self, __key: Any) -> bool: ... + +# since how file uploads are represented in formdata is implementation-specific +# we have to be generous in what we accept in the return of getlist/getall +# we can make this generic if we ever want to be more specific +class _MultiDictLikeWithGetlist(_MultiDictLikeBase, Protocol): + def getlist(self, __key: str) -> list[Any]: ... + +class _MultiDictLikeWithGetall(_MultiDictLikeBase, Protocol): + def getall(self, __key: str) -> list[Any]: ... + +_MultiDictLike: TypeAlias = _MultiDictLikeWithGetall | _MultiDictLikeWithGetlist + +class DefaultMeta: + def bind_field(self, form: BaseForm, unbound_field: UnboundField[_FieldT], options: MutableMapping[str, Any]) -> _FieldT: ... + @overload + def wrap_formdata(self, form: BaseForm, formdata: None) -> None: ... + @overload + def wrap_formdata(self, form: BaseForm, formdata: _MultiDictLike) -> _MultiDictLikeWithGetlist: ... + def render_field(self, field: Field, render_kw: SupportsItems[str, Any]) -> Markup: ... + csrf: bool + csrf_field_name: str + csrf_secret: Any | None + csrf_context: Any | None + csrf_class: type[Any] | None + def build_csrf(self, form: BaseForm) -> Any: ... + locales: Literal[False] | Collection[str] + cache_translations: bool + translations_cache: dict[str, _SupportsGettextAndNgettext] + def get_translations(self, form: BaseForm) -> _SupportsGettextAndNgettext: ... + def update_values(self, values: SupportsItems[str, Any]) -> None: ... + # since meta can be extended with arbitary data we add a __getattr__ + # method that returns Any + def __getattr__(self, name: str) -> Any: ... diff --git a/stubs/WTForms/wtforms/utils.pyi b/stubs/WTForms/wtforms/utils.pyi new file mode 100644 index 000000000000..ea7408985c91 --- /dev/null +++ b/stubs/WTForms/wtforms/utils.pyi @@ -0,0 +1,19 @@ +from collections.abc import Iterable, Iterator +from typing import Any +from typing_extensions import Literal + +from wtforms.meta import _MultiDictLikeWithGetall + +def clean_datetime_format_for_strptime(formats: Iterable[str]) -> list[str]: ... + +class UnsetValue: + def __bool__(self) -> Literal[False]: ... + +unset_value: UnsetValue + +class WebobInputWrapper: + def __init__(self, multidict: _MultiDictLikeWithGetall) -> None: ... + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + def __contains__(self, name: str) -> bool: ... + def getlist(self, name: str) -> list[Any]: ... diff --git a/stubs/WTForms/wtforms/validators.pyi b/stubs/WTForms/wtforms/validators.pyi new file mode 100644 index 000000000000..9f71afd4d3f3 --- /dev/null +++ b/stubs/WTForms/wtforms/validators.pyi @@ -0,0 +1,158 @@ +from collections.abc import Callable, Collection, Iterable +from decimal import Decimal +from re import Match, Pattern +from typing import Any, TypeVar, overload + +from wtforms.fields import Field, StringField +from wtforms.form import BaseForm + +_ValuesT = TypeVar("_ValuesT", bound=Collection[Any], contravariant=True) + +class ValidationError(ValueError): + def __init__(self, message: str = "", *args: object) -> None: ... + +class StopValidation(Exception): + def __init__(self, message: str = "", *args: object) -> None: ... + +class EqualTo: + fieldname: str + message: str | None + def __init__(self, fieldname: str, message: str | None = None) -> None: ... + def __call__(self, form: BaseForm, field: Field) -> None: ... + +class Length: + min: int + max: int + message: str | None + field_flags: dict[str, Any] + def __init__(self, min: int = -1, max: int = -1, message: str | None = None) -> None: ... + def __call__(self, form: BaseForm, field: StringField) -> None: ... + +class NumberRange: + min: float | Decimal | None + max: float | Decimal | None + message: str | None + field_flags: dict[str, Any] + def __init__( + self, min: float | Decimal | None = None, max: float | Decimal | None = None, message: str | None = None + ) -> None: ... + # any numeric field will work, for now we don't try to use a union + # to restrict to the defined numeric fields, since user-defined fields + # will likely not use a common base class, just like the existing + # numeric fields + def __call__(self, form: BaseForm, field: Field) -> None: ... + +class Optional: + string_check: Callable[[str], bool] + field_flags: dict[str, Any] + def __init__(self, strip_whitespace: bool = True) -> None: ... + def __call__(self, form: BaseForm, field: Field) -> None: ... + +class DataRequired: + message: str | None + field_flags: dict[str, Any] + def __init__(self, message: str | None = None) -> None: ... + def __call__(self, form: BaseForm, field: Field) -> None: ... + +class InputRequired: + message: str | None + field_flags: dict[str, Any] + def __init__(self, message: str | None = None) -> None: ... + def __call__(self, form: BaseForm, field: Field) -> None: ... + +class Regexp: + regex: Pattern[str] + message: str | None + def __init__(self, regex: str | Pattern[str], flags: int = 0, message: str | None = None) -> 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 + def __init__( + self, + message: str | None = None, + granular_message: bool = False, + check_deliverability: bool = False, + allow_smtputf8: bool = True, + allow_empty_local: bool = False, + ) -> None: ... + def __call__(self, form: BaseForm, field: StringField) -> None: ... + +class IPAddress: + ipv4: bool + ipv6: bool + message: str | None + def __init__(self, ipv4: bool = True, ipv6: bool = False, message: str | None = None) -> None: ... + def __call__(self, form: BaseForm, field: StringField) -> None: ... + @classmethod + def check_ipv4(cls, value: str | None) -> bool: ... + @classmethod + def check_ipv6(cls, value: str | None) -> bool: ... + +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): + validate_hostname: HostnameValidation + def __init__(self, require_tld: bool = True, message: str | None = None) -> None: ... + def __call__(self, form: BaseForm, field: StringField) -> None: ... # type: ignore[override] + +class UUID: + message: str | None + def __init__(self, message: str | None = None) -> None: ... + def __call__(self, form: BaseForm, field: StringField) -> None: ... + +class AnyOf: + values: Collection[Any] + message: str | None + values_formatter: Callable[[Any], str] + @overload + def __init__(self, values: Collection[Any], message: str | None = None, values_formatter: None = None) -> None: ... + @overload + def __init__(self, values: _ValuesT, message: str | None, values_formatter: Callable[[_ValuesT], str]) -> None: ... + @overload + def __init__(self, values: _ValuesT, message: str | None = None, *, values_formatter: Callable[[_ValuesT], str]) -> None: ... + def __call__(self, form: BaseForm, field: Field) -> None: ... + @staticmethod + def default_values_formatter(values: Iterable[object]) -> str: ... + +class NoneOf: + values: Collection[Any] + message: str | None + values_formatter: Callable[[Any], str] + @overload + def __init__(self, values: Collection[Any], message: str | None = None, values_formatter: None = None) -> None: ... + @overload + def __init__(self, values: _ValuesT, message: str | None, values_formatter: Callable[[_ValuesT], str]) -> None: ... + @overload + def __init__(self, values: _ValuesT, message: str | None = None, *, values_formatter: Callable[[_ValuesT], str]) -> None: ... + def __call__(self, form: BaseForm, field: Field) -> None: ... + @staticmethod + def default_values_formatter(v: Iterable[object]) -> str: ... + +class HostnameValidation: + hostname_part: Pattern[str] + tld_part: Pattern[str] + require_tld: bool + allow_ip: bool + def __init__(self, require_tld: bool = True, allow_ip: bool = False) -> None: ... + def __call__(self, hostname: str) -> bool: ... + +email = Email +equal_to = EqualTo +ip_address = IPAddress +mac_address = MacAddress +length = Length +number_range = NumberRange +optional = Optional +input_required = InputRequired +data_required = DataRequired +regexp = Regexp +url = URL +any_of = AnyOf +none_of = NoneOf diff --git a/stubs/WTForms/wtforms/widgets/__init__.pyi b/stubs/WTForms/wtforms/widgets/__init__.pyi new file mode 100644 index 000000000000..89e4b0488d59 --- /dev/null +++ b/stubs/WTForms/wtforms/widgets/__init__.pyi @@ -0,0 +1,2 @@ +from wtforms.widgets.core import * +from wtforms.widgets.core import Input as Input, html_params as html_params diff --git a/stubs/WTForms/wtforms/widgets/core.pyi b/stubs/WTForms/wtforms/widgets/core.pyi new file mode 100644 index 000000000000..90d13e764a56 --- /dev/null +++ b/stubs/WTForms/wtforms/widgets/core.pyi @@ -0,0 +1,121 @@ +from decimal import Decimal +from typing import Any +from typing_extensions import Literal + +from markupsafe import Markup +from wtforms.fields import Field, FormField, SelectFieldBase, StringField +from wtforms.fields.choices import _Option + +__all__ = ( + "CheckboxInput", + "ColorInput", + "DateInput", + "DateTimeInput", + "DateTimeLocalInput", + "EmailInput", + "FileInput", + "HiddenInput", + "ListWidget", + "MonthInput", + "NumberInput", + "Option", + "PasswordInput", + "RadioInput", + "RangeInput", + "SearchInput", + "Select", + "SubmitInput", + "TableWidget", + "TextArea", + "TextInput", + "TelInput", + "TimeInput", + "URLInput", + "WeekInput", +) + +def html_params(**kwargs: object) -> str: ... + +class ListWidget: + html_tag: Literal["ul", "ol"] + prefix_label: bool + def __init__(self, html_tag: Literal["ul", "ol"] = "ul", prefix_label: bool = True) -> None: ... + # any iterable field is fine, since people might define iterable fields + # that are not derived from FieldList, we just punt and accept any field + # with Intersection we could be more specific + def __call__(self, field: Field, **kwargs: object) -> Markup: ... + +class TableWidget: + with_table_tag: bool + def __init__(self, with_table_tag: bool = True) -> None: ... + def __call__(self, field: FormField[Any], **kwargs: object) -> Markup: ... + +class Input: + validation_attrs: list[str] + input_type: str + def __init__(self, input_type: str | None = None) -> None: ... + def __call__(self, field: Field, **kwargs: object) -> Markup: ... + @staticmethod + def html_params(**kwargs: object) -> str: ... + +class TextInput(Input): ... + +class PasswordInput(Input): + hide_value: bool + def __init__(self, hide_value: bool = True) -> None: ... + +class HiddenInput(Input): + field_flags: dict[str, Any] + +class CheckboxInput(Input): ... +class RadioInput(Input): ... + +class FileInput(Input): + multiple: bool + def __init__(self, multiple: bool = False) -> None: ... + +class SubmitInput(Input): ... + +class TextArea: + validation_attrs: list[str] + def __call__(self, field: StringField, **kwargs: object) -> Markup: ... + +class Select: + validation_attrs: list[str] + multiple: bool + 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: ... + +class Option: + def __call__(self, field: _Option, **kwargs: object) -> Markup: ... + +class SearchInput(Input): ... +class TelInput(Input): ... +class URLInput(Input): ... +class EmailInput(Input): ... +class DateTimeInput(Input): ... +class DateInput(Input): ... +class MonthInput(Input): ... +class WeekInput(Input): ... +class TimeInput(Input): ... +class DateTimeLocalInput(Input): ... + +class NumberInput(Input): + step: Decimal | float | str | None + min: Decimal | float | str | None + max: Decimal | float | str | None + def __init__( + self, + step: Decimal | float | str | None = None, + min: Decimal | float | str | None = None, + 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 ColorInput(Input): ...