Be Hai Nguyen
Posted on April 29, 2023
An approach to customise WTForms 3.0.1 built-in error messages before (possible) translation and sending back to the callers.
I am using WTForms 3.0.1 to do basic data validations, and I would like to customise built-in messages before sending them back to the callers; or possibly translating them into other languages, then sending back to the callers.
-- An example of such built-in message is “Not a valid integer value.”
And by customising, I mean making them a bit clearer, such as prefix them with the field label. E.g.:
● Employee Id: Not a valid integer value.
These built-in messages are often exception messages raised by the method def process_formdata(self, valuelist):
of the appropriate field class. The message Not a valid integer value.
comes from class IntegerField(Field)
, in the numeric.py
module:
def process_formdata(self, valuelist):
if not valuelist:
return
try:
self.data = int(valuelist[0])
except ValueError as exc:
self.data = None
raise ValueError(self.gettext("Not a valid integer value.")) from exc
This method gets called when we instantiate a Form
with data to be validated.
self.gettext("Not a valid integer value.")
is related to internationalisation. It is the def gettext(self, string):
method in class DummyTranslations:
, in module i18n.py
.
In class DummyTranslations:
, def gettext(self, string):
just returns the string as is, i.e. Not a valid integer value.
According to Internationalization (i18n) | Writing your own translations provider -- we can replace class DummyTranslations:
with our own, following the example thus given, I came up with the following test script:
from pprint import pprint
from wtforms import Form
from wtforms import (
IntegerField,
DateField,
)
from wtforms.validators import (
InputRequired,
NumberRange,
)
from werkzeug.datastructures import MultiDict
class MyTranslations(object):
def gettext(self, string):
return f"MyTranslations: {string}"
def ngettext(self, singular, plural, n):
if n == 1:
return f"MyTranslations: {singular}"
return f"MyTranslations: {plural}"
class MyBaseForm(Form):
def _get_translations(self):
return MyTranslations()
class TestForm(MyBaseForm):
id = IntegerField('Id', validators=[InputRequired('Id required'), NumberRange(1, 32767)])
hired_date = DateField('Hired Date', validators=[InputRequired('Hired Date required')],
format='%d/%m/%Y')
form_data = MultiDict(mapping={'id': '', 'hired_date': ''})
form = TestForm(form_data)
res = form.validate()
assert res == False
pprint(form.errors)
Basically, I just prepend built-in messages with MyTranslations:
, so that the above message would come out as “MyTranslations: Not a valid integer value.”
.
class MyBaseForm(Form):
is taken from the documentation. In class TestForm(MyBaseForm)
, there are two required fields, an integer field and a date field. The rest of the code is pretty much run-of-the-mill Python code.
The output I expected is:
{'hired_date': ['MyTranslations: Not a valid date value.'],
'id': ['MyTranslations: Not a valid integer value.',
'MyTranslations: Number must be between 1 and 32767.']}
But I get:
{'hired_date': ['Not a valid date value.'],
'id': ['Not a valid integer value.', 'Number must be between 1 and 32767.']}
👎 It is still using the default DummyTranslations
class -- I am certain, because I did trace into it. The documentation appears misleading.
I could not find any solution online, in fact there is very little discussion on this topic. I finally able to correct class MyBaseForm(Form):
following class FlaskForm(Form):
in the Flask-WTF library:
from wtforms.meta import DefaultMeta
...
class MyBaseForm(Form):
class Meta(DefaultMeta):
def get_translations(self, form):
return MyTranslations()
The rest of the code remains unchanged. I now got the output I expected, which is:
{'hired_date': ['MyTranslations: Not a valid date value.'],
'id': ['MyTranslations: Not a valid integer value.',
'MyTranslations: Number must be between 1 and 32767.']}
How do I replace MyTranslations
with respective field labels, Id
and Hired Date
in this case? We know method def gettext(self, string)
of the MyTranslations class
gets called from the fields, but def gettext(self, string)
does not have any reference to calling field instance.
Enable to find anything from the code. I finally choose to use introspection, that is, getting the caller information at runtime. The caller in this case is the field instance. Please note, it is possible that Python introspection might not always work.
MyTranslations class
gets updated as follows:
import inspect
...
class MyTranslations(object):
def gettext(self, string):
caller = inspect.currentframe().f_back.f_locals
return f"{caller['self'].label.text}: {string}"
def ngettext(self, singular, plural, n):
caller = inspect.currentframe().f_back.f_locals
if n == 1:
return f"{caller['self'].label.text}: {singular}"
return f"{caller['self'].label.text}: {plural}"
And this is the output I am looking for:
{'hired_date': ['Hired Date: Not a valid date value.'],
'id': ['Id: Not a valid integer value.',
'Id: Number must be between 1 and 32767.']}
Complete listing of the final test script:
from pprint import pprint
import inspect
from wtforms import Form
from wtforms.meta import DefaultMeta
from wtforms import (
IntegerField,
DateField,
)
from wtforms.validators import (
InputRequired,
NumberRange,
)
from werkzeug.datastructures import MultiDict
class MyTranslations(object):
def gettext(self, string):
caller = inspect.currentframe().f_back.f_locals
return f"{caller['self'].label.text}: {string}"
def ngettext(self, singular, plural, n):
caller = inspect.currentframe().f_back.f_locals
if n == 1:
return f"{caller['self'].label.text}: {singular}"
return f"{caller['self'].label.text}: {plural}"
class MyBaseForm(Form):
class Meta(DefaultMeta):
def get_translations(self, form):
return MyTranslations()
class TestForm(MyBaseForm):
id = IntegerField('Id', validators=[InputRequired('Id required'), NumberRange(1, 32767)])
hired_date = DateField('Hired Date', validators=[InputRequired('Hired Date required')],
format='%d/%m/%Y')
form_data = MultiDict(mapping={'id': '', 'hired_date': ''})
form = TestForm(form_data)
res = form.validate()
assert res == False
pprint(form.errors)
Please note that I am not sure if this is the final solution for what I want to achieve, for me, it is interesting regardless: I finally know how to write my own translation class (I am aware that MyTranslations class
in this post might not be at all a valid implementation.)
Furthermore, we could always argue that, since the final errors dictionary does contain field names, i.e. hired_date
and id
:
{'hired_date': ['Hired Date: Not a valid date value.'],
'id': ['Id: Not a valid integer value.',
'Id: Number must be between 1 and 32767.']}
We could always use field names to access field information from the still valid form instance, and massage the error messages. Translating into other languages can still take place -- but prior via translation also. But I don't like this approach. Regardless of the validity of this post, I did enjoy investigating this issue.
Thank you for reading. I hope you could somehow use this information... Stay safe as always.
✿✿✿
Feature image sources:
- https://in.pinterest.com/pin/337277459600111737/
- https://www.omgubuntu.co.uk/2022/09/ubuntu-2210-kinetic-kudu-default-wallpaper
- https://seeklogo.com/vector-logo/332789/python
- https://github.com/wtforms/wtforms/issues/569, https://user-images.githubusercontent.com/19359364/116413884-4b4e7500-a838-11eb-83b0-704ebb3454b0.png
Posted on April 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.