Repository: pallets-eco/wtforms (Python)

Issue:
SelectMultipleField pre_validate expects a tuple list even though a normal list is a valid argument
Due to the patch provided in #605 `SelectMultipleField.pre_validate()` behaves unexpectedly, the method wasn't updated to support a value only list as `choices`.

`pre_validate` expects `choices` to be a list of  `(value, label)` tuples thus validating the choices by doing:

```python
values = list(c[0] for c in self.choices)
for d in self.data:
    if d not in values:
        raise ValidationError(
            self.gettext("'%(value)s' is not a valid choice for this field")
                % dict(value=d)
                )
```

however this means that if `choices` is a value only list, such as `['foo', 'bar']`, `c[0] in self.choices` checks for the first char of the element (`c[0]='f', c[0]='b'`) breaking the validation.



A possible fix could be reusing the checks used in `iter_choices`:

```python
if not self.choices:
    choices = []
elif isinstance(self.choices[0], (list, tuple)):
    choices = self.choices
else:
    choices = zip(self.choices, self.choices)	
```

however I personally suggest that formatting `choices` at `__init__` would be a better option, it would stop these issues and make things more consistent. The main issue is that at the moment every method that uses `choices` has to check for its format, might as well define it a the top and be done with it.


Discussion:
Could reproduce with:

```python
>>> import wtforms
>>> class F(wtforms.Form):
...     foo = wtforms.SelectMultipleField(choices=["foo", "bar"])
>>> F(foo=["foo"]).validate()
False
```



Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 929e0310e..0e0ba945a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -41,6 +41,8 @@ Unreleased
 -   Error messages standardization. :issue:`613` :pr:`620` :pr:`626` :pr:`627`
 -   :class:`~wtforms.fields.core.SelectMultipleField` `validate_choice`
     bugfix. :issue:`606` :pr:`642`
+-   Fixed SelectMultipleField validation when using choices list shortcut.
+    :issue:`612` :pr:`661`
 
 
 Version 2.3.3
diff --git a/src/wtforms/fields/core.py b/src/wtforms/fields/core.py
index 331e5d62f..90611483e 100644
--- a/src/wtforms/fields/core.py
+++ b/src/wtforms/fields/core.py
@@ -616,16 +616,20 @@ def pre_validate(self, form):
         if self.choices is None:
             raise TypeError(self.gettext("Choices cannot be None."))
 
-        if self.validate_choice and self.data:
-            values = list(c[0] for c in self.choices)
-            for d in self.data:
-                if d not in values:
-                    raise ValidationError(
-                        self.gettext(
-                            "'%(value)s' is not a valid choice for this field."
-                        )
-                        % dict(value=d)
-                    )
+        if not self.validate_choice or not self.data:
+            return
+
+        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]
+            raise ValidationError(
+                self.ngettext(
+                    "'%(value)s' is not a valid choice for this field.",
+                    "'%(value)s' are not valid choices for this field.",
+                    len(unacceptable),
+                )
+                % dict(value="', '".join(unacceptable))
+            )
 
 
 class RadioField(SelectField):


Test patch:
diff --git a/tests/fields/test_selectmultiple.py b/tests/fields/test_selectmultiple.py
index 4fc585653..1d9009b91 100644
--- a/tests/fields/test_selectmultiple.py
+++ b/tests/fields/test_selectmultiple.py
@@ -69,14 +69,15 @@ def choices():
 
 
 def test_choice_shortcut():
-    F = make_form(a=SelectMultipleField(choices=["foo", "bar"], validate_choice=False))
-    form = F(a="bar")
+    F = make_form(a=SelectMultipleField(choices=["foo", "bar"]))
+    form = F(a=["bar"])
+    assert form.validate()
     assert '<option value="foo">foo</option>' in form.a()
 
 
 @pytest.mark.parametrize("choices", [[], None])
 def test_empty_choice(choices):
-    F = make_form(a=SelectMultipleField(choices=choices, validate_choice=False))
+    F = make_form(a=SelectMultipleField(choices=choices))
     form = F(a="bar")
     assert form.a() == '<select id="a" multiple name="a"></select>'