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'>
source to share
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
source to share
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
source to share
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]
source to share
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 .
source to share
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]
source to share
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 thatchoices
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
source to share