📜 ⬆️ ⬇️

Use Python to process HTML forms.

When I first started using django, the most enjoyable moment after ORM, for me, was the django.forms package. Now django is in the past - I use the Werkzeug + SqlAlchemy + Jinja2 stack, and sometimes I even try to experiment with non-relational data stores instead of SqlAlchemy. But I did not find a replacement for django.forms. Therefore, I decided to sketch out something of my own.

As a result, I came to about the following description. At the input we have the data represented by the type dict, and the keys of this dictionary are strings, and the values ​​are strings or other dictionaries of the same structure. For example:

 data = {
     "key1": "value1"
     "key2": {
         "key3": "value3"
     }
 }


Next, we have some assumptions about this data - some set of rules, which we will call a schema. Now we need a way to go through all the fields of the dictionary with the data and check their value for correctness, and also lead to the necessary types. It's simple!
')
This leads to quite understandable implementation requirements:

* A simple way to describe the schemes - I want it to be visual and convenient, that is, declarative.
* Reuse code - quite tedious to describe the same schemes 10 times.
* Defining schemas for nested data structures - and this may be needed.

Basic principles of implementation
Basic principles

It is assumed that the data validation error will be described by the following exception:

 class SchemaValidationError (TypeError):
    def __init __ (self, error):
        self.error = error


Data validation is practically an analysis of data types, so I consider it appropriate to inherit from the standard TypeError exception.

The scheme will be defined as a class whose attributes will be objects describing the fields. Since we want to describe nested constructions, we can have attributes of string fields as well as other schemes with attributes. This is what happens in the first stage:

 class SchemaElement (object):
     u "" "
     Abstract class for the schema element.
     "" "
     def validate (self, data):
         raise NotImplementedError ()

 class Field (SchemaElement):
    u "" "
    The Field class describes a string field.
    "" "
    def validate (self, data):
        raise SchemaValidationError ("not valid value")

 class Schema (SchemaElement):
    u "" "
    The Schema class describes a validation scheme.
    "" "
    def validate (self, data):
        # Data validation code data
        return data


Since the schema element can be either a field or another scheme, I inherited Field and Schema from the general class SchemaElement. This is a composite design pattern; it is great for describing hierarchical data types.

SchemaElement also defines an abstract interface for validation - the validate method. The fact is that now, following this interface, we can not distinguish between Field and Schema objects from the point of view of validation, for us this is the same thing.

Field class heirs will be used to describe schema fields, that is, to handle string values. In order to implement the data validation algorithm for a specific field, you simply need to override the validate method, which will return the correct and provided data data or throw a SchemaValidationError exception in case of an error. The default implementation will always throw an exception.

The Schema class will be used to describe the structure consisting of fields and other schemas. The code of the validate method will be presented a little later.
Declarative schema description

As I have already said, the most successful for me is the task of defining schemas in the form of a class whose attributes are other Field and Schema objects. This is called a declarative description. To implement this, we need a metaclass for the Schema container class:

 class SchemaMeta (type):
    def __new __ (mcs, name, bases, attrs):
        if not name == "Schema":
            fields = {}
            for base in reversed (bases):
                if issubclass (base, Schema) and not base is Schema:
                    fields.update (base .__ fields__)
            for field_name, field in attrs.items ():
                if isinstance (field, SchemaElement):
                    fields [field_name] = attrs [field_name]
            attrs ["__ fields__"] = fields
        cls = type .__ new __ (mcs, name, bases, attrs)
        return cls

    def __contains __ (cls, value):
        return value in cls .__ fields__

    def __iter __ (cls):
        return cls .__ fields __. items () .__ iter __ ()


The main reason why I use this metaclass is to group all the fields of the scheme together and put it in the __fields__ attribute. This will be useful when processing fields or introspecting the structure, since __fields__ does not contain unnecessary garbage, as if we bypass __dict__ every time.

If we create a class with the name Schema, then the metaclass will not process it in any way, if it is another class inheriting from Schema, it will first collect all fields of superclasses in right-to-left order in __fields__ and then add fields of the current class there.

I also added the __contains__ methods, which will check if the field with the given name is inside the schema, and the __iter__ method, which makes the class with the schema iterable. Let me remind you that since we defined these methods at the metaclass, we get class methods, which is equivalent to applying the classmethod decorator to object methods.

It remains to add the attribute __metaclass__ to the Schema class:

 class Schema (SchemaElement):
     ...
     __metaclass__ = SchemaMeta
     ...


We can already define schemas as follows:

 >>> class MySchema (Schema):
 ... my_field = Field ()

 >>> class AnotherSchema (MySchema):
 ... another_field = Field ()

 >>> "my_field" in MySchema
 True
 >>> "another_field" in AnotherSchema
 True
 >>> "my_field" in AnotherSchema
 True


Inheritance of schemes works - the attribute My_field appeared in the scheme AnotherSchema. To create a schema for validating hierarchical data structures, simply add another schema attribute with the schema attribute:

 >>> class CompositeSchema (Schema):
         sub_schema = MySchema ()
         my_field = Field ()

 >>> "my_field" in CompositeSchema
 True
 >>> "sub_schema" in CompositeSchema
 True
 >>> "my_field" in CompositeSchema.sub_schema
 True


Data validation

Validation is performed by the validate method, objects of the Field class should redefine it themselves, the implementation of the same validate method of the Schema class I quote here:

 class Schema (SchemaElement):
    ...
    def validate (self, data):
        errors = {}
        for field_name, field in self .__ fields __. items ():
            try:
                data [field_name] = field.validate (data.get (field_name, None))
            except SchemaValidationError, error:
                errors [field_name] = error.error
        if errors:
            raise SchemaValidationError (errors)
        return data
    ...


First, each field of the scheme is called the validate method with the necessary parameter from the data dictionary. If there is an error, it is caught and stored in the errors dictionary. After we have bypassed all the fields, the errors dictionary is checked, and if it is not empty, the SchemaValidationError exception is thrown with this dictionary as a parameter. This allows us to collect all the errors, starting from the lowest level in the hierarchy.

Now you can try to define several basic fields and schemas and try validating the data in action:

 class NotEmptyField (Field):
     u "" "
     A class that describes a field that cannot be empty.
     "" "
     def validate (self, data):
         print "Validation fields"
         if not data:
             raise SchemaValidationError ("empty field")

 class CustomSchema (Schema):
     not_empty_field = NotEmptyField ()

     def validate (self, data):
         print "Validation of Scheme Fields"
         data = super (CustomSchema, self) .validate (data)
         print "Validation Code at Schema Level"
         return data


Inside the validate method, we must call the validate method of the superclass. Also, be sure to return the data or throw a SchemaValidationError exception. Let's check our form in:

 >>> schema = CustomSchema ()
 >>> try:
 ... schema.validate ({"not_empty_field": "some value"})
 ... except SchemaValidationError, e:
 ... errors = e.error
 Validation of schema fields
 Field validation
 Validation code at schema level
 >>> schema.errors
 {}


Now we will try to provide invalid data for validation:

 >>> try:
 ... schema.validate ({"not_empty_field": ""})
 ... except SchemaValidationError, e:
 ... errors = e.error
 First, let's validate the schema fields.
 Field validation
 >>> errors
 {"not_empty_field": "empty field"}


As expected, data validation failed.
Conclusion

And so, we have a small but already powerful enough library for data validation. Of course, you must replenish it with the necessary fields (Field successor classes). By the way, it turned out pretty compact - no more than 130 lines. If you want to get the source code, you can write to me.

Source: https://habr.com/ru/post/49949/


All Articles