Using WTForms with Enum

I have the following code:

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

      

And this is my wtform field:

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

      

This is the html generated in my form:

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

      

Somehow, when I click the Submit button, I keep getting an Invalid Selection.

Any ideas why?

This is my terminal output:

When I look at the terminal I see the following:

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

      

+3


source to share


6 answers


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)

      

So I hacked it up and it works.



It looks to me like coercion would apply to both (x, y) for (x, y) in variants.

I cannot understand why I keep seeing: Can't coerce None <type 'NoneType'>

although

-3


source


I think you need to convert the argument passed to the method coerce

to an enumeration instance.



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

      

+2


source


This is much cleaner than the accepted solution as you don't need to specify parameters more than once.

By default, Python converts objects to strings using their path, so you end up with Company.EnterMedia and so on. In the solution below, I use __str__

to tell python to use a name instead, and then use the [] notation to find the enum object by name.

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]

      

+2


source


WTForm will pass either strings None

or data already given to coerce

; this is a bit annoying but easily handles by checking if the data to be enforced is an instance:

isinstance(someobject, Company)

      

Otherwise, the function coerce

must be called ValueError

or TypeError

coerced.

You want to use enumeration names as values ​​in a select box; it will always be strings. If your enum values ​​are suitable as labels, that's great, you can use them for the text read by the option, but don't confuse them with option values, which must be unique, the enumeration values ​​shouldn't be.

Classes Enum

allow you to map a string containing an enum name to an instance Enum

using a subscription:

enum_instance = Company[enum_name]

      

See Programmatically Accessing Enumeration Elements and Their Attributes in the module's documentation enum

.

We can then leave the conversion of enum objects to unique strings (for the value="..."

tags attribute <option>

) and label the strings (to show users) to standard hook methods in the enum class like __str__

and __html__

.

Together for your specific installation, use:

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)
)

      

The above keeps the implementation of the Enum class separate from the presentation; cource_for_enum()

takes care of matching KeyError

against ValueError

s. Pairs (v, escape(v))

provide a value and label for each parameter; str(v)

is used for an <option value="...">

attribute <option value="...">

, and that same string is then used through Company[__html__result]

to force fallback to enumeration instances. WTForms 3.0 will start using MarkupSafe

for labels, but until then we can directly provide the same functionality with escape(v)

, which in turn uses __html__

to provide suitable rendering.

If you find coerce_for_enum()

it tedious to remember what to add to the list and use it, you can also generate choices

parameters coerce

using a helper function; You can even check if there are any suitable __str__

and __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}

      

and for your example, then use

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

      

Note that after the release of WTForm 3.0, you can use the method __html__

on enum objects without having to use markdownsafe.escape()

it as the project switches to using MarkupSafe for the label values .

+2


source


I just went down that same rabbit hole. Not sure why, but coerce

gets called with None

when the form is initialized. After wasting a lot of time, I decided it wasn't worth coercing, and instead I just used:

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

      

and get the value:

selected_value = MyEnum[field.data]

      

+1


source


The function pointed to by the parameter coerce

should convert the string passed by the browser ( <select>

ed value <option>

) to the type of the values ​​you specified when choices

:

The fields choices

store a property that choices

is a sequence of pairs ( value

, label

). The value portion can be of any type in theory, but since form data is submitted by the browser as strings, you need to provide a function that can cast the string representation back to a comparable object.

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

Thus, given the value of coercion can be compared to the configured .

Since you are already using strings of your enum element names as values ​​( choices=[(choice.name, choice.name) for choice in Company]

), there is no coercion for you.

If you choose to use an integer Enum::value

as the values ​​for <option>

, you will have to force the returned strings back in int

for comparison.

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

      

If you want to extract enum elements from your form, you will have to customize them to your own choices

( [(choice, choice.name) for choice in Company]

) and force Company.EnterMedia

string serialization (for example Company.EnterMedia

) back to Instances Enum

, referring to the issues mentioned in other answers, such as the None

forced enum instances passed to your function:

Given what you return Company::name

in Company::__str__

and use EnterMedia

by default:

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

      

Hth, DTK

0


source







All Articles