Repository: wtforms/wtforms
Pull request #584 (2020-04-22 14:05:02)

fixed extra_validators parameter
[BaseForm.validate](https://wtforms.readthedocs.io/en/2.3.x/forms/#wtforms.form.BaseForm.validate) method could take a `extra_validators` parameter, but [Form.validate](https://wtforms.readthedocs.io/en/2.3.x/forms/#wtforms.form.Form.validate) could not.

If this is ok I would also like to backport it to branch 2.3.x


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index ee26299..c77cab6 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -13,6 +13,9 @@ Unreleased
 -   Restored ``HTMLString`` and ``escape_html`` as aliases for
     MarkupSafe functions. Their use shows a ``DeprecationWarning``.
     :issue:`581`, :pr:`583`
+-   ``Form.validate`` takes an ``extra_validators`` parameter, mapping
+    field names to lists of extra validator functions. This matches
+    ``BaseForm.validate``. :pr:`584`
 
 
 Version 2.3.0
diff --git a/wtforms/form.py b/wtforms/form.py
index d3ad6bd..2057b73 100644
--- a/wtforms/form.py
+++ b/wtforms/form.py
@@ -292,15 +292,27 @@ class Form(with_metaclass(FormMeta, BaseForm)):
             else:
                 super(Form, self).__delattr__(name)
 
-    def validate(self):
-        """
-        Validates the form by calling `validate` on each field, passing any
-        extra `Form.validate_<fieldname>` validators to the field validator.
+    def validate(self, extra_validators=None):
+        """Validate the form by calling ``validate`` on each field.
+        Returns ``True`` if validation passes.
+
+        If the form defines a ``validate_<fieldname>`` method, it is
+        appended as an extra validator for the field's ``validate``.
+
+        :param extra_validators: A dict mapping field names to lists of
+            extra validator methods to run. Extra validators run after
+            validators passed when creating the field. If the form has
+            ``validate_<fieldname>``, it is the last extra validator.
         """
-        extra = {}
+        if extra_validators is not None:
+            extra = extra_validators.copy()
+        else:
+            extra = {}
+
         for name in self._fields:
             inline = getattr(self.__class__, 'validate_%s' % name, None)
+
             if inline is not None:
-                extra[name] = [inline]
+                extra.setdefault(name, []).append(inline)
 
         return super(Form, self).validate(extra)


Test patch:
diff --git a/tests/form.py b/tests/form.py
index 0710a60..6fd12f5 100644
--- a/tests/form.py
+++ b/tests/form.py
@@ -151,6 +151,23 @@ class FormTest(TestCase):
         form = self.F()
         self.assertEqual(form.validate(), False)
 
+    def test_validate_with_extra(self):
+        class F2(self.F):
+            other = TextField()
+
+        def extra(form, field):
+            if field.data != "extra":
+                raise ValidationError("error")
+
+        form = F2(test="foobar", other="extra")
+        assert form.validate(extra_validators={"other": [extra]})
+
+        form = F2(test="foobar", other="nope")
+        assert not form.validate(extra_validators={"other": [extra]})
+
+        form = F2(test="nope", other="extra")
+        assert not form.validate(extra_validators={"other": [extra]})
+
     def test_field_adding_disabled(self):
         form = self.F()
         self.assertRaises(TypeError, form.__setitem__, 'foo', TextField())