Pull to refresh

Используем Python для обработки HTML форм

Reading time6 min
Views6.1K
Когда я только начинал пользоваться django, самым приятным моментом после ORM, для меня, был пакет django.forms. Теперь django в прошлом — я использую стэк Werkzeug + SqlAlchemy + Jinja2, а иногда даже пытаюсь вместо SqlAlchemy экспериментировать с нереляционными хранилищами данных. Но вот замену django.forms я так и не нашёл. Поэтому решил набросать по-быстренькому что-нибудь своё.

В итоге, я пришёл примерно к следующему описанию. На входе мы имеем данные представленные типом dict, причём ключами этого словарика являются строки, а значениями — строки или другие словарики, той же структуры. Например:

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


Далее у нас есть некоторые предположения относительно этих данных — какой-то набор правил, который мы будем называть схемой. Теперь нам нужен способ пройтись по всем полям словарика с данными и проверить их значение на правильность, а также привести к нужным типам. Всё просто!

Отсюда вытекают вполне понятные требования к реализации:

* Простой способ описания схем — хочется, чтобы это было наглядно и удобно, то есть декларативно.
* Переиспользование кода — довольно нудно по 10 раз описывать одни и те же схемы.
* Определение схем для вложенных структур данных — и это может понадобиться.

Основные принципы реализации
Базовые принципы

Предполагается, что ошибка валидации данных будет описываться следующим исключением:

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


Валидация данных, это практически анализ типов данных, поэтому я считаю уместным наследоваться от стандартного исключения TypeError.

Схема будет задаваться ввиде класса, аттрибутами которого будут объекты, описывающие поля. Так как мы хотим описывать вложенные конструкции, то аттрибутами у нас могут быть как объекты строковых полей, так и другие схемы. Вот что получается на первом этапе:

class SchemaElement(object):
    u"""
    Абстрактный класс для элемента схемы.
    """
    def validate(self, data):
        raise NotImplementedError()

class Field(SchemaElement):
   u"""
   Класс Field описывает строковое поле.
   """
   def validate(self, data):
       raise SchemaValidationError("not valid value")

class Schema(SchemaElement):
   u"""
   Класс Schema описывает схему валидации.
   """
   def validate(self, data):
       # Код валидации данных data
       return data


Так как элементом схемы может быть как поле, так и другая схема, я наследовал Field и Schema от общего класса SchemaElement. Это шаблон проектирования composite, он отлично подходит для описания иерархических типов данных.

Также SchemaElement определяет абстрактный интерфейс для валидации — метод validate. Дело в том, что теперь следуя этому интерфейсу, мы можем не различать объекты Field и Schema с точки зрения валидации, для нас это одно и тоже.

Наследники класса Field будут использоваться для описания полей схемы, то есть для обработки строковых значений. Для того, чтобы реализовать алгоритм валидации данных для конкретного поля, нужно просто переопределить метод validate, который будет возвращать правильные и приведённые данные data или выкидывать исключение SchemaValidationError в случае ошибки. Реализация по-умолчания всегда будет выкидывать исключение.

Класс Schema будет использоваться для описания структуры состоящей из полей и другим схем. Код метода validate будет представлен чуть позднее.
Декларативное описание схем

Как я уже говорил, наиболее удачным мне кажется задание схем ввиде класса, аттрибутами которого являются другие объекты Field и Schema. Это называется декларативное описание. Чтобы это реализовать нам понадобиться метакласс для класса-контейнера Schema:

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__()


Основная причина почему я использую этот метакласс — это желание сгруппировать все поля схемы вместе и поместить в аттрибут __fields__. Это будет удобно при обработке полей или интроспекции структуры, так как __fields__ не содержит лишнего мусора, как если бы мы каждый раз обходили __dict__.

Если мы создаём класс с именем Schema, то метакласс никак не будет обрабатывать его, если же это другой класс, наследующийся от Schema, то сначала он соберёт в __fields__ все поля суперклассов в порядке справа-налево и потом добавит туда поля текущего класса.

Также я добавил методы __contains__, который будет проверять содержится ли поле с данным именем внутри схемы, и метод __iter__, что делает класс со схемой итерируемым. Напомню, что так как мы опредили эти методы у метакласса, то мы получаем методы класса, что эквивалентно применению декоратора classmethod на методы объекта.

Теперь осталоcь добавить аттрибут __metaclass__ в класс Schema:

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


Мы уже можем определять схемы следующим образом:

>>> 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


Наследование схем работает — аттрибут my_field появился и у схемы AnotherSchema. Чтобы создать схему для валидации иерархических структур данных, нужно просто добавить аттрибутом схемы другую схему:

>>> 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


Валидация данных

Валидация выполняется методом validate, объекты класса Field должны сами переопредилить его, реализацию же метода validate у класса Schema я привожу тут:

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
   ...


Сначала у каждого поля схемы вызывается метод validate c нужным параметром из словаря data. Если есть ошибка, она ловится и сохраняется в словаре errors. После того, как мы обошли все поля, проверяется словарик errors, и если он не пуст, то выкидывается исключение SchemaValidationError с этим словариком ввиде параметра. Это позволяет нам собрать все ошибки, начиная с самого нижнего уровня в иерархии.

Теперь можно попробовать определить несколько базовых полей и схем и попробовать валидацию данных в действии:

class NotEmptyField(Field):
    u"""
    Класс описывающий поле, которое не может быть пустым.
    """
    def validate(self, data):
        print "Валидация поля"
        if not data:
            raise SchemaValidationError("empty field")

class CustomSchema(Schema):
    not_empty_field = NotEmptyField()

    def validate(self, data):
        print "Валидацию полей схемы"
        data = super(CustomSchema, self).validate(data)
        print "Код валидации на уровне схемы"
        return data


Внутри метода validate мы должны обязательно вызвать метод validate суперкласса. Также обязательно необходимо вернуть data или выкинуть исключение SchemaValidationError. Проверим нашу форму в деле:

>>> schema = CustomSchema()
>>> try:
...     schema.validate({ "not_empty_field": "some value" })
... except SchemaValidationError, e:
...     errors = e.error
Валидацию полей схемы
Валидация поля
Код валидации на уровне схемы
>>> schema.errors
{}


Теперь попробуем предоставить на валидацию неверные данные:

>>> try:
...     schema.validate({ "not_empty_field": "" })
... except SchemaValidationError, e:
...     errors = e.error
Сначала сделаем валидацию полей схемы
Валидация поля
>>> errors
{ "not_empty_field": "empty field" }


Как и предполагалось, валидация данных завершилась ошибкой.
Заключение

И так, мы имеем маленькую, но уже достаточно мощную библиотечку для валидации данных. Конечно необходимо пополнить её необходимыми полями (классами-наследниками Field). Кстати получилось довольно компактно — не более 130 строк. Если есть желание получить исходный код, вы можете написать мне.
Tags:
Hubs:
Total votes 9: ↑8 and ↓1+7
Comments4

Articles