【问题标题】:Using WTForms with Enum将 WTForms 与 Enum 一起使用
【发布时间】:2017-05-19 20:54:55
【问题描述】:

我有以下代码:

class Company(enum.Enum):
    EnterMedia = 'EnterMedia'
    WhalesMedia = 'WhalesMedia'

    @classmethod
    def choices(cls):
        return [(choice.name, choice.name) for choice in cls]

    @classmethod
    def coerce(cls, item):
        print "Coerce", item, type(item)
        if item == 'WhalesMedia':
            return Company.WhalesMedia
        elif item == 'EnterMedia':
            return Company.EnterMedia
        else:
            raise ValueError

这是我的 wtform 字段:

company = SelectField("Company", choices=Company.choices(), coerce=Company.coerce)

这是在我的表单中生成的 html:

<select class="" id="company" name="company" with_label="">
    <option value="EnterMedia">EnterMedia</option>
    <option value="WhalesMedia">WhalesMedia</option>
</select>

不知何故,当我点击提交时,我不断收到“Not a Valid Choice”。

有什么想法吗?

这是我的终端输出:

当我查看终端时,我看到以下内容:

Coerce None <type 'NoneType'>
Coerce EnterMedia <type 'unicode'>
Coerce EnterMedia <type 'str'>
Coerce WhalesMedia <type 'str'>

【问题讨论】:

  • 您可以简单地通过索引枚举将字符串名称映射到对象(强制):Company["EnterMedia"] 产生Company.EnterMedia。您可以使用coerce=Company.__getitem__。对于,调用也有效,所以coerce=Company。请参阅Programmatic access。您的名称和值匹配,因此您可以在此处使用。

标签: python flask enums wtforms


【解决方案1】:

WTForm 将传入字符串None已强制数据coerce;这有点烦人,但可以通过测试要强制的数据是否已经是一个实例来轻松处理:

isinstance(someobject, Company)

coerce 函数必须在强制执行时引发 ValueErrorTypeError

您想使用 枚举名称 作为选择框中的值;这些总是字符串。如果您的枚举 values 适合作为标签,那就太好了,您可以将它们用于选项可读文本,但不要将它们与选项值混淆,选项值必须是唯一的,枚举值不需要。

Enum 类允许您使用订阅将包含枚举名称的字符串映射到 Enum 实例:

enum_instance = Company[enum_name]

请参阅 enum 模块文档中的 Programmatic access to enumeration members and their attributes

接下来,我们可以将枚举对象转换为唯一字符串(用于&lt;option&gt; 标签的value="..." 属性)并将字符串标签(以显示给用户)留给枚举类上的标准钩子方法,例如如__str____html__

对于您的特定设置,请一起使用:

from markupsafe import escape

class Company(enum.Enum):
    EnterMedia = 'Enter Media'
    WhalesMedia = 'Whales Media'

    def __str__(self):
        return self.name  # value string

    def __html__(self):
        return self.value  # label string

def coerce_for_enum(enum):
    def coerce(name):
        if isinstance(name, enum):
            return name
        try:
            return enum[name]
        except KeyError:
            raise ValueError(name)
    return coerce

company = SelectField(
    "Company",
    # (unique value, human-readable label)
    # the escape() call can be dropped when using wtforms 3.0 or newer
    choices=[(v, escape(v)) for v in Company],
    coerce=coerce_for_enum(Company)
)

以上内容使 Enum 类的实现与表示分离; cource_for_enum() 函数负责将KeyErrors 映射到ValueErrors。 (v, escape(v)) 对提供每个选项的值和标签; str(v) 用于&lt;option value="..."&gt; 属性值,然后通过Company[__html__result] 使用相同的字符串强制返回枚举实例。 WTForms 3.0 将开始使用MarkupSafe 作为标签,但在那之前,我们可以直接提供与escape(v) 相同的功能,而escape(v) 反过来使用__html__ 来提供合适的渲染。

如果必须记住要在列表解析中放入什么以及使用coerce_for_enum() 变得乏味,您可以使用辅助函数生成choicescoerce 选项;您甚至可以让它验证是否有合适的 __str____html__ 方法可用:

def enum_field_options(enum):
    """Produce WTForm Field instance configuration options for an Enum

    Returns a dictionary with 'choices' and 'coerce' keys, use this as
    **enum_fields_options(EnumClass) when constructing a field:

    enum_selection = SelectField("Enum Selection", **enum_field_options(EnumClass))

    Labels are produced from str(enum_instance.value) or 
    str(eum_instance), value strings with str(enum_instance).

    """
    assert not {'__str__', '__html__'}.isdisjoint(vars(enum)), (
        "The {!r} enum class does not implement __str__ and __html__ methods")

    def coerce(name):
        if isinstance(name, enum):
            # already coerced to instance of this enum
            return name
        try:
            return enum[name]
        except KeyError:
            raise ValueError(name)

    return {'choices': [(v, escape(v)) for v in enum], 'coerce': coerce}

对于你的例子,然后使用

company = SelectField("Company", **enum_field_options(Company))

请注意,一旦 WTForm 3.0 发布,您可以在枚举对象上使用__html__ 方法,而不必使用markdownsafe.escape(),因为项目是switching to using MarkupSafe for the label values

【讨论】:

    【解决方案2】:

    我认为您需要将传递给coerce 方法的参数转换为枚举的实例。

    import enum
    
    class Company(enum.Enum):
        EnterMedia = 'EnterMedia'
        WhalesMedia = 'WhalesMedia'
    
        @classmethod
        def choices(cls):
            return [(choice.name, choice.value) for choice in cls]
    
        @classmethod
        def coerce(cls, item):
            item = cls(item) \
                   if not isinstance(item, cls) \
                   else item  # a ValueError thrown if item is not defined in cls.
            return item.value
            # if item.value == 'WhalesMedia':
            #     return Company.WhalesMedia.value
            # elif item.value == 'EnterMedia':
            #     return Company.EnterMedia.value
            # else:
            #     raise ValueError
    

    【讨论】:

    • 我试了一下,它不起作用,得到一个“无效的选择”。这是正在呈现的内容:&lt;select id="company" name="company"&gt;&lt;option selected value="EnterMedia"&gt;EnterMedia&lt;/option&gt;&lt;option value="WhalesMedia"&gt;WhalesMedia&lt;/option&gt;&lt;/select&gt;。这就是 form.company.data = Company.WhalesMedia (type:&lt;enum 'Company'&gt;)
    • 我相信错误是由 Enum.coerce 将 form.data 映射到枚举引起的,但 WTForm 的 SelectField 正在寻找一个字符串,因为这是选择中给出的内容
    • @Sparrowcide 我更新了答案以返回枚举字段中的值。
    • 将字符串名称映射到枚举对象是通过使用[...]: Company['EnterMedia'] 完成的。见Programmatic access。对于,调用也可以工作。
    【解决方案3】:

    我刚刚落入了同一个兔子洞。不知道为什么,但是在初始化表单时,coerce 会被 None 调用。在浪费了很多时间之后,我决定不值得强迫,而是我只是使用:

    field = SelectField("Label", choices=[(choice.name, choice.value) for choice in MyEnum])
    

    并获得价值:

    selected_value = MyEnum[field.data]
    

    【讨论】:

    • 也可以工作和验证。
    【解决方案4】:

    这比公认的解决方案干净得多,因为您不需要多次放置选项。

    默认情况下,Python 将使用对象的路径将对象转换为字符串,这就是您最终使用 Company.EnterMedia 等的原因。在下面的解决方案中,我使用__str__ 告诉 python 应该使用名称,然后使用 [] 表示法通过名称查找枚举对象。

    class Company(enum.Enum):
        EnterMedia = 'EnterMedia'
        WhalesMedia = 'WhalesMedia'
    
        def __str__(self):
            return self.name
    
        @classmethod
        def choices(cls):
            return [(choice, choice.value) for choice in cls]
    
        @classmethod
        def coerce(cls, item):
            return item if isinstance(item, Company) else Company[item]
    

    【讨论】:

    • 永远不要使用type(...) == ...。您想要isinstance(item, Company),或者如果不允许使用子类,请使用type(item) is Company(注意身份测试,而不是平等)。具体的 Enum 类是不可子分类的,所以 isinstance() 非常好。
    • 而且 coerce() 永远不会被枚举实例调用,因为 WTF 专门处理字符串!
    【解决方案5】:

    coerce参数指向的函数需要将浏览器下发的字符串(&lt;select&gt;ed&lt;option&gt;的值)转换成你在choices中指定的值的类型:

    选择字段保留一个choices 属性,它是(valuelabel)对的序列。理论上值部分可以是任何类型,但由于表单数据是由浏览器作为字符串发送的,因此您需要提供一个函数,该函数可以将字符串表示形式强制转换为可比较的对象。

    https://wtforms.readthedocs.io/en/2.2.1/fields.html#wtforms.fields.SelectField

    这样coerced provided value 可以是compared with the configured ones

    由于您已经使用枚举项的名称 strings 作为值 (choices=[(choice.name, choice.name) for choice in Company]),因此无需强制执行。

    如果您决定使用 整数 Enum::values 作为 &lt;option&gt;s 的值,则必须将返回的字符串强制转换回 ints 以进行比较。

    choices=[(choice.value, choice.name) for choice in Company],
    coerce=int
    

    如果您想从表单中取出枚举项,则必须在 choices ([(choice, choice.name) for choice in Company]) 中配置这些项,并将它们的字符串序列化(例如 Company.EnterMedia)强制返回到 Enum 实例中,处理其他答案中提到的问题,例如 None 和强制枚举实例被传递到您的函数中:

    假设您在Company::__str__ 中返回Company::name 并默认使用EnterMedia

    coerce=lambda value: value if isinstance(value, Company) else Company[value or Company.EnterMedia.name]
    

    hth, dtk

    【讨论】:

      【解决方案6】:

      这里有一个不同的方法,它只是创建一个新的 WTF EnumField 并对枚举类型进行一些类操作以使其与这些函数无缝使用:

      import enum
      
      @enum.unique
      class MyEnum(enum.Enum):
          foo = 0
          bar = 10
      

      然后在某处创建 EnumField 定义,它只是扩展 SelectField 以使用 Enum 类型:

      import enum
      from markupsafe import escape
      from wtforms import SelectField
      
      from typing import Union, Callable
      
      
      class EnumField(SelectField):
          def coerce(enum_type: enum.Enum) -> Callable[[Union[enum.Enum, str]], enum.Enum]:
              def coerce(name: Union[enum.Enum, str]) -> enum.Enum:
                  if isinstance(name, enum_type):
                      return name
                  try:
                      return enum_type[name]
                  except KeyError:
                      raise ValueError(name)
              return coerce
      
          def __init__(self, enum_type: enum.Enum, *args, **kwargs):
              def attach_functions(enum_type: enum.Enum) -> enum.Enum:
                  enum_type.__str__ = lambda self: self.name
                  enum_type.__html__ = lambda self: self.name
                  return enum_type
      
              _enum_type = attach_functions(enum_type)
              super().__init__(_enum_type.__name__,
                  choices=[(v, escape(v)) for v in _enum_type],
                  coerce=EnumField.coerce(_enum_type), *args, **kwargs)
      

      现在在您的代码中,您可以天真地使用东西:

      class MyForm(FlaskForm):
          field__myenum = EnumField(MyEnum)
          submit = SubmitField('Submit')
      
      @app.route("/action", methods=['GET', 'POST'])
      def action():
          form = MyForm()
          if form.validate_on_submit():
              print('Enum value is: ', form.field__myenum)  #<MyEnum.foo: 0>
              return redirect(url_for('.action'))
          elif request.method == 'GET':  # display the information on record
              form.field__myenum.data = MyEnum.foo
              form.field__myenum.default = MyEnum.foo
          return render_template('action.html', form=form)
      

      【讨论】:

        【解决方案7】:
        class Company(enum.Enum):
          WhalesMedia = 'WhalesMedia'
          EnterMedia = 'EnterMedia'
        
          @classmethod
          def choices(cls):
            return [(choice, choice.value) for choice in cls]
        
          @classmethod
          def coerce(cls, item):
            """item will be both type(enum) AND type(unicode).
            """
            if item == 'Company.EnterMedia' or item == Company.EnterMedia:
              return Company.EnterMedia
            elif item == 'Company.WhalesMedia' or item == Company.WhalesMedia:
              return Company.WhalesMedia
            else:
              print "Can't coerce", item, type(item)
        

        所以我四处乱窜,这行得通。

        在我看来,对于 (x,y) 的选择,强制都将应用于 (x,y)。

        我似乎无法理解为什么我一直看到:Can't coerce None &lt;type 'NoneType'&gt; 虽然

        【讨论】:

        • 看我的解决方案,它相当干净
        猜你喜欢
        • 1970-01-01
        • 2019-01-01
        • 2012-12-24
        • 2020-04-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多