Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
.. currentmodule:: wtforms

Version 3.3.3
-------------

Unreleased

- Fix :class:`~fields.SelectField` shorthand ``{value: label}`` choices
with lazy labels, such as Flask-Babel's ``lazy_gettext``. Each character
of the label was rendered as an option of an optgroup.

Version 3.3.2
-------------

Expand Down
7 changes: 5 additions & 2 deletions src/wtforms/fields/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,12 @@ def _choices_from_input(self, choices):
def _is_shorthand_dict(choices):
"""``True`` if ``choices`` matches the shorthand dict syntax.

Shorthand is ``dict[str, str | dict[str, str]]``.
Shorthand is ``{value: label}`` and ``{optgroup: {value: label}}``.
The legacy syntax is ``{optgroup: [choices]}``, so only list and tuple
values mark it. Lazy translation strings are not ``str`` but are
iterable, so they cannot be told apart from groups otherwise.
"""
return all(isinstance(v, (str, dict)) for v in choices.values())
return not any(isinstance(v, (list, tuple)) for v in choices.values())

@staticmethod
def _warn_legacy_choices(choices):
Expand Down
37 changes: 37 additions & 0 deletions tests/fields/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,43 @@ def test_dict_mixed_flat_and_optgroup_choices():
assert '<option value="ml">OCaml</option>' in html


class LazyLabel:
"""Iterable lazy string that is not a ``str``, like Flask-Babel's."""

def __init__(self, text):
self.text = text

def __str__(self):
return self.text

def __html__(self):
return self.text

def __iter__(self):
return iter(self.text)


def test_dict_shorthand_choices_with_lazy_labels(recwarn):
"""Lazy labels in the shorthand dict render as labels, not as optgroups."""
F = make_form(
a=SelectField(
choices={
"fr": LazyLabel("French"),
"Asia": {"ja": LazyLabel("Japanese")},
}
)
)
form = F(a="fr")
assert form.a() == (
'<select id="a" name="a">'
'<option selected value="fr">French</option>'
'<optgroup label="Asia"><option value="ja">Japanese</option></optgroup>'
"</select>"
)
assert form.validate()
assert not [w for w in recwarn if issubclass(w.category, DeprecationWarning)]


def test_tuple_choices_deprecation():
F = make_form(a=SelectField(choices=[("a", "Foo")]))
with pytest.warns(DeprecationWarning):
Expand Down
Loading