DEV Community

Cover image for Python: WTForms 3.0.1 customising built-in error messages.
Be Hai Nguyen
Be Hai Nguyen

Posted on

Python: WTForms 3.0.1 customising built-in error messages.

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
Enter fullscreen mode Exit fullscreen mode

This method gets called when we instantiate a Form with data to be validated.

self.gettext("Not a valid integer value.")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.']}
Enter fullscreen mode Exit fullscreen mode

But I get:

{'hired_date': ['Not a valid date value.'],
 'id': ['Not a valid integer value.', 'Number must be between 1 and 32767.']}
Enter fullscreen mode Exit fullscreen mode

👎 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()
Enter fullscreen mode Exit fullscreen mode

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.']}
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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.']}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.']}
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (0)