Repository: wtforms/wtforms (python)

Issue:
Form errors property isn't idempotent
The 'errors' property in BaseForm isn't idempotent. In particular - if you iterate over a forms attributes using e.g. inspect.getmembers - this will cause 'errors' to change from 'None' to dict({}). And so it won't be updated if any fields add/incur errors later in processing.

At a minimum, it should treat 'None' and empty dir the same. But I don't understand why it doesn't re-calc the errors every time?

Pull request description:
Avoid to cache form errors
Caching form errors may cause some strange behaviors when a form `errors` attribute is accessed during validation, and performance benefits are not obvious.

This should fix #522 

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 894caad..a8f47a4 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -66,6 +66,7 @@ Unreleased
 -   Choices which name and data are the same do not need to use tuples. (:pr:`526`)
 -   :class:`~validators.URL` validator now allows query parameters in the URL. (:pr:`523`, :pr:`524`).
 -   Updated French and Japanese translations. (:pr:`514`, :pr:`506`)
+-   form.errors is not cached and will update if an error is appended to a field after access. (:pr:`568`)
 
 
 Version 2.2.1
diff --git a/docs/forms.rst b/docs/forms.rst
index 0e4ddee..e6b32f8 100644
--- a/docs/forms.rst
+++ b/docs/forms.rst
@@ -47,10 +47,6 @@ The Form class
         A dict containing a list of errors for each field. Empty if the form
         hasn't been validated, or there were no errors.
 
-        Note that this is a lazy property, and will only be generated when you
-        first access it. If you call :meth:`validate` after accessing it, the
-        cached result will be invalidated and regenerated on next access.
-
     .. attribute:: meta
 
         This is an object which contains various configuration options and also
diff --git a/src/wtforms/form.py b/src/wtforms/form.py
index 6e58388..8697314 100644
--- a/src/wtforms/form.py
+++ b/src/wtforms/form.py
@@ -31,7 +31,6 @@ class BaseForm(object):
 
         self.meta = meta
         self._prefix = prefix
-        self._errors = None
         self._fields = OrderedDict()
 
         if hasattr(fields, "items"):
@@ -126,7 +125,6 @@ class BaseForm(object):
 
         Returns `True` if no errors occur.
         """
-        self._errors = None
         success = True
         for name, field in iteritems(self._fields):
             if extra_validators is not None and name in extra_validators:
@@ -143,11 +141,7 @@ class BaseForm(object):
 
     @property
     def errors(self):
-        if self._errors is None:
-            self._errors = dict(
-                (name, f.errors) for name, f in iteritems(self._fields) if f.errors
-            )
-        return self._errors
+        return {name: f.errors for name, f in self._fields.items() if f.errors}
 
 
 class FormMeta(type):


Test patch:
diff --git a/tests/test_form.py b/tests/test_form.py
index ce17bdd..e1d5884 100644
--- a/tests/test_form.py
+++ b/tests/test_form.py
@@ -5,7 +5,7 @@ import pytest
 from wtforms.form import BaseForm, Form
 from wtforms.meta import DefaultMeta
 from wtforms.fields import StringField, IntegerField
-from wtforms.validators import ValidationError
+from wtforms.validators import ValidationError, DataRequired
 from tests.common import DummyPostData
 
 
@@ -230,6 +230,21 @@ class TestForm:
         assert F(DummyPostData(), test="test").test.data == "processed"
         assert F(DummyPostData({"test": "foo"}), test="test").test.data == "foo"
 
+    def test_errors_access_during_validation(self):
+        class F(Form):
+            foo = StringField(validators=[DataRequired()])
+
+            def validate(self):
+                super(F, self).validate()
+                self.errors
+                self.foo.errors.append("bar")
+                return True
+
+        form = F(foo="whatever")
+        form.validate()
+
+        assert {"foo": ["bar"]} == form.errors
+
 
 class TestMeta:
     class F(Form):