问题描述
在使用 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中定义的nameprefix等属性,相关代码在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期望的扁平化数据格式。然而,在使用时,发现初始化后的表单有时候获取不到数据,故弃之。
个人博客 原文地址