wtforms快速使用和源码分析(基于flask)

chuck-y chuck-y     2022-10-09     227

关键词:

wtforms

和django的form组件大同小异,下面给出一个应用举例以便快速查询。

开始使用

技术分享图片
  1 from flask import Flask, render_template, request, redirect
  2 
  3 from wtforms import Form
  4 
  5 from wtforms.fields import core
  6 from wtforms.fields import html5
  7 from wtforms.fields import simple
  8 
  9 from wtforms import validators
 10 from wtforms import widgets
 11 
 12 app = Flask(__name__, template_folder=templates)
 13 app.debug = True
 14 
 15 class MyValidator(object):
 16     def __init__(self,message):
 17         self.message = message
 18     def __call__(self, form, field):
 19         print(field.data)
 20         if field.data == 王浩:
 21             return None
 22         raise validators.StopValidation(self.message)
 23 
 24 
 25 class LoginForm(Form):
 26     name = simple.StringField(
 27         label=用户名,
 28         validators=[
 29             # MyValidator(message=‘用户名必须等于王浩‘)
 30             validators.DataRequired(message=用户名不能为空.),
 31             validators.Length(min=6, max=18, message=用户名长度必须大于%(min)d且小于%(max)d)
 32         ],
 33         widget=widgets.TextInput(),
 34         render_kw={class: form-control}
 35     )
 36     pwd = simple.PasswordField(
 37         label=密码,
 38         validators=[
 39             validators.DataRequired(message=密码不能为空.),
 40             validators.Length(min=8, message=用户名长度必须大于%(min)d),
 41             validators.Regexp(regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[[email protected]$!%*?&])[A-Za-z[email protected]$!%*?&]{8,}",
 42                               message=密码至少8个字符,至少1个大写字母,1个小写字母,1个数字和1个特殊字符)
 43         ],
 44         widget=widgets.PasswordInput(),
 45         render_kw={class: form-control}
 46     )
 47 
 48 
 49 @app.route(/login, methods=[GET, POST])
 50 def login():
 51     if request.method == GET:
 52         form = LoginForm()
 53         return render_template(login.html, form=form)
 54     else:
 55         form = LoginForm(formdata=request.form)
 56         if form.validate():
 57             print(用户提交数据通过格式验证,提交的值为:, form.data)
 58         else:
 59             print(form.errors)
 60         return render_template(login.html, form=form)
 61 
 62 
 63 # ########################### 用户注册 ##########################
 64 class RegisterForm(Form):
 65     name = simple.StringField(
 66         label=用户名,
 67         validators=[
 68             validators.DataRequired()
 69         ],
 70         widget=widgets.TextInput(),
 71         render_kw={class: form-control},
 72         default=alex
 73     )
 74 
 75     pwd = simple.PasswordField(
 76         label=密码,
 77         validators=[
 78             validators.DataRequired(message=密码不能为空.)
 79         ],
 80         widget=widgets.PasswordInput(),
 81         render_kw={class: form-control}
 82     )
 83 
 84     pwd_confirm = simple.PasswordField(
 85         label=重复密码,
 86         validators=[
 87             validators.DataRequired(message=重复密码不能为空.),
 88             validators.EqualTo(pwd, message="两次密码输入不一致")
 89         ],
 90         widget=widgets.PasswordInput(),
 91         render_kw={class: form-control}
 92     )
 93 
 94     email = html5.EmailField(
 95         label=邮箱,
 96         validators=[
 97             validators.DataRequired(message=邮箱不能为空.),
 98             validators.Email(message=邮箱格式错误)
 99         ],
100         widget=widgets.TextInput(input_type=email),
101         render_kw={class: form-control}
102     )
103 
104     gender = core.RadioField(
105         label=性别,
106         choices=(
107             (1, ),
108             (2, ),
109         ),
110         coerce=int
111     )
112     city = core.SelectField(
113         label=城市,
114         choices=(
115             (bj, 北京),
116             (sh, 上海),
117         )
118     )
119 
120     hobby = core.SelectMultipleField(
121         label=爱好,
122         choices=(
123             (1, 篮球),
124             (2, 足球),
125         ),
126         coerce=int
127     )
128 
129     favor = core.SelectMultipleField(
130         label=喜好,
131         choices=(
132             (1, 篮球),
133             (2, 足球),
134         ),
135         widget=widgets.ListWidget(prefix_label=False),
136         option_widget=widgets.CheckboxInput(),
137         coerce=int,
138         default=[1, 2]
139     )
140 
141     def __init__(self, *args, **kwargs):
142         super(RegisterForm, self).__init__(*args, **kwargs)
143         self.favor.choices = ((1, 篮球), (2, 足球), (3, 羽毛球))
144 
145     def validate_pwd_confirm(self, field):
146         """
147         自定义pwd_confirm字段规则,例:与pwd字段是否一致
148         :param field: 
149         :return: 
150         """
151         # 最开始初始化时,self.data中已经有所有的值
152 
153         if field.data != self.data[pwd]:
154             # raise validators.ValidationError("密码不一致") # 继续后续验证
155             raise validators.StopValidation("密码不一致")  # 不再继续后续验证
156 
157 
158 @app.route(/register, methods=[GET, POST])
159 def register():
160     if request.method == GET:
161         # 设置默认值
162         form = RegisterForm(data={gender: 1})
163         return render_template(register.html, form=form)
164     else:
165         form = RegisterForm(formdata=request.form)
166         if form.validate():
167             print(用户提交数据通过格式验证,提交的值为:, form.data)
168         else:
169             print(form.errors)
170         return render_template(register.html, form=form)
171 
172 
173 if __name__ == __main__:
174     app.run()
View Code

源码分析

  • 高级用法
    • metaclass的另类使用
  • 切入点:
    • 当定义好一个自定义的Form类,项目加载Form类所在模块,代码都做了什么?
    • 在视图函数中实例化Form类,代码都做了什么?
      • 模板渲染调用Form的字段时,代码做了什么?
    • 前端填好数据,返回后端校验时,代码做了什么?  

详细分析

  1. 项目加载Form类所在模块
    • Form
      • 这是声明Form的代码:class Form(with_metaclass(FormMeta, BaseForm))

      • 可见这里调用一个函数with_metaclass

        技术分享图片
        1 def with_metaclass(meta, base=object):
        2      return meta("NewBase", (base,), {})
        View Code
      • 该函数返回了一个FormMeta元类创建的NewBase类作为Form类的基类

      • 元类创建类会执行元类的__init__方法

        技术分享图片
        1  def __init__(cls, name, bases, attrs):
        2      type.__init__(cls, name, bases, attrs)
        3      cls._unbound_fields = None
        4      cls. = None
        View Code
      • 此处为Form类定义了_unbound_fields_wtforms_meta两个静态字段

    • 类的字段
      • 拿一个字段举例:username = simple.StringField()
      • 可见是实例化了一个StringField
        • StringField类定义了一个静态字段:widget = widgets.TextInput()
          • 这里是实例化了一个插件类TextInput
            • TextInput类定义了一个静态字段:input_type = ‘text‘指定生成html标签时的type
            • 基类中的__init__方法只是将检测了一下input_type
        • StringField类没有__init__方法,__new__方法,显然要从基类中寻找
          • 基类中的__new__方法返回的是:UnboundField(cls, *args, **kwargs)实例化了一个UnboundField类的对象

            技术分享图片
            1  creation_counter = 0
            2  def __init__(self, field_class, *args, **kwargs):
            3      UnboundField.creation_counter += 1
            4      self.field_class = field_class
            5      self.args = args
            6      self.kwargs = kwargs
            7      self.creation_counter = UnboundField.creation_counter
            View Code
            • 可见UnboundField类封装了字段的类StringField,并给了字段一个编号,看来wtforms应该就是通过这个编号来识别字段的顺序
          • 基类中的__init__方法

            • 有意思的是此时__init__的接收的self已经不是Field类的实例,而是UnboundField类的实例
            • init这里大部分操作都是常规的赋值操作,不过也有个值得关注的地方
              • if _translations is not None: self._translations = _translations
                • 通过这两行代码可以看出,wtforms内部还实现了实现了多语言的提示信息
      • 最后得出结论,form类的静态字段如username此时存储的是UnboundField类的实例
  2. 在视图函数中实例化form类
    • 首先执行元类的__call__方法

      技术分享图片
       1 def __call__(cls, *args, **kwargs):
       2      """
       3      Construct a new `Form` instance.
       4 
       5      Creates the `_unbound_fields` list and the internal `_wtforms_meta`
       6      subclass of the class Meta in order to allow a proper inheritance
       7      hierarchy.
       8      """
       9      if cls._unbound_fields is None:
      10          fields = []
      11          for name in dir(cls):
      12              if not name.startswith(_):
      13                  unbound_field = getattr(cls, name)
      14                  if hasattr(unbound_field, _formfield):
      15                      fields.append((name, unbound_field))
      16          # We keep the name as the second element of the sort
      17          # to ensure a stable sort.
      18          fields.sort(key=lambda x: (x[1].creation_counter, x[0]))
      19          cls._unbound_fields = fields
      20  
      21      # Create a subclass of the ‘class Meta‘ using all the ancestors.
      22      if cls._wtforms_meta is None:
      23          bases = []
      24          for mro_class in cls.__mro__:
      25              if Meta in mro_class.__dict__:
      26                  bases.append(mro_class.Meta)
      27          cls._wtforms_meta = type(Meta, tuple(bases), {})
      28      return type.__call__(cls, *args, **kwargs)
      View Code
      • 这里就是给form类的_unbound_fields和_wtforms_meta赋值
        • 使用dir(cls)获取form类的所有变量字符串,当其不为‘_‘开头时说明是自定义form的字段,获取字段的对应的对象,根据字段的编号排序后加入_unbound_fields列表
          • 此处的判断做的还不够好,或许通过cls.__dict__.items()获取到所有的变量名和值,判断值是否为UnboundField的实例,若为UnboundField的实例则加入列表
        • 使用__mro__获取form类所有的继承关系,挨个寻找这些类中的Meta字段对应的类计入bases列表,最后通过type一次型创建一个继承了所有bases列表中的类的Meta类,并存入_wtforms_meta字段
    • 接着应该执行类的__new__方法,不过这里没有定义,忽略此步骤

    • 然后执行类的__init__方法

      技术分享图片
      1  def __init__(self, formdata=None, obj=None, prefix=‘‘, data=None, meta=None, **kwargs):
      2      meta_obj = self._wtforms_meta()
      3      if meta is not None and isinstance(meta, dict):
      4          meta_obj.update_values(meta)
      5      super(Form, self).__init__(self._unbound_fields, meta=meta_obj, prefix=prefix)
      6  
      7      for name, field in iteritems(self._fields):
      8          setattr(self, name, field)
      9      self.process(formdata, obj, data=data, **kwargs)
      View Code
      • 这里首先实例化了_wtforms_meta字段对应的Meta类然后传入了基类的__init__方法

        技术分享图片
         1 def __init__(self, fields, prefix=‘‘, meta=DefaultMeta()):
         2      if prefix and prefix[-1] not in -_;:/.:
         3          prefix += -
         4  
         5      self.meta = meta
         6      self._prefix = prefix
         7      self._errors = None
         8      self._fields = OrderedDict()
         9  
        10      if hasattr(fields, items):
        11          fields = fields.items()
        12  
        13      translations = self._get_translations()
        14      extra_fields = []
        15      if meta.csrf:
        16          self._csrf = meta.build_csrf(self)
        17          extra_fields.extend(self._csrf.setup_form(self))
        18  
        19      for name, unbound_field in itertools.chain(fields, extra_fields):
        20          options = dict(name=name, prefix=prefix, translations=translations)
        21          field = meta.bind_field(self, unbound_field, options)
        22          self._fields[name] = field
        View Code
        • prefix是设置生成html标签的name属性的值的前缀
        • meta是一个继承了form类以及所有form类继承的类中的Meta类的实例
          • 这里检测了是否启用了csrf字段
          • 并将meta中额外定义的有关csrf的字段加入extra_fields
        • 最后将fields和extra_fields中所有的字段全部放置在form类实例的_fields字段中
          • 这里通过field = meta.bind_field(self, unbound_field, options)对所有的UnboundField类实例做了处理,找回了原有的Field
      • 然后将类中的_fields表中的字段设置为form类实例的属性

        • 注意:此时的字段已经变回了原来的Field,尚不明确为何要多进行这样的操作
      • 这行代码主要是针对有数据传入时的操作self.process(formdata, obj, data=data, **kwargs)

        • 在第4点详细查看
  3. 模板渲染调用form的字段
    • 此时本质上就是调用了字段的__str__方法,把返回的字符串放置在模板

      技术分享图片
      1 def __str__(self):
      2      """
      3      Returns a HTML representation of the field. For more powerful rendering,
      4      see the `__call__` method.
      5      """
      6      return self()
      View Code
    • 转为调用字段的__call__方法

      技术分享图片
       1 def __call__(self, **kwargs):
       2      """
       3      Render this field as HTML, using keyword args as additional attributes.
       4 
       5      This delegates rendering to
       6      :meth:`meta.render_field <wtforms.meta.DefaultMeta.render_field>`
       7      whose default behavior is to call the field‘s widget, passing any
       8      keyword arguments from this call along to the widget.
       9 
      10      In all of the WTForms HTML widgets, keyword arguments are turned to
      11      HTML attributes, though in theory a widget is free to do anything it
      12      wants with the supplied keyword arguments, and widgets don‘t have to
      13      even do anything related to HTML.
      14      """
      15      return self.meta.render_field(self, kwargs)
      View Code
    • 继续调用Meta类的render_field方法,这个方法在DefaultMeta

      技术分享图片
       1 def render_field(self, field, render_kw):
       2      """
       3      render_field allows customization of how widget rendering is done.
       4 
       5      The default implementation calls ``field.widget(field, **render_kw)``
       6      """
       7      other_kw = getattr(field, render_kw, None)
       8      if other_kw is not None:
       9          render_kw = dict(other_kw, **render_kw)
      10      return field.widget(field, **render_kw)
      View Code
    • 这里调用了字段的插件对象的__call__方法

      技术分享图片
      1 def __call__(self, field, **kwargs):
      2      kwargs.setdefault(id, field.id)
      3      kwargs.setdefault(type, self.input_type)
      4      if value not in kwargs:
      5          kwargs[value] = field._value()
      6      return HTMLString(<input %s> % self.html_params(name=field.name, **kwargs))
      View Code
    • 至此,完成了Form类实例的__str__方法,返回了一个HTML的input标签的字符串

  4. 前端填好数据,返回后端校验
    • 依然是实例化一个Form类的对象,大部分流程和第2点讨论的一致,不过在执行到Form类的__init__方法的最后一行时开始不同

      • self.process(formdata, obj, data=data, **kwargs)

        技术分享图片
         1 def process(self, formdata=None, obj=None, data=None, **kwargs):
         2      formdata = self.meta.wrap_formdata(self, formdata)
         3  
         4      if data is not None:
         5          kwargs = dict(data, **kwargs)
         6  
         7      for name, field, in iteritems(self._fields):
         8          if obj is not None and hasattr(obj, name):
         9              field.process(formdata, getattr(obj, name))
        10          elif name in kwargs:
        11              field.process(formdata, kwargs[name])
        12          else:
        13              field.process(formdata)
        View Code
      • 这里根据传入的数据不同做不同的操作

        • formdata = self.meta.wrap_formdata(self, formdata)是将不具有getlist方法的formdata的对象封装一个getlist对象
        • field.process函数就是将数据封装进self.data和self.row_data
          技术分享图片
           1 def process(self, formdata, data=unset_value):
           2      self.process_errors = []
           3      if data is unset_value:
           4          try:
           5              data = self.default()
           6          except TypeError:
           7              data = self.default
           8  
           9      self.object_data = data
          10  
          11      try:
          12          self.process_data(data)
          13      except ValueError as e:
          14          self.process_errors.append(e.args[0])
          15  
          16      if formdata:
          17          try:
          18              if self.name in formdata:
          19                  self.raw_data = formdata.getlist(self.name)
          20              else:
          21                  self.raw_data = []
          22              self.process_formdata(self.raw_data)
          23          except ValueError as e:
          24              self.process_errors.append(e.args[0])
          25  
          26      try:
          27          for filter in self.filters:
          28              self.data = filter(self.data)
          29      except ValueError as e:
          30              self.process_errors.append(e.args[0])
          View Code
    • 然后调用form.validate方法

      技术分享图片
      1 def validate(self):
      2      extra = {}
      3      for name in self._fields:
      4          inline = getattr(self.__class__, validate_%s % name, None)
      5          if inline is not None:
      6              extra[name] = [inline]
      7      return super(Form, self).validate(extra)
      View Code
      • 把每个字段的校验规则封装进extra
    • 然后调用BaseForm的validate()

      技术分享图片
       1 def validate(self, extra_validators=None):
       2      self._errors = None
       3      success = True
       4      for name, field in iteritems(self._fields):
       5          if extra_validators is not None and name in extra_validators:
       6              extra = extra_validators[name]
       7          else:
       8              extra = tuple()
       9          if not field.validate(self, extra):
      10              success = False
      11      return success
      View Code
    • 然后挨个调用字段的校验方法完成校验

 

示例:自定义简单的form组件

flask-wtforms

what‘sthe WTForms  WTForms是一个支持多个web框架的form组件,主要用于对用户请求数据进行验证。其作用是可以为轻量级的框架提供类似Django的form的功能。安装:pip3installwtforms 源码流程分析实例化流程分析#源码流程1.执行typ... 查看详情

使用flask和WTForms在一个页面中的多个表单

】使用flask和WTForms在一个页面中的多个表单【英文标题】:MultipleformsinasinglepageusingflaskandWTForms【发布时间】:2013-08-1920:28:52【问题描述】:我在同一页面上有多个表单将发布请求发送到同一处理程序在烧瓶中。我正在使用wtforms... 查看详情

自动完成 Flask wtforms

】自动完成Flaskwtforms【英文标题】:AutocompleteFlaskwtforms【发布时间】:2021-03-0413:59:44【问题描述】:我想使用数量和商品价格自动更新我的总金额字段?有没有办法使用没有javascript的烧瓶来做到这一点?我希望在输入数量和商... 查看详情

flask第七篇flask中的wtforms使用

一、简单介绍flask中的wtformsWTForms是一个支持多个web框架的form组件,主要用于对用户请求数据进行验证。安装:pip3installwtforms二、简单使用wtforms组件1、用户登录具体代码:fromflaskimportFlask,render_template,request,redirectfromwtforms.fieldsimp... 查看详情

85flask之wtforms

本篇导航:wtforms组件的使用自定义From组件 一、wtforms组件的使用1、flask中的wtformsWTForms是一个支持多个web框架的form组件,主要用于对用户请求数据进行验证。安装:pip3installwtforms2、wtforms组件的使用之登录验证1)图示2)manag... 查看详情

flask中的wtforms使用(代码片段)

一、简单介绍flask中的wtformsWTForms是一个支持多个web框架的form组件,主要用于对用户请求数据进行验证。安装:pip3installwtforms二、简单使用wtforms组件1、用户登录具体代码:fromflaskimportFlask,render_template,request,redirectfromwtforms.fieldsimp... 查看详情

当我将 POST 与 Flask 一起使用时,我遇到了 wtforms 选择字段的问题

】当我将POST与Flask一起使用时,我遇到了wtforms选择字段的问题【英文标题】:I\'mhavingproblemswithwtformsselectfieldswheniuseaPOSTwithFlask【发布时间】:2013-04-0222:31:44【问题描述】:我对wtforms和烧瓶还很陌生,并且在使用selectfields时遇到... 查看详情

使用 WTForms 字段列表

】使用WTForms字段列表【英文标题】:WorkingwithWTFormsFieldList【发布时间】:2011-08-2510:17:22【问题描述】:我通过Flask.WTF扩展将WTForms与Flask一起使用。不过,这个问题不是Flask特有的。WTForms包含一个FieldListfieldforlistsoffields。我想用... 查看详情

在网站的主要布局模板中使用 Flask-wtforms 的困难

】在网站的主要布局模板中使用Flask-wtforms的困难【英文标题】:DifficultieswithhavingaFlask-wtformsinthemainlayouttemplateofthewebsite【发布时间】:2021-11-1714:20:31【问题描述】:我过去一直在使用烧瓶、python和引导程序在网站上工作。我在我... 查看详情

Flask WTForms:DataRequired 和 InputRequired 之间的区别

】FlaskWTForms:DataRequired和InputRequired之间的区别【英文标题】:FlaskWTForms:DifferencebetweenDataRequiredandInputRequired【发布时间】:2014-07-2121:14:45【问题描述】:wtforms.valiadators中的DataRequired和InputRequired有什么区别我的注册表单中有一些... 查看详情

flask学习第7篇:flask中的wtforms使用(代码片段)

flask中的wtforms使用一、简单介绍flask中的wtformsWTForms是一个支持多个web框架的form组件,主要用于对用户请求数据进行验证。安装:pip3installwtforms二、简单使用wtforms组件1、用户登录具体代码:fromflaskimportFlask,render_template,request,redir... 查看详情

使用从前一个字段中选择的值填充 WTForms 选择字段

】使用从前一个字段中选择的值填充WTForms选择字段【英文标题】:populateWTFormsselectfieldusingvalueselectedfrompreviousfield【发布时间】:2017-05-0501:35:43【问题描述】:对此的新手,尝试按照众所周知的Flask教程构建应用程序,使用Flask-bo... 查看详情

flask信号和wtforms(代码片段)

一、信号1.1.所有内置信号request_started=_signals.signal(‘request-started‘)#请求到来前执行request_finished=_signals.signal(‘request-finished‘)#请求结束后执行before_render_template=_signals.signal(‘before-render-template‘)#模板渲 查看详情

使用 Flask-WTForms,如何设置 html 表单部分的样式?

】使用Flask-WTForms,如何设置html表单部分的样式?【英文标题】:UsingFlask-WTForms,howdoIstylemyformsectionofthehtml?【发布时间】:2016-04-1618:04:56【问题描述】:我阅读了Flask-WTF极其简化的wiki,但对我能用它做什么并不太了解。我的印象... 查看详情

为啥 Flask WTForms 和 WTForms-SQLAlchemy QuerySelectField 会产生太多无法解包的值?

】为啥FlaskWTForms和WTForms-SQLAlchemyQuerySelectField会产生太多无法解包的值?【英文标题】:WhydoesFlaskWTFormsandWTForms-SQLAlchemyQuerySelectFieldproducetoomanyvaluestounpack?为什么FlaskWTForms和WTForms-SQLAlchemyQuerySelectField会产生太多无法解包的值?【发... 查看详情

如何通过 AJAX 使用 Flask-WTForms CSRF 保护?

】如何通过AJAX使用Flask-WTFormsCSRF保护?【英文标题】:HowtouseFlask-WTFormsCSRFprotectionwithAJAX?【发布时间】:2015-10-3112:33:39【问题描述】:Flask-WTForms提供CSRF保护。它在使用普通HTML表单时效果很好,但在使用AJAX时过程不太清楚。我的... 查看详情

flask-wtforms(代码片段)

WTForms表单验证基本使用Flask-WTF是简化了WTForms操作的一个第三方库,WTForms表单的两个主要功能是验证用户提交数据的合法性以及渲染模板。当然还包括一些其他的功能:CSRF保护,文件上传等。安装Flask-WTF默认也会安装WTForms,因... 查看详情

Flask-WTForms 在我的项目目录中找不到 WTForms

】Flask-WTForms在我的项目目录中找不到WTForms【英文标题】:Flask-WTFormscan\'tfindWTFormsinmyprojectdirectory【发布时间】:2012-07-2708:38:54【问题描述】:这是我在***上的第一篇文章,大家好。我正在做博客应用程序来学习Python和Flask,我想... 查看详情