pythonflask-wtformswtforms

Field validator calls a parser - how to save the parsed value?


A form has a field. The field has a validator. The validator calls a parser. If and only if the parsing succeeds, the validation is successful.

However, later I have to call the parser again to get the parsed value. How can I avoid this duplication? Should I attach the result to the field object as a new attribute?

Simplified Flask code:

def data_validator(form, field):
    try:
        # first parser call, but the result will be lost
        result = parse(field.data)
    except Exception as err:
         raise ValidationError(f"Invalid data: {err}") from None

class DataForm(FlaskForm):
    datafield = wtf.fields.StringField('Enter data', validators=[data_validator])
    ...

@app.post('/some/url')
def view_function():
    form = DataForm()
    if form.validate_on_submit():
        # second parser call
        result = parse(form.datafield.data)
    ...


Solution

  • In the name of "(...) Although practicality beats purity." (one of Python's guiding motos when you tipe import this - I'd say, yes, just attach the parser result to the field instance itself:

    def data_validator(form, field):
        try:
            result = parse(field.data)  # <-- first parser call, but the result will be lost
        except Exception as err:
             raise ValidationError(f"Invalid data: {err}") from None
        field.cooked = result
    
    

    And go on with life. This is one of the advantages of Python being a dynamic language.

    However, if you are using static type annotation in this project, the type checker tool will complain about this: static type checking is fundamentally incompatible with dynamic aspects of the language. The easiest way to overcome that is telling the static checker to ignore this assignment and subsequent reading from this attribute. You may want to make use of typing.Cast when reading the attribute back, or even having a separate annotated field class you can cast the field too, so that the static checker is aware of the new parameter (.cooked in the example above).

    Other ways to do that would be: mark the parse function as cached with functools.cache - that way, you'd have to write the call to parse, but the results wouldn't be re-calculated. I am not sure that would work because I think it'd require field to be hashable.

    Another way would be creating a cache yourself, preferably using contextvars.Contextvar: this would be more complex, but under certain points of view, it would be more "correct" than adding the value as a new attribute on the field instance.