3
\$\begingroup\$

I've started working on a small Flask project for no real reason other than fun, and pulled myself back on form validation. I realize that there are existing libraries like WTForms, but wanted to try and write something myself.

I tried to make the high-level interaction similar to that of SQLAlchemy, where you define models with column (in this case field) attributes and they get done 'automatically', but this is the first time I've tried writing something like this so I'm looking for criticisms primarily in the FormDeserializer.__init__ (I've never had a practical purpose for dir and __dict__ so I'm unsure if my usage is acceptable)

import typing
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug import ImmutableMultiDict

class Field():
    class FieldLengthExceeded(Exception):
        pass

    class MissingFieldData(Exception):
        pass

    def __init__(self, name: str, *, strict: bool=False, max_length: int=1024):
        self.name: str = name
        self._value: typing.Optional[str] = None
        self._strict: bool = strict
        self._max_length: int = max_length

    @property
    def value(self) -> typing.Optional[str]:
        return self._value

    @value.setter
    def value(self, value):
        if value is not None and len(value) > self._max_length:
            raise self.FieldLengthExceeded
        else:
            self._value = value

    def validate(self):
        if self._value is None and self._strict:
            raise self.MissingFieldData
        elif self._value is not None:
            self._type_specific_validation()

    def _type_specific_validation(self):
        pass


class IntegerField(Field):
    class InvalidInteger(Exception):
        pass

    def _type_specific_validation(self):
        if not all([x in '0123456789' for x in self._value]):
            raise self.InvalidInteger

    @Field.value.getter
    def value(self) -> typing.Optional[int]:
        return self._value if self._value is None else int(self._value)


class PasswordField(Field):
    class Password(str):
        def __eq__(self, comparator):
            return check_password_hash(self, comparator)

    @Field.value.getter
    def value(self) -> typing.Optional[Password]:
        return self._value if self._value is None else self.Password(generate_password_hash(self._value))



class EmailField(Field):
    class InvalidEmail(Exception):
        pass

    def _type_specific_validation(self):
        if '@' not in self._value:
            raise self.InvalidEmail


class FormDeserializer():
    def __init__(self, data: ImmutableMultiDict):
        for attrib in dir(self):
            d = getattr(self, attrib)
            if isinstance(d, Field):
                # Get the attribute from the form data
                v: typing.Optional[str] = data.get(d.name, None)

                d.value = v

                # Check the value is valid
                d.validate()

                # Finally, rebind to the value of the field
                self.__dict__[attrib] = d.value

### Example ###
class TestForm(FormDeserializer):
    username = Field('username', strict=False, max_length=24)
    password = PasswordField('password', strict=True)

    email = EmailField('email')

data = {'username': 'jellywx', 'password': 'hello world', 'email': 't@tm.tm'}
f = TestForm(data)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.