Repository: wtforms/wtforms (python)

Issue:
Validators do not raise IndexError when it catches a ValueError
Hi. When a validator accidentally raises a ZeroDivisionError, the is correctly raised, but when a validator accidentally raises a ValueError, an IndexError is raised instead. This makes the debugging a bit more difficult.

Here is a script to reproduce the problem:

```python
import wtforms
import werkzeug


def division_error_validator(form, field): raise ZeroDivisionError
class DivisionErrorForm(wtforms.Form):
    foo = wtforms.StringField(validators=[division_error_validator])

def value_error_validator(form, field): raise ValueError
class ValueErrorForm(wtforms.Form):
    foo = wtforms.StringField(validators=[value_error_validator])


try:
    DivisionErrorForm(werkzeug.ImmutableMultiDict({"bar": "baz"})).validate()
except ZeroDivisionError:
    print("ZeroDivisionError OK")
except Exception as exc:
    print("ZeroDivisionError KO - found %s" % type(exc))

try:
    ValueErrorForm(werkzeug.ImmutableMultiDict({"bar": "baz"})).validate()
except ValueError:
    print("ValueError OK")
except Exception as exc:
    print("ValueError KO - found %s" % type(exc))

# prints
# ZeroDivisionError OK
# ValueError KO - found <type 'exceptions.IndexError'>
```


Pull request description:
Validators correctly raise exceptions
This PR should fix #445 
Because `ValidationError` inherits from `ValueError`, A `ValueError` raised inside a validator is just considered as the validator refusing the data. This behavior felt unexpected to me, and is fixed in this PR.

Here is the piece of code from #445 illustrating the bug:

```python
>>> from wtforms import StringField, Form
>>> from werkzeug.datastructures import MultiDict
...
>>> def division_error_validator(form, field): raise ZeroDivisionError
>>> class DivisionErrorForm(Form):
...     foo = StringField(validators=[division_error_validator])
...
>>> def value_error_validator(form, field): raise ValueError
>>> class ValueErrorForm(Form):
...    foo = StringField(validators=[value_error_validator])
...
>>> DivisionErrorForm(MultiDict({"bar": "baz"})).validate()
Traceback (most recent call last):
...
ZeroDivisionError

>>> ValueErrorForm(MultiDict({"bar": "baz"})).validate()
Traceback (most recent call last):
  File "/home/azmeuk/dev/wtforms/src/wtforms/fields/core.py", line 275, in _run_validation_chain
    validator(form, self)
  File "<stdin>", line 1, in value_error_validator
ValueError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/azmeuk/dev/wtforms/src/wtforms/form.py", line 300, in validate
    return super(Form, self).validate(extra)
  File "/home/azmeuk/dev/wtforms/src/wtforms/form.py", line 136, in validate
    if not field.validate(self, extra):
  File "/home/azmeuk/dev/wtforms/src/wtforms/fields/core.py", line 255, in validate
    stop_validation = self._run_validation_chain(form, chain)
  File "/home/azmeuk/dev/wtforms/src/wtforms/fields/core.py", line 281, in _run_validation_chain
    self.errors.append(e.args[0])
IndexError: tuple index out of range
```

Patch:
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index ca89f26..0000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,33 +0,0 @@
-### Actual Behavior
-
-```python
-# Paste a minimal piece of code that causes the problem. Here is an example:
-
->>> import wtforms
->>> class F(wtforms.Form):
-...     foo = wtforms.StringField()
->>> f = F(foo="bar")
->>> print(f.foo.shrug)
-Traceback (most recent call last):
-  File "<stdin>", line 1, in <module>
-AttributeError: 'StringField' object has no attribute 'shrug'
-```
-
-### Expected Behavior
-
-```python
-# Show us, and explain what behavior you would have expected.
-# Here is an example:
-
->>> import wtforms
->>> class F(wtforms.Form):
-...     foo = wtforms.StringField()
->>> f = F(foo="bar")
->>> print(f.foo.shrug)
-¯\_(ツ)_/¯
-```
-
-### Environment
-
-* Python version:
-* wtforms version:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
deleted file mode 100644
index 9945944..0000000
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ /dev/null
@@ -1,15 +0,0 @@
-Describe the issue you are attempting to fix.
-
-Link to any relevant issues or pull requests.
-
-Describe what this patch does to fix the issue.
-
-<!--
-Commit checklist:
-
-* add tests that fail without the patch
-* ensure all tests pass with ``pytest``
-* add documentation to the relevant docstrings or pages
-* add ``versionadded`` or ``versionchanged`` directives to relevant docstrings
-* add a changelog entry if this patch changes code
--->
diff --git a/CHANGES.rst b/CHANGES.rst
index 3ee928e..5c16afb 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -69,6 +69,9 @@ Unreleased
 -   form.errors is not cached and will update if an error is appended to a field after access. (:pr:`568`)
 -   :class:`~wtforms.validators.NumberRange` correctly handle *not a number*
     values. (:pr:`505`, :pr:`548`)
+-   :class:`ValueError` raised by a validator are handled like regular exceptions. validators
+    need to raise :class:`~wtforms.validators.ValidationError` or
+    :class:`~wtforms.validators.StopValidation` to make a validation fail (:pr:`445`, :pr:`551`).
 
 
 Version 2.2.1
diff --git a/src/wtforms/fields/core.py b/src/wtforms/fields/core.py
index 9afd795..3cd2c12 100644
--- a/src/wtforms/fields/core.py
+++ b/src/wtforms/fields/core.py
@@ -11,7 +11,7 @@ from wtforms import widgets
 from wtforms.compat import izip, text_type
 from wtforms.i18n import DummyTranslations
 from wtforms.utils import unset_value
-from wtforms.validators import StopValidation
+from wtforms.validators import StopValidation, ValidationError
 
 __all__ = (
     "BooleanField",
@@ -246,7 +246,7 @@ class Field(object):
             if e.args and e.args[0]:
                 self.errors.append(e.args[0])
             stop_validation = True
-        except ValueError as e:
+        except ValidationError as e:
             self.errors.append(e.args[0])
 
         # Run validators
@@ -257,7 +257,7 @@ class Field(object):
         # Call post_validate
         try:
             self.post_validate(form, stop_validation)
-        except ValueError as e:
+        except ValidationError as e:
             self.errors.append(e.args[0])
 
         return len(self.errors) == 0
@@ -277,7 +277,7 @@ class Field(object):
                 if e.args and e.args[0]:
                     self.errors.append(e.args[0])
                 return True
-            except ValueError as e:
+            except ValidationError as e:
                 self.errors.append(e.args[0])
 
         return False
@@ -553,7 +553,7 @@ class SelectField(SelectFieldBase):
                 if self.data == v:
                     break
             else:
-                raise ValueError(self.gettext("Not a valid choice"))
+                raise ValidationError(self.gettext("Not a valid choice"))
 
 
 class SelectMultipleField(SelectField):
@@ -591,7 +591,7 @@ class SelectMultipleField(SelectField):
             values = list(c[0] for c in self.choices)
             for d in self.data:
                 if d not in values:
-                    raise ValueError(
+                    raise ValidationError(
                         self.gettext("'%(value)s' is not a valid choice for this field")
                         % dict(value=d)
                     )


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 1532026..c415f3a 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -293,9 +293,9 @@ class PrePostTestField(StringField):
 
     def post_validate(self, form, stopped):
         if self.data == "p":
-            raise ValueError("Post")
+            raise validators.ValidationError("Post")
         elif stopped and self.data == "stop-post":
-            raise ValueError("Post-stopped")
+            raise validators.ValidationError("Post-stopped")
 
 
 class TestPrePostValidation:
@@ -1002,9 +1002,9 @@ class TestFieldList:
     def test_validators(self):
         def validator(form, field):
             if field.data and field.data[0] == "fail":
-                raise ValueError("fail")
+                raise validators.ValidationError("fail")
             elif len(field.data) > 2:
-                raise ValueError("too many")
+                raise validators.ValidationError("too many")
 
         F = make_form(a=FieldList(self.t, validators=[validator]))
 
diff --git a/tests/test_validators.py b/tests/test_validators.py
index a54a0c1..f7e0cc7 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -3,7 +3,7 @@ import decimal
 import pytest
 import re
 
-from wtforms import Label
+from wtforms import Label, Form, StringField
 from wtforms.compat import text_type
 from wtforms.validators import (
     StopValidation,
@@ -670,3 +670,17 @@ def test_regexp_message(dummy_form, dummy_field, grab_error_message):
     validator = regexp("^a", message="foo")
     dummy_field.data = "f"
     assert grab_error_message(validator, dummy_form, dummy_field) == "foo"
+
+
+@pytest.mark.parametrize("exc", [IndexError, ZeroDivisionError, ValueError])
+def test_raise_exceptions(exc):
+    def validate(form, field):
+        raise exc
+
+    class F(Form):
+        field = StringField(validators=[validate])
+
+    f = F()
+
+    with pytest.raises(exc):
+        f.validate()