Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stubs for WTForms #10557

Merged
merged 11 commits into from
Sep 29, 2023
41 changes: 41 additions & 0 deletions stubs/WTForms/@tests/stubtest_allowlist.txt
Original file line number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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