DEV Community

Cover image for Python: WTForms 3.0.1 IntegerField and InputRequired do not accept 0 as valid!
Be Hai Nguyen
Be Hai Nguyen

Posted on

Python: WTForms 3.0.1 IntegerField and InputRequired do not accept 0 as valid!

0 is a valid integer value. In the latest version of WTForms, version 3.0.1, IntegerField and InputRequired don't accept 0 as valid. This appears to be an ongoing issue dating back several years. I am proposing a patch, which seems to be working for me.

WTForms 3.0.1 is a great validation library. And I think it is also framework-independent: we can implement our own generic business rules classes, and use it as a basic data validation engine, we can then use these business rules classes in any framework of our own choosing.

I have recently found an issue with it, my form has an IntegerField, and the InputRequired validator, whereby 0 is an acceptable value.

-- But 0 gets rejected!

This issue has been reported over the years, but so far, it is still in this latest version:

Following is my attempt to reproduce this issue, and how to get around it. ❶ Create project virtual environment. ❷ Write a test script.

❶ Create project virtual environment. We will need Werkzeug and WTForms 3.0.1 packages.

The test project lives under /webwork/wtforms_test:

$ cd webwork/
$ mkdir wtforms_test
$ cd wtforms_test/
behai@HP-Pavilion-15:~/webwork/wtforms_test$ virtualenv venv
behai@HP-Pavilion-15:~/webwork/wtforms_test$ source venv/bin/activate
(venv) behai@HP-Pavilion-15:~/webwork/wtforms_test$ ./venv/bin/pip install werkzeug WTForms
Enter fullscreen mode Exit fullscreen mode

061-01.png

❷ Test script.

Content of /webwork/wtforms_test$/wtforms_bug.py
Enter fullscreen mode Exit fullscreen mode
from pprint import pprint

from werkzeug.datastructures import MultiDict

from wtforms import (    
    Form,
    IntegerField,
)

from wtforms.validators import (
    InputRequired,
    NumberRange,
)

BREAK_HOUR_01_MSG = "Break Hour must have a value."
BREAK_HOUR_02_MSG = "Break Hour is between 0 and 23."

class TestForm(Form):
    break_hour = IntegerField('Break Hour', validators=[InputRequired(BREAK_HOUR_01_MSG), 
                                                        NumberRange(0, 23, BREAK_HOUR_02_MSG)])

def validate(data: dict, form: Form) -> tuple():
    form_data = MultiDict(mapping=data)

    f = form(form_data)
    res = f.validate()
    return res, f.errors

print("\n--break_hour: -1")
res, errors = validate({'break_hour': -1}, TestForm)
print(res)
pprint(errors)

print("\n--break_hour: 'xx'")
res, errors = validate({'break_hour': 'xx'}, TestForm)
print(res)
pprint(errors)

print("\n--break_hour: 0")
res, errors = validate({'break_hour': 0}, TestForm)
print(res)
pprint(errors)

print("\n--break_hour: 1")
res, errors = validate({'break_hour': 1}, TestForm)
print(res)
pprint(errors)
Enter fullscreen mode Exit fullscreen mode

It is simple, the form has only a single integer field break_hour, it is a required field, and accepts any value in the range of 0 to 23 -- and follows by 4 ( four ) tests.

To run:

(venv) behai@HP-Pavilion-15:~/webwork/wtforms_test$ venv/bin/python wtforms_bug.py
Enter fullscreen mode Exit fullscreen mode

Output:

--break_hour: -1
False
{'break_hour': ['Break Hour is between 0 and 23.']}

--break_hour: 'xx'
False
{'break_hour': ['Not a valid integer value.',
                'Break Hour is between 0 and 23.']}

--break_hour: 0
False
{'break_hour': ['Break Hour must have a value.']}

--break_hour: 1
True
{}
Enter fullscreen mode Exit fullscreen mode

061-02.png

-1 and 'xx' get rejected, which are correct. But 0 gets rejected is a bug: 0 is a valid value.

I traced the issue to InputRequired, I am printing the code for this class below:

InputRequired in ./venv/lib/python3.10/site-packages/wtforms/validators.py
Enter fullscreen mode Exit fullscreen mode
class InputRequired:
    """
    Validates that input was provided for this field.

    Note there is a distinction between this and DataRequired in that
    InputRequired looks that form-input data was provided, and DataRequired
    looks at the post-coercion data.

    Sets the `required` attribute on widgets.
    """

    def __init__(self, message=None):
        self.message = message
        self.field_flags = {"required": True}

    def __call__(self, form, field):
        if field.raw_data and field.raw_data[0]:
            return

        if self.message is None:
            message = field.gettext("This field is required.")
        else:
            message = self.message

        field.errors[:] = []
        raise StopValidation(message)
Enter fullscreen mode Exit fullscreen mode

The issue is caused by the first line in def call(self, form, field):

    def __call__(self, form, field):
        if field.raw_data and field.raw_data[0]:
            return
Enter fullscreen mode Exit fullscreen mode

field.raw_data[0] is evaluated to 0, and it is a False, causing the whole if statement to evaluate to False.

Change it to:

        if field.raw_data and len(field.raw_data):
Enter fullscreen mode Exit fullscreen mode

And it will accept 0 as a value. The correct expected output is now:

--break_hour: -1
False
{'break_hour': ['Break Hour is between 0 and 23.']}

--break_hour: 'xx'
False
{'break_hour': ['Not a valid integer value.',
                'Break Hour is between 0 and 23.']}

--break_hour: 0
True
{}

--break_hour: 1
True
{}
Enter fullscreen mode Exit fullscreen mode

061-03.png

Right now, I am just having the change made locally. I am not sure what to do with it just yet. Thank you for reading. I hope this info is useful. Stay safe as always.

✿✿✿

Feature image sources:

Top comments (1)

Collapse
 
behainguyen profile image
Be Hai Nguyen

I ended up subclass InputRequired, and use InputRequiredEx in place of InputRequired:

class InputRequiredEx(InputRequired):
    def __call__(self, form, field):
        if field.raw_data and len(field.raw_data):
            return

        if self.message is None:
            message = field.gettext("This field is required.")
        else:
            message = self.message

        field.errors[:] = []
        raise StopValidation(message)
Enter fullscreen mode Exit fullscreen mode