Repository: wtforms/wtforms (python)

Issue:
SelectMultipleField should support choices specified as coercible values.
### Actual Behavior

```python
>>> class DummyPostData(dict):
...     def getlist(self, key):
...             v = self[key]
...             if not isinstance(v, (list, tuple)):
...                     v = [v]
...             return v
...
>>> import wtforms
>>> class F(wtforms.Form):
...     foo=wtforms.SelectMultipleField(choices=[('1', 'One'), ('2', 'Two')], coerce=int)
...
>>> post_data = DummyPostData(foo=["1", "2"])
>>> form = F(post_data)
>>> form.validate()
False
>>> form.errors
{'foo': ["'1', '2' are not valid choices for this field."]}
```

### Expected Behavior

The form should be valid when the post data is a valid option after coercing both post data and choices:

```python
>>> class DummyPostData(dict):
...     def getlist(self, key):
...             v = self[key]
...             if not isinstance(v, (list, tuple)):
...                     v = [v]
...             return v
...
>>> import wtforms
>>> class F(wtforms.Form):
...     foo=wtforms.SelectMultipleField(choices=[('1', 'One'), ('2', 'Two')], coerce=int)
...
>>> post_data = DummyPostData(foo=["1", "2"])
>>> form = F(post_data)
>>> form.validate()
True
```


### Environment

* Python version: 3.9
* wtforms version: 3.1.1


Pull request description:
Fixes SelectMultipleField value coercion on validation.
Fixes the `SelectMultipleField` validation error when using coercion detailed in #822.

I also avoids to use set to temporarily store choice values, so there is no more hashing issue.

fixes #822

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 186501b..18129d4 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,11 @@
 .. currentmodule:: wtforms
 
+Unreleased
+----------
+
+- Fix :class:`~fields.SelectMultipleField` value coercion on validation.
+  :issue:`822` :pr:`823`
+
 Version 3.1.1
 -------------
 
diff --git a/src/wtforms/fields/choices.py b/src/wtforms/fields/choices.py
index 2b1fd2e..62ed497 100644
--- a/src/wtforms/fields/choices.py
+++ b/src/wtforms/fields/choices.py
@@ -121,8 +121,9 @@ class SelectField(SelectFieldBase):
             _choices = zip(choices, choices)
 
         for value, label, *other_args in _choices:
+            selected = self.coerce(value) == self.data
             render_kw = other_args[0] if len(other_args) else {}
-            yield (value, label, self.coerce(value) == self.data, render_kw)
+            yield (value, label, selected, render_kw)
 
     def process_data(self, value):
         try:
@@ -173,9 +174,9 @@ class SelectMultipleField(SelectField):
         else:
             _choices = zip(choices, choices)
 
-        for value, label, *args in _choices:
+        for value, label, *other_args in _choices:
             selected = self.data is not None and self.coerce(value) in self.data
-            render_kw = args[0] if len(args) else {}
+            render_kw = other_args[0] if len(other_args) else {}
             yield (value, label, selected, render_kw)
 
     def process_data(self, value):
@@ -201,9 +202,11 @@ class SelectMultipleField(SelectField):
         if self.choices is None:
             raise TypeError(self.gettext("Choices cannot be None."))
 
-        acceptable = {c[0] for c in self.iter_choices()}
-        if any(d not in acceptable for d in self.data):
-            unacceptable = [str(d) for d in set(self.data) - acceptable]
+        acceptable = [self.coerce(choice[0]) for choice in self.iter_choices()]
+        if any(data not in acceptable for data in self.data):
+            unacceptable = [
+                str(data) for data in set(self.data) if data not in acceptable
+            ]
             raise ValidationError(
                 self.ngettext(
                     "'%(value)s' is not a valid choice for this field.",
diff --git a/src/wtforms/locale/sv/LC_MESSAGES/wtforms.po b/src/wtforms/locale/sv/LC_MESSAGES/wtforms.po
index d31bf0d..d1c9a4a 100644
--- a/src/wtforms/locale/sv/LC_MESSAGES/wtforms.po
+++ b/src/wtforms/locale/sv/LC_MESSAGES/wtforms.po
@@ -6,10 +6,10 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: WTForms 2.0dev\n"
-"Report-Msgid-Bugs-To: eloi.rivard@nubla.fr\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
 "POT-Creation-Date: 2023-10-05 13:42+0200\n"
-"PO-Revision-Date: 2023-11-25 05:13+0000\n"
-"Last-Translator: bittin1ddc447d824349b2 <bittin@reimu.nl>\n"
+"PO-Revision-Date: 2023-08-22 10:53+0000\n"
+"Last-Translator: Luna Jernberg <droidbittin@gmail.com>\n"
 "Language-Team: Swedish <https://hosted.weblate.org/projects/wtforms/wtforms/"
 "sv/>\n"
 "Language: sv\n"
@@ -17,7 +17,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 5.2.1-rc\n"
+"X-Generator: Weblate 5.0-dev\n"
 "Generated-By: Babel 2.8.0\n"
 
 #: src/wtforms/validators.py:86
@@ -110,12 +110,14 @@ msgid "Invalid value, can't be any of: %(values)s."
 msgstr "Felaktigt värde, får inte vara något av: %(values)s."
 
 #: src/wtforms/validators.py:698
+#, fuzzy
+#| msgid "This field is required."
 msgid "This field cannot be edited"
-msgstr "Detta fält kan inte redigeras"
+msgstr "Det här fältet är obligatoriskt."
 
 #: src/wtforms/validators.py:714
 msgid "This field is disabled and cannot have a value"
-msgstr "Det här fältet är inaktiverat och kan inte ha ett värde"
+msgstr ""
 
 #: src/wtforms/csrf/core.py:96
 msgid "Invalid CSRF Token."
@@ -150,11 +152,12 @@ msgid "Invalid choice(s): one or more data inputs could not be coerced."
 msgstr "Felaktigt val; ett eller flera inputfält kunde inte coerca:s"
 
 #: src/wtforms/fields/choices.py:204
-#, python-format
+#, fuzzy, python-format
+#| msgid "'%(value)s' is not a valid choice for this field."
 msgid "'%(value)s' is not a valid choice for this field."
 msgid_plural "'%(value)s' are not valid choices for this field."
-msgstr[0] "'%(value)s' är inte ett giltigt val för detta fält."
-msgstr[1] "'%(value)s' är inte giltiga val för detta fält."
+msgstr[0] "'%(value)s' är inte ett giltigt värde för det här fältet"
+msgstr[1] "'%(value)s' är inte ett giltigt värde för det här fältet"
 
 #: src/wtforms/fields/datetime.py:51
 msgid "Not a valid datetime value."
@@ -169,8 +172,10 @@ msgid "Not a valid time value."
 msgstr "Inte ett giltigt tidsvärde."
 
 #: src/wtforms/fields/datetime.py:148
+#, fuzzy
+#| msgid "Not a valid date value."
 msgid "Not a valid week value."
-msgstr "Inte ett giltigt veckovärde."
+msgstr "Inte ett giltigt datum"
 
 #: src/wtforms/fields/numeric.py:82 src/wtforms/fields/numeric.py:92
 msgid "Not a valid integer value."


Test patch:
diff --git a/tests/fields/test_selectmultiple.py b/tests/fields/test_selectmultiple.py
index 5fd2a99..127fe45 100644
--- a/tests/fields/test_selectmultiple.py
+++ b/tests/fields/test_selectmultiple.py
@@ -190,3 +190,16 @@ def test_optgroup_option_render_kw():
     assert list(form.a.iter_choices()) == [
         ("a", "Foo", True, {"title": "foobar", "data-foo": "bar"})
     ]
+
+
+def test_can_supply_coercable_values_as_options():
+    F = make_form(
+        a=SelectMultipleField(
+            choices=[("1", "One"), ("2", "Two")],
+            coerce=int,
+        )
+    )
+    post_data = DummyPostData(a=["1", "2"])
+    form = F(post_data)
+    assert form.validate()
+    assert form.a.data == [1, 2]