wtforms 源码阅读

Author Avatar
呃哦 4月 04, 2020

问题描述

在使用 wtforms 时遇到个问题,对于嵌套接口数据,wtforms 表示用FormField 对已有的表单进行复用。

期望的数据模型:

期望的wtforms 代码:

示例表单

然而,wtforms并不按期望中的代码获取表单值。官方文档也没写清如何获取,遂,翻下源码看下如何解析的。

注:与本次问题无关的代码如 csrfhtml render 等功能暂不关心,因为接口走 RESTful 风格,不通过wtforms 生成表单。

解决步骤

概要

先概要讲下 wtforms 的表单处理过程:

  1. 通过自定义的 FormMeta 类,控制 Form 类的生成,生成类定义下的字段声明字典 _unbound_fields
  2. 通过 BaseForm_unbound_fields进行 表单字段 和form 实例的绑定,绑定包括
    • 表单字段在form中声明的name
    • 属性名的前缀
    • 字段的翻译文本等
  3. field 通过 process 方法获取在 form 中的 value,keyprefix+name
  4. 字段通过 validate 方法验证自身约束检查,其中,先验证自身的 validate 方法,再验证属性定义的 validators 验证器列表

源码分析

1. 获取 form 定义的字段列表

对于用户定义的 Form 类,通过python的元类特性,内省解析类定义的字段列表,此时该字段类尚未初始化,只是确定类的字段子弹 name->field_class,对应的代码在 wtforms/form.py185 行,获取类未绑定的字段列表

form.__call__

2. 对字段进行绑定初始化

  1. 流程走到的 Form 的实例初始化工作,先是通过 BaseForm__init__ 对上面元类获取到的类字段列表进行初始化,即将 Field 类初始化,绑定该 FieldForm 中定义的 name prefix等属性,相关代码在wtforms/form.py50

    BaseForm.__init__

  2. 对于上面的关键代码 field = meta.bind_field(self, unbound_field, options),对应 UnboundField 类的 bind_field 函数,跳转过去,可以看到,是返回该字段的一个实例:

    Field.bind

    如此,则对应看下Field基类的 __init__ 方法。忽略其他非目标代码,只关注问题的代码则是如下一行:self.name = _prefix + _name

3. 对类属性进行覆盖

​ 第二步中,对于类定义的字段列表属性,在BaseForm已经将其初始化并绑定了当前的 form 实例,则 Form 实例的 __init__ 方法往下走,可以看到,对类属性进行了覆盖为当前实例属性,如此,我们在代码中多次初始化,访问的不是类属性,而是实例属性。

实例属性绑定

4. 处理表单数据

Form__init__ 方法,最后一步,通过调用 self.process 处理当前的表单数据,可以看到,主要为将当前表单处理下发给 Fieldprocess 函数进行处理获得自身数据

处理表单数据

如此,则跳转至 Field 中的 process 方法,忽略掉无关的代码,可以看到,是根据字段的 self.name 从表单中获取数据

字段获取表单数据

而对于 self.name 的变量定义,咱们在之前的 Field.__init__ 源码中可以看到是 _prefix + _name

其中,_name 来源于类定义的属性名,_prefix 类初始化时候的参数,默认为空字符串。

至此,整个 wtforms 框架对表单数据的解析提取基本流程走完。

5. FormField 字段的用法

有了上面的源码阅读后,我们可以直接看 FormField的代码实现,可以看到,在 process 方法中,对我们 FormField 类进行了实例化,传递了 prefix。关键代码如下

FormField处理数据

即对我们子表单类的复用是通过直接初始化这个子表单类实例作为表单的一个字段。

初始化时,指定了 self.name + self.separatorself.separator__init__中默认为 -,这个就不贴代码了。通过上面的源码我们可以知道,子表单字段的表单中的 key 对应应该为

form.name + formfield.separator + subform.name

问题答案

通过上面的源码分析,我们可以得出对于上面表单代码,对应的数据结构应该是如下:

示例数据2

吐槽

  1. 这反人类的数据解析提取方式,我都不好意思跟前端说这么上传数据
  2. 对应的解决方案有个 wtforms-json 库提供了图1数据格式的解析,主要是将 json 的数据转换为 wtforms 期望的扁平化数据格式。然而,在使用时,发现初始化后的表单有时候获取不到数据,故弃之。