将 WTForms 与枚举一起使用

Using WTForms with Enum

我有以下代码:

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>

不知何故,当我点击提交时,我总是收到“无效选择”。

知道为什么吗?

这是我的终端输出:

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

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

我认为您需要将传递给 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
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 <type 'NoneType'> 尽管

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

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

并获取值:

selected_value = MyEnum[field.data]

这比公认的解决方案更简洁,因为您不需要多次放置选项。

默认情况下 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]

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

isinstance(someobject, Company)

coerce 函数必须在强制转换时引发 ValueErrorTypeError

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

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

enum_instance = Company[enum_name]

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

接下来,我们可以将枚举对象转换为唯一字符串(针对 <option> 标签的 value="..." 属性)并将标签字符串(向用户显示)保留为标准挂钩方法在枚举 class 上,例如 __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 class 实现与演示分开; cource_for_enum() 函数负责将 KeyError 映射到 ValueError(v, escape(v)) 对提供每个选项的值和标签; str(v) 用于 <option value="..."> 属性值,然后通过 Company[__html__result] 使用相同的字符串强制返回枚举实例。 WTForms 3.0 将开始使用 MarkupSafe 作为标签,但在那之前,我们可以直接使用 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.

coerce参数指向的函数需要将浏览器传递过来的字符串(<select>ed <option>的值)转换成你的值类型在您的 choices 中指定:

Select fields keep a choices property which is a sequence of (value, label) pairs. The value portion can be any type in theory, but as form data is sent by the browser as strings, you will need to provide a function which can coerce the string representation back to a comparable object.

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

那样 coerced provided value can be compared with the configured ones.

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

如果您决定使用 整数 Enum::value 作为 <option> 的值,您必须强制 returned 字符串回到 ints 进行比较。

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

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

给你 return Company::__str__ 中的 Company::name 并使用 EnterMedia 作为默认值:

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

Hth, dtk

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

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)