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

1

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:

Reinvent your career. Join DEV.

It takes one minute and is worth it for your career.

Get started

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

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay