Skip to content
Merged
41 changes: 41 additions & 0 deletions stubs/WTForms/@tests/stubtest_allowlist.txt
Original file line numberDiff line numberDiff line change
@@ -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
49 changes: 49 additions & 0 deletions stubs/WTForms/@tests/test_cases/check_filters.py
Original file line numberDiff line numberDiff line change
@@ -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
33 changes: 33 additions & 0 deletions stubs/WTForms/@tests/test_cases/check_validators.py
Original file line numberDiff line numberDiff line change
@@ -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()])
26 changes: 26 additions & 0 deletions stubs/WTForms/@tests/test_cases/check_widgets.py
Original file line numberDiff line numberDiff line change
@@ -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())
3 changes: 3 additions & 0 deletions stubs/WTForms/METADATA.toml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
version = "3.0.*"
upstream_repository = "https://github.com/wtforms/wtforms"
requires = ["MarkupSafe"]
4 changes: 4 additions & 0 deletions stubs/WTForms/wtforms/__init__.pyi
Original file line numberDiff line numberDiff line change
@@ -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
Empty file.
39 changes: 39 additions & 0 deletions stubs/WTForms/wtforms/csrf/core.pyi
Original file line numberDiff line numberDiff line change
@@ -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: ...
18 changes: 18 additions & 0 deletions stubs/WTForms/wtforms/csrf/session.pyi
Original file line numberDiff line numberDiff line change
@@ -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]: ...
8 changes: 8 additions & 0 deletions stubs/WTForms/wtforms/fields/__init__.pyi
Original file line numberDiff line numberDiff line change
@@ -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 *
76 changes: 76 additions & 0 deletions stubs/WTForms/wtforms/fields/choices.pyi
Original file line numberDiff line numberDiff line change
@@ -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): ...
Loading