wtforms 源码阅读
问题描述
在使用 wtforms
时遇到个问题,对于嵌套接口数据,wtforms
表示用FormField
对已有的表单进行复用。
期望的数据模型:
期望的wtforms
代码:
然而,wtforms
并不按期望中的代码获取表单值。官方文档也没写清如何获取,遂,翻下源码看下如何解析的。
注:与本次问题无关的代码如
csrf
,html render
等功能暂不关心,因为接口走RESTful
风格,不通过wtforms
生成表单。
解决步骤
概要
先概要讲下 wtforms
的表单处理过程:
- 通过自定义的
FormMeta
类,控制Form
类的生成,生成类定义下的字段声明字典_unbound_fields
- 通过
BaseForm
对_unbound_fields
进行 表单字段 和form
实例的绑定,绑定包括- 表单字段在form中声明的name
- 属性名的前缀
- 字段的翻译文本等
- field 通过
process
方法获取在form
中的 value,key
为prefix+name
- 字段通过
validate
方法验证自身约束检查,其中,先验证自身的validate
方法,再验证属性定义的validators
验证器列表
源码分析
1. 获取 form 定义的字段列表
对于用户定义的 Form
类,通过python
的元类特性,内省解析类定义的字段列表,此时该字段类尚未初始化,只是确定类的字段子弹 name->field_class
,对应的代码在 wtforms/form.py 第 185
行,获取类未绑定的字段列表
2. 对字段进行绑定初始化
流程走到的
Form
的实例初始化工作,先是通过BaseForm
的__init__
对上面元类获取到的类字段列表进行初始化,即将Field
类初始化,绑定该Field
在Form
中定义的name
prefix
等属性,相关代码在wtforms/form.py 第50
行对于上面的关键代码
field = meta.bind_field(self, unbound_field, options)
,对应UnboundField
类的bind_field
函数,跳转过去,可以看到,是返回该字段的一个实例:如此,则对应看下
Field
基类的__init__
方法。忽略其他非目标代码,只关注问题的代码则是如下一行:self.name = _prefix + _name
3. 对类属性进行覆盖
第二步中,对于类定义的字段列表属性,在BaseForm
已经将其初始化并绑定了当前的 form
实例,则 Form
实例的 __init__
方法往下走,可以看到,对类属性进行了覆盖为当前实例属性,如此,我们在代码中多次初始化,访问的不是类属性,而是实例属性。
4. 处理表单数据
在 Form
的 __init__
方法,最后一步,通过调用 self.process
处理当前的表单数据,可以看到,主要为将当前表单处理下发给 Field
的 process
函数进行处理获得自身数据
如此,则跳转至 Field
中的 process
方法,忽略掉无关的代码,可以看到,是根据字段的 self.name
从表单中获取数据
而对于 self.name
的变量定义,咱们在之前的 Field.__init__
源码中可以看到是 _prefix + _name
其中,_name
来源于类定义的属性名,_prefix
类初始化时候的参数,默认为空字符串。
至此,整个 wtforms
框架对表单数据的解析提取基本流程走完。
5. FormField 字段的用法
有了上面的源码阅读后,我们可以直接看 FormField
的代码实现,可以看到,在 process
方法中,对我们 FormField
类进行了实例化,传递了 prefix
。关键代码如下
即对我们子表单类的复用是通过直接初始化这个子表单类实例作为表单的一个字段。
初始化时,指定了 self.name + self.separator
,self.separator
在 __init__
中默认为 -
,这个就不贴代码了。通过上面的源码我们可以知道,子表单字段的表单中的 key 对应应该为
form.name + formfield.separator + subform.name
问题答案
通过上面的源码分析,我们可以得出对于上面表单代码,对应的数据结构应该是如下:
吐槽
- 这反人类的数据解析提取方式,我都不好意思跟前端说这么上传数据
- 对应的解决方案有个 wtforms-json 库提供了图1数据格式的解析,主要是将
json
的数据转换为wtforms
期望的扁平化数据格式。然而,在使用时,发现初始化后的表单有时候获取不到数据,故弃之。