Repository: wtforms/wtforms
Pull request #608 (2020-05-23 16:32:05)

SelectField choices can be callable
`SelectField`, `SelectMultipleField` and `RadioField` *choices* parameter can now a be a function.

The function is called at each form instantiation, thus not at the form definition. This should provide a more elegant way than `myform.choices = ...`


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 5d2fff9..a7ecb68 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,6 +20,9 @@ Unreleased
     :class:`~validators.ValidationError` or
     :class:`~validators.StopValidation` to make a validation fail.
     :issue:`445`
+-   :class:`~fields.SelectField`, :class:`~fields.SelectMultipleField` and
+    :class:`~fields.RadioField` *choices* parameter can be a callable.
+    :pr:`608`
 
 
 Version 2.3.1
diff --git a/docs/fields.rst b/docs/fields.rst
index c7e3da7..2d183d0 100644
--- a/docs/fields.rst
+++ b/docs/fields.rst
@@ -268,8 +268,9 @@ refer to a single input from the form.
 
 .. class:: SelectField(default field arguments, choices=[], coerce=str, option_widget=None, validate_choice=True)
 
-    Select fields take a ``choices`` parameter which is a list of
-    ``(value, label)`` pairs. It can also be a list of only values, in
+    Select fields take a ``choices`` parameter which is eitherr a list of
+    ``(value, label)`` pairs, or a function taking no argument, and returning
+    a list of ``(value, label)`` pairs. It can also be a list of only values, in
     which case the value is used as the label. The value can be any
     type, but because form data is sent to the browser as strings, you
     will need to provide a ``coerce`` function that converts a string
diff --git a/src/wtforms/fields/core.py b/src/wtforms/fields/core.py
index 9c4b46a..e5fdb7e 100644
--- a/src/wtforms/fields/core.py
+++ b/src/wtforms/fields/core.py
@@ -505,6 +505,8 @@ class SelectField(SelectFieldBase):
     ):
         super().__init__(label, validators, **kwargs)
         self.coerce = coerce
+        if callable(choices):
+            choices = choices()
         self.choices = list(choices) if choices is not None else None
         self.validate_choice = validate_choice
 


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 5cf7cd5..965b951 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -419,6 +419,18 @@ class TestSelectField:
         form = F(a="bar")
         assert form.a() == '<select id="a" name="a"></select>'
 
+    def test_callable_choices(self):
+        def choices():
+            return ["foo", "bar"]
+
+        F = make_form(a=SelectField(choices=choices))
+        form = F(a="bar")
+
+        assert list(str(x) for x in form.a) == [
+            '<option value="foo">foo</option>',
+            '<option selected value="bar">bar</option>',
+        ]
+
 
 class TestSelectMultipleField:
     class F(Form):
@@ -462,6 +474,18 @@ class TestSelectMultipleField:
         assert not form.validate()
         assert form.b.data == [1, 3]
 
+    def test_callable_choices(self):
+        def choices():
+            return ["foo", "bar"]
+
+        F = make_form(a=SelectField(choices=choices))
+        form = F(a="bar")
+
+        assert list(str(x) for x in form.a) == [
+            '<option value="foo">foo</option>',
+            '<option selected value="bar">bar</option>',
+        ]
+
 
 class TestRadioField:
     class F(Form):
@@ -512,6 +536,24 @@ class TestRadioField:
             "</ul>"
         )
 
+    def test_callable_choices(self):
+        def choices():
+            return [("a", "hello"), ("b", "bye")]
+
+        class F(Form):
+            a = RadioField(choices=choices, default="a")
+
+        form = self.F()
+        assert form.a.data == "a"
+        assert form.a() == (
+            '<ul id="a">'
+            '<li><input checked id="a-0" name="a" type="radio" value="a"> '
+            '<label for="a-0">hello</label></li>'
+            '<li><input id="a-1" name="a" type="radio" value="b"> '
+            '<label for="a-1">bye</label></li>'
+            "</ul>"
+        )
+
 
 class TestStringField:
     class F(Form):