Skip to content

Commit 549a590

Browse files
committed
refactor: introduce Choice and SelectChoice dataclasses
Choices can now be passed as Choice/SelectChoice instances, which provides stronger typing, more explicit arguments and simpler code. - passing choices as tuples is deprecated - passing optgroups as a dict is deprecated - <option> HTML attributes can be set via Choice.render_kw - empty <optgroup> elements are no longer rendered
1 parent 83a4412 commit 549a590

12 files changed

Lines changed: 355 additions & 201 deletions

File tree

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ Unreleased
77

88
- Fix :class:`~validators.Disabled` validation with provided formdata. :pr:`880`
99
- End support for Python 3.9, start support for Python 3.14. :pr:`883`
10+
- :class:`~fields.SelectField` refactor. Choices tuples and dicts are
11+
deprecated in favor of :class:`~fields.Choice`. :pr:`739`
12+
- ``<option>`` HTML attributes can be passed using
13+
:class:`~fields.Choice`. :issue:`692` :pr:`739`
1014

1115
Version 3.2.1
1216
-------------

docs/fields.rst

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,30 @@ refer to a single input from the form.
260260

261261
.. autoclass:: MonthField(default field arguments, format='%Y-%m')
262262

263-
.. autoclass:: RadioField(default field arguments, choices=[], coerce=str)
263+
.. autoclass:: SearchField(default field arguments)
264+
265+
.. autoclass:: SubmitField(default field arguments)
266+
267+
.. autoclass:: StringField(default field arguments)
268+
269+
.. code-block:: jinja
270+
271+
{{ form.username(size=30, maxlength=50) }}
272+
273+
.. autoclass:: TelField(default field arguments)
274+
275+
.. autoclass:: TimeField(default field arguments, format='%H:%M')
276+
277+
.. autoclass:: URLField(default field arguments)
278+
279+
Choice Fields
280+
-------------
281+
282+
.. autoclass:: Choice
283+
284+
.. autoclass:: SelectChoice
285+
286+
.. autoclass:: RadioField(default field arguments, choices=None, coerce=str)
264287

265288
.. code-block:: jinja
266289
@@ -274,47 +297,55 @@ refer to a single input from the form.
274297
Simply outputting the field without iterating its subfields will result in
275298
a ``<ul>`` list of radio choices.
276299

277-
.. class:: SelectField(default field arguments, choices=[], coerce=str, option_widget=None, validate_choice=True)
300+
301+
.. class:: SelectField(default field arguments, choices=None, coerce=str, option_widget=None, validate_choice=True)
278302

279303
Select fields take a ``choices`` parameter which is either:
280304

281-
* a list of ``(value, label)`` or ``(value, label, render_kw)`` tuples.
305+
* a list of :class:`Choice`.
282306
It can also be a list of only values, in which case the value is used
283-
as the label. If set, the ``render_kw`` dictionnary will be rendered as
284-
HTML ``<option>`` parameters. The value can be of any
307+
as the label. The value can be of any
285308
type, but because form data is sent to the browser as strings, you
286309
will need to provide a ``coerce`` function that converts a string
287310
back to the expected type.
288-
* a dictionary of ``{label: list}`` pairs defining groupings of options.
289-
* a function taking no argument, and returning either a list or a dictionary.
311+
* a function taking no argument, and returning a list of :class:`Choice`.
290312

291313

292314
**Select fields with static choice values**::
293315

294316
class PastebinEntry(Form):
295-
language = SelectField('Programming Language', choices=[('cpp', 'C++'), ('py', 'Python'), ('text', 'Plain Text')])
317+
language = SelectField('Programming Language', choices=[
318+
Choice('cpp', 'C++'),
319+
Choice('py', 'Python'),
320+
Choice('text', 'Plain Text'),
321+
])
296322

297-
Note that the `choices` keyword is only evaluated once, so if you want to make
298-
a dynamic drop-down list, you'll want to assign the choices list to the field
299-
after instantiation. Any submitted choices which are not in the given choices
300-
list will cause validation on the field to fail. If this option cannot be
301-
applied to your problem you may wish to skip choice validation (see below).
323+
**Select fields with ``<optgroup>``**::
324+
325+
Use :class:`SelectChoice` to assign an option to an ``<optgroup>``.
326+
327+
class PastebinEntry(Form):
328+
language = SelectField('Programming Language', choices=[
329+
SelectChoice('cpp', 'C++', optgroup='Compiled'),
330+
SelectChoice('rs', 'Rust', optgroup='Compiled'),
331+
SelectChoice('py', 'Python', optgroup='Interpreted'),
332+
SelectChoice('text', 'Plain Text'),
333+
])
302334

303335
**Select fields with dynamic choice values**::
304336

337+
def available_groups():
338+
return [Choice(g.id, g.name) for g in Group.query.order_by('name')]
339+
305340
class UserDetails(Form):
306-
group_id = SelectField('Group', coerce=int)
341+
group_id = SelectField('Group', coerce=int, choices=available_groups)
307342

308343
def edit_user(request, id):
309344
user = User.query.get(id)
310345
form = UserDetails(request.POST, obj=user)
311-
form.group_id.choices = [(g.id, g.name) for g in Group.query.order_by('name')]
312346

313-
Note we didn't pass a `choices` to the :class:`~wtforms.fields.SelectField`
314-
constructor, but rather created the list in the view function. Also, the
315-
`coerce` keyword arg to :class:`~wtforms.fields.SelectField` says that we
316-
use :func:`int()` to coerce form data. The default coerce is
317-
:func:`str()`.
347+
Note that the `coerce` keyword arg to :class:`~wtforms.fields.SelectField` says
348+
that we use :func:`int()` to coerce form data. The default coerce is :func:`str()`.
318349

319350
**Coerce function example**::
320351

@@ -324,7 +355,11 @@ refer to a single input from the form.
324355
return value
325356

326357
class NonePossible(Form):
327-
my_select_field = SelectField('Select an option', choices=[('1', 'Option 1'), ('2', 'Option 2'), ('None', 'No option')], coerce=coerce_none)
358+
my_select_field = SelectField('Select an option', choices=[
359+
Choice('1', 'Option 1'),
360+
Choice('2', 'Option 2'),
361+
Choice('None', 'No option'),
362+
], coerce=coerce_none)
328363

329364
Note when the option None is selected a 'None' str will be passed. By using a coerce
330365
function the 'None' str will be converted to None.
@@ -349,29 +384,13 @@ refer to a single input from the form.
349384
a list of fields each representing an option. The rendering of this can be
350385
further controlled by specifying `option_widget=`.
351386

352-
.. autoclass:: SearchField(default field arguments)
353-
354-
.. autoclass:: SelectMultipleField(default field arguments, choices=[], coerce=str, option_widget=None)
387+
.. autoclass:: SelectMultipleField(default field arguments, choices=None, coerce=str, option_widget=None)
355388

356389
The data on the SelectMultipleField is stored as a list of objects, each of
357390
which is checked and coerced from the form input. Any submitted choices
358391
which are not in the given choices list will cause validation on the field
359392
to fail.
360393

361-
.. autoclass:: SubmitField(default field arguments)
362-
363-
.. autoclass:: StringField(default field arguments)
364-
365-
.. code-block:: jinja
366-
367-
{{ form.username(size=30, maxlength=50) }}
368-
369-
.. autoclass:: TelField(default field arguments)
370-
371-
.. autoclass:: TimeField(default field arguments, format='%H:%M')
372-
373-
.. autoclass:: URLField(default field arguments)
374-
375394

376395
Convenience Fields
377396
------------------
@@ -461,7 +480,7 @@ complex data structures such as lists and nested objects can be represented.
461480
FormField::
462481

463482
class IMForm(Form):
464-
protocol = SelectField(choices=[('aim', 'AIM'), ('msn', 'MSN')])
483+
protocol = SelectField(choices=[Choice('aim', 'AIM'), Choice('msn', 'MSN')])
465484
username = StringField()
466485

467486
class ContactForm(Form):

docs/widgets.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ class. For example, here is a widget that renders a
9090
kwargs.setdefault('type', 'checkbox')
9191
field_id = kwargs.pop('id', field.id)
9292
html = ['<ul %s>' % html_params(id=field_id, class_=ul_class)]
93-
for value, label, checked, render_kw in field.iter_choices():
94-
choice_id = '%s-%s' % (field_id, value)
95-
options = dict(kwargs, name=field.name, value=value, id=choice_id)
96-
if checked:
93+
for choice in field.iter_choices():
94+
choice_id = '%s-%s' % (field_id, choice.value)
95+
options = dict(kwargs, name=field.name, value=choice.value, id=choice_id)
96+
if choice._selected:
9797
options['checked'] = 'checked'
9898
html.append('<li><input %s /> ' % html_params(**options))
99-
html.append('<label for="%s">%s</label></li>' % (choice_id, label))
99+
html.append('<label for="%s">%s</label></li>' % (choice_id, choice.label))
100100
html.append('</ul>')
101101
return ''.join(html)
102102

src/wtforms/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from wtforms import validators
22
from wtforms import widgets
3+
from wtforms.fields.choices import Choice
34
from wtforms.fields.choices import RadioField
5+
from wtforms.fields.choices import SelectChoice
46
from wtforms.fields.choices import SelectField
57
from wtforms.fields.choices import SelectFieldBase
68
from wtforms.fields.choices import SelectMultipleField
@@ -76,4 +78,6 @@
7678
"URLField",
7779
"EmailField",
7880
"ColorField",
81+
"Choice",
82+
"SelectChoice",
7983
]

src/wtforms/fields/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
from wtforms.fields.choices import Choice
12
from wtforms.fields.choices import RadioField
3+
from wtforms.fields.choices import SelectChoice
24
from wtforms.fields.choices import SelectField
35
from wtforms.fields.choices import SelectFieldBase
46
from wtforms.fields.choices import SelectMultipleField
@@ -37,6 +39,8 @@
3739
"Field",
3840
"Flags",
3941
"Label",
42+
"Choice",
43+
"SelectChoice",
4044
"SelectField",
4145
"SelectMultipleField",
4246
"SelectFieldBase",

0 commit comments

Comments
 (0)