Skip to content

Commit f8a673f

Browse files
Add stubs for WTForms (#10557)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent cb4425f commit f8a673f

24 files changed

+1305
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Error: is not present at runtime
2+
# =============================
3+
# This is hack to get around Field.__new__ not being able to return
4+
# UnboundField
5+
wtforms.Field.__get__
6+
wtforms.fields.Field.__get__
7+
wtforms.fields.core.Field.__get__
8+
# Since DefaultMeta can contain arbitrary values we added __getattr__
9+
# to let mypy know that arbitrary attribute access is possible
10+
wtforms.meta.DefaultMeta.__getattr__
11+
12+
# Error: variable differs from runtime
13+
# ======================
14+
# _unbound_fields has some weird semantics: due to the metaclass it
15+
# will be None until the form class has been instantiated at least
16+
# once and then will stick around until someone adds a new field
17+
# to the class, which clears it back to None. Which means on instances
18+
# it will always be there and on the class it depends, so maybe this
19+
# should use a dummy descriptor? For now we just pretend it's set.
20+
# The behavior is documented in FormMeta, so I think it's fine.
21+
wtforms.Form._unbound_fields
22+
wtforms.form.Form._unbound_fields
23+
24+
# widget is both used as a ClassVar and instance variable and does
25+
# not necessarily reflect an upper bound on Widget, so we always use
26+
# our Widget Protocol definition that's contravariant on Self
27+
wtforms.Field.widget
28+
wtforms.FormField.widget
29+
wtforms.SelectField.widget
30+
wtforms.SelectMultipleField.widget
31+
wtforms.TextAreaField.widget
32+
wtforms.fields.Field.widget
33+
wtforms.fields.FormField.widget
34+
wtforms.fields.SelectField.widget
35+
wtforms.fields.SelectMultipleField.widget
36+
wtforms.fields.TextAreaField.widget
37+
wtforms.fields.choices.SelectField.widget
38+
wtforms.fields.choices.SelectMultipleField.widget
39+
wtforms.fields.core.Field.widget
40+
wtforms.fields.form.FormField.widget
41+
wtforms.fields.simple.TextAreaField.widget
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
from wtforms import Field, Form
4+
5+
6+
class Filter1:
7+
def __call__(self, value: object) -> None:
8+
...
9+
10+
11+
class Filter2:
12+
def __call__(self, input: None) -> None:
13+
...
14+
15+
16+
def not_a_filter(a: object, b: object) -> None:
17+
...
18+
19+
20+
def also_not_a_filter() -> None:
21+
...
22+
23+
24+
# we should accept any mapping of sequences, we can't really validate
25+
# the filter functions when it's this nested
26+
form = Form()
27+
form.process(extra_filters={"foo": (str.upper, str.strip, int), "bar": (Filter1(), Filter2())})
28+
form.process(extra_filters={"foo": [str.upper, str.strip, int], "bar": [Filter1(), Filter2()]})
29+
30+
# regardless of how we pass the filters into Field it should work
31+
field = Field(filters=(str.upper, str.lower, int))
32+
Field(filters=(Filter1(), Filter2()))
33+
Field(filters=[str.upper, str.lower, int])
34+
Field(filters=[Filter1(), Filter2()])
35+
field.process(None, extra_filters=(str.upper, str.lower, int))
36+
field.process(None, extra_filters=(Filter1(), Filter2()))
37+
field.process(None, extra_filters=[str.upper, str.lower, int])
38+
field.process(None, extra_filters=[Filter1(), Filter2()])
39+
40+
# but if we pass in some callables with an incompatible param spec
41+
# then we should get type errors
42+
Field(filters=(str.upper, str.lower, int, not_a_filter)) # type:ignore
43+
Field(filters=(Filter1(), Filter2(), also_not_a_filter)) # type:ignore
44+
Field(filters=[str.upper, str.lower, int, also_not_a_filter]) # type:ignore
45+
Field(filters=[Filter1(), Filter2(), not_a_filter]) # type:ignore
46+
field.process(None, extra_filters=(str.upper, str.lower, int, not_a_filter)) # type:ignore
47+
field.process(None, extra_filters=(Filter1(), Filter2(), also_not_a_filter)) # type:ignore
48+
field.process(None, extra_filters=[str.upper, str.lower, int, also_not_a_filter]) # type:ignore
49+
field.process(None, extra_filters=[Filter1(), Filter2(), not_a_filter]) # type:ignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from __future__ import annotations
2+
3+
from wtforms import DateField, Field, Form, StringField
4+
from wtforms.validators import Email, Optional
5+
6+
form = Form()
7+
# on form we should accept any validator mapping
8+
form.validate({"field": (Optional(),), "string_field": (Optional(), Email())})
9+
form.validate({"field": [Optional()], "string_field": [Optional(), Email()]})
10+
11+
# both StringField validators and Field validators should be valid
12+
# as inputs on a StringField
13+
string_field = StringField(validators=(Optional(), Email()))
14+
string_field.validate(form, (Optional(), Email()))
15+
16+
# but not on Field
17+
field = Field(validators=(Optional(), Email())) # type:ignore
18+
field.validate(form, (Optional(), Email())) # type:ignore
19+
20+
# unless we only pass the Field validator
21+
Field(validators=(Optional(),))
22+
field.validate(form, (Optional(),))
23+
24+
# DateField should accept Field validators but not StringField validators
25+
date_field = DateField(validators=(Optional(), Email())) # type:ignore
26+
date_field.validate(form, (Optional(), Email())) # type:ignore
27+
DateField(validators=(Optional(),))
28+
29+
# for lists we can't be as strict so we won't get type errors here
30+
Field(validators=[Optional(), Email()])
31+
field.validate(form, [Optional(), Email()])
32+
DateField(validators=[Optional(), Email()])
33+
date_field.validate(form, [Optional(), Email()])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
from wtforms import Field, FieldList, Form, FormField, SelectField, StringField
4+
from wtforms.widgets import Input, ListWidget, Option, Select, TableWidget, TextArea
5+
6+
# more specific widgets should only work on more specific fields
7+
Field(widget=Input())
8+
Field(widget=TextArea()) # type:ignore
9+
Field(widget=Select()) # type:ignore
10+
11+
# less specific widgets are fine, even if they're often not what you want
12+
StringField(widget=Input())
13+
StringField(widget=TextArea())
14+
15+
SelectField(widget=Input(), option_widget=Input())
16+
SelectField(widget=Select(), option_widget=Option())
17+
# a more specific type other than Option widget is not allowed
18+
SelectField(widget=Select(), option_widget=TextArea()) # type:ignore
19+
20+
# we should be able to pass Field() even though it wants an unbound_field
21+
# this gets around __new__ not working in type checking
22+
FieldList(Field(), widget=Input())
23+
FieldList(Field(), widget=ListWidget())
24+
25+
FormField(Form, widget=Input())
26+
FormField(Form, widget=TableWidget())

stubs/WTForms/METADATA.toml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version = "3.0.*"
2+
upstream_repository = "https://github.com/wtforms/wtforms"
3+
requires = ["MarkupSafe"]

stubs/WTForms/wtforms/__init__.pyi

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from wtforms import validators as validators, widgets as widgets
2+
from wtforms.fields import *
3+
from wtforms.form import Form as Form
4+
from wtforms.validators import ValidationError as ValidationError

stubs/WTForms/wtforms/csrf/__init__.pyi

Whitespace-only changes.

stubs/WTForms/wtforms/csrf/core.pyi

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from abc import abstractmethod
2+
from collections.abc import Callable, Sequence
3+
from typing import Any
4+
from typing_extensions import Self
5+
6+
from wtforms.fields import HiddenField
7+
from wtforms.fields.core import UnboundField, _Filter, _FormT, _Validator, _Widget
8+
from wtforms.form import BaseForm
9+
from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext
10+
11+
class CSRFTokenField(HiddenField):
12+
current_token: str | None
13+
csrf_impl: CSRF
14+
def __init__(
15+
self,
16+
label: str | None = None,
17+
validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None,
18+
filters: Sequence[_Filter] = (),
19+
description: str = "",
20+
id: str | None = None,
21+
default: str | Callable[[], str] | None = None,
22+
widget: _Widget[Self] | None = None,
23+
render_kw: dict[str, Any] | None = None,
24+
name: str | None = None,
25+
_form: BaseForm | None = None,
26+
_prefix: str = "",
27+
_translations: _SupportsGettextAndNgettext | None = None,
28+
_meta: DefaultMeta | None = None,
29+
*,
30+
csrf_impl: CSRF,
31+
) -> None: ...
32+
33+
class CSRF:
34+
field_class: type[CSRFTokenField]
35+
def setup_form(self, form: BaseForm) -> list[tuple[str, UnboundField[Any]]]: ...
36+
@abstractmethod
37+
def generate_csrf_token(self, csrf_token_field: CSRFTokenField) -> str: ...
38+
@abstractmethod
39+
def validate_csrf_token(self, form: BaseForm, field: CSRFTokenField) -> None: ...
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from _typeshed import SupportsItemAccess
2+
from datetime import datetime, timedelta
3+
from typing import Any
4+
5+
from wtforms.csrf.core import CSRF, CSRFTokenField
6+
from wtforms.form import BaseForm
7+
from wtforms.meta import DefaultMeta
8+
9+
class SessionCSRF(CSRF):
10+
TIME_FORMAT: str
11+
form_meta: DefaultMeta
12+
def generate_csrf_token(self, csrf_token_field: CSRFTokenField) -> str: ...
13+
def validate_csrf_token(self, form: BaseForm, field: CSRFTokenField) -> None: ...
14+
def now(self) -> datetime: ...
15+
@property
16+
def time_limit(self) -> timedelta: ...
17+
@property
18+
def session(self) -> SupportsItemAccess[str, Any]: ...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from wtforms.fields.choices import *
2+
from wtforms.fields.choices import SelectFieldBase as SelectFieldBase
3+
from wtforms.fields.core import Field as Field, Flags as Flags, Label as Label
4+
from wtforms.fields.datetime import *
5+
from wtforms.fields.form import *
6+
from wtforms.fields.list import *
7+
from wtforms.fields.numeric import *
8+
from wtforms.fields.simple import *
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from collections.abc import Callable, Iterable, Iterator, Sequence
2+
from typing import Any
3+
from typing_extensions import Self, TypeAlias
4+
5+
from wtforms.fields.core import Field, _Filter, _FormT, _Validator, _Widget
6+
from wtforms.form import BaseForm
7+
from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext
8+
9+
# technically this allows a list, but we're more strict for type safety
10+
_Choice: TypeAlias = tuple[Any, str]
11+
_GroupedChoices: TypeAlias = dict[str, Iterable[_Choice]]
12+
_FullChoice: TypeAlias = tuple[Any, str, bool] # value, label, selected
13+
_FullGroupedChoices: TypeAlias = tuple[str, Iterable[_FullChoice]]
14+
_Option: TypeAlias = SelectFieldBase._Option
15+
16+
class SelectFieldBase(Field):
17+
option_widget: _Widget[_Option]
18+
def __init__(
19+
self,
20+
label: str | None = None,
21+
validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None,
22+
option_widget: _Widget[_Option] | None = None,
23+
*,
24+
filters: Sequence[_Filter] = (),
25+
description: str = "",
26+
id: str | None = None,
27+
default: object | None = None,
28+
widget: _Widget[Self] | None = None,
29+
render_kw: dict[str, Any] | None = None,
30+
name: str | None = None,
31+
_form: BaseForm | None = None,
32+
_prefix: str = "",
33+
_translations: _SupportsGettextAndNgettext | None = None,
34+
_meta: DefaultMeta | None = None,
35+
) -> None: ...
36+
def iter_choices(self) -> Iterator[_FullChoice]: ...
37+
def has_groups(self) -> bool: ...
38+
def iter_groups(self) -> Iterator[_FullGroupedChoices]: ...
39+
def __iter__(self) -> Iterator[_Option]: ...
40+
41+
class _Option(Field):
42+
checked: bool
43+
44+
class SelectField(SelectFieldBase):
45+
coerce: Callable[[Any], Any]
46+
choices: list[_Choice] | _GroupedChoices
47+
validate_choice: bool
48+
def __init__(
49+
self,
50+
label: str | None = None,
51+
validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None,
52+
coerce: Callable[[Any], Any] = ...,
53+
choices: Iterable[_Choice] | _GroupedChoices | None = None,
54+
validate_choice: bool = True,
55+
*,
56+
filters: Sequence[_Filter] = (),
57+
description: str = "",
58+
id: str | None = None,
59+
default: object | None = None,
60+
widget: _Widget[Self] | None = None,
61+
option_widget: _Widget[_Option] | None = None,
62+
render_kw: dict[str, Any] | None = None,
63+
name: str | None = None,
64+
_form: BaseForm | None = None,
65+
_prefix: str = "",
66+
_translations: _SupportsGettextAndNgettext | None = None,
67+
_meta: DefaultMeta | None = None,
68+
) -> None: ...
69+
def iter_choices(self) -> Iterator[_FullChoice]: ...
70+
def has_groups(self) -> bool: ...
71+
def iter_groups(self) -> Iterator[_FullGroupedChoices]: ...
72+
73+
class SelectMultipleField(SelectField):
74+
data: list[Any] | None
75+
76+
class RadioField(SelectField): ...

0 commit comments

Comments
 (0)