Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/wtforms/fields/choices.py: 30%
114 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:32 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:32 +0000
1import itertools
3from wtforms import widgets
4from wtforms.fields.core import Field
5from wtforms.validators import ValidationError
7__all__ = (
8 "SelectField",
9 "SelectMultipleField",
10 "RadioField",
11)
14class SelectFieldBase(Field):
15 option_widget = widgets.Option()
17 """
18 Base class for fields which can be iterated to produce options.
20 This isn't a field, but an abstract base class for fields which want to
21 provide this functionality.
22 """
24 def __init__(self, label=None, validators=None, option_widget=None, **kwargs):
25 super().__init__(label, validators, **kwargs)
27 if option_widget is not None:
28 self.option_widget = option_widget
30 def iter_choices(self):
31 """
32 Provides data for choice widget rendering. Must return a sequence or
33 iterable of (value, label, selected) tuples.
34 """
35 raise NotImplementedError()
37 def has_groups(self):
38 return False
40 def iter_groups(self):
41 raise NotImplementedError()
43 def __iter__(self):
44 opts = dict(
45 widget=self.option_widget,
46 validators=self.validators,
47 name=self.name,
48 render_kw=self.render_kw,
49 _form=None,
50 _meta=self.meta,
51 )
52 for i, (value, label, checked) in enumerate(self.iter_choices()):
53 opt = self._Option(label=label, id="%s-%d" % (self.id, i), **opts)
54 opt.process(None, value)
55 opt.checked = checked
56 yield opt
58 class _Option(Field):
59 checked = False
61 def _value(self):
62 return str(self.data)
65class SelectField(SelectFieldBase):
66 widget = widgets.Select()
68 def __init__(
69 self,
70 label=None,
71 validators=None,
72 coerce=str,
73 choices=None,
74 validate_choice=True,
75 **kwargs,
76 ):
77 super().__init__(label, validators, **kwargs)
78 self.coerce = coerce
79 if callable(choices):
80 choices = choices()
81 if choices is not None:
82 self.choices = choices if isinstance(choices, dict) else list(choices)
83 else:
84 self.choices = None
85 self.validate_choice = validate_choice
87 def iter_choices(self):
88 if not self.choices:
89 choices = []
90 elif isinstance(self.choices, dict):
91 choices = list(itertools.chain.from_iterable(self.choices.values()))
92 else:
93 choices = self.choices
95 return self._choices_generator(choices)
97 def has_groups(self):
98 return isinstance(self.choices, dict)
100 def iter_groups(self):
101 if isinstance(self.choices, dict):
102 for label, choices in self.choices.items():
103 yield (label, self._choices_generator(choices))
105 def _choices_generator(self, choices):
106 if not choices:
107 _choices = []
109 elif isinstance(choices[0], (list, tuple)):
110 _choices = choices
112 else:
113 _choices = zip(choices, choices)
115 for value, label in _choices:
116 yield (value, label, self.coerce(value) == self.data)
118 def process_data(self, value):
119 try:
120 # If value is None, don't coerce to a value
121 self.data = self.coerce(value) if value is not None else None
122 except (ValueError, TypeError):
123 self.data = None
125 def process_formdata(self, valuelist):
126 if not valuelist:
127 return
129 try:
130 self.data = self.coerce(valuelist[0])
131 except ValueError as exc:
132 raise ValueError(self.gettext("Invalid Choice: could not coerce.")) from exc
134 def pre_validate(self, form):
135 if self.choices is None:
136 raise TypeError(self.gettext("Choices cannot be None."))
138 if not self.validate_choice:
139 return
141 for _, _, match in self.iter_choices():
142 if match:
143 break
144 else:
145 raise ValidationError(self.gettext("Not a valid choice."))
148class SelectMultipleField(SelectField):
149 """
150 No different from a normal select field, except this one can take (and
151 validate) multiple choices. You'll need to specify the HTML `size`
152 attribute to the select field when rendering.
153 """
155 widget = widgets.Select(multiple=True)
157 def _choices_generator(self, choices):
158 if choices:
159 if isinstance(choices[0], (list, tuple)):
160 _choices = choices
161 else:
162 _choices = zip(choices, choices)
163 else:
164 _choices = []
166 for value, label in _choices:
167 selected = self.data is not None and self.coerce(value) in self.data
168 yield (value, label, selected)
170 def process_data(self, value):
171 try:
172 self.data = list(self.coerce(v) for v in value)
173 except (ValueError, TypeError):
174 self.data = None
176 def process_formdata(self, valuelist):
177 try:
178 self.data = list(self.coerce(x) for x in valuelist)
179 except ValueError as exc:
180 raise ValueError(
181 self.gettext(
182 "Invalid choice(s): one or more data inputs could not be coerced."
183 )
184 ) from exc
186 def pre_validate(self, form):
187 if self.choices is None:
188 raise TypeError(self.gettext("Choices cannot be None."))
190 if not self.validate_choice or not self.data:
191 return
193 acceptable = {c[0] for c in self.iter_choices()}
194 if any(d not in acceptable for d in self.data):
195 unacceptable = [str(d) for d in set(self.data) - acceptable]
196 raise ValidationError(
197 self.ngettext(
198 "'%(value)s' is not a valid choice for this field.",
199 "'%(value)s' are not valid choices for this field.",
200 len(unacceptable),
201 )
202 % dict(value="', '".join(unacceptable))
203 )
206class RadioField(SelectField):
207 """
208 Like a SelectField, except displays a list of radio buttons.
210 Iterating the field will produce subfields (each containing a label as
211 well) in order to allow custom rendering of the individual radio fields.
212 """
214 widget = widgets.ListWidget(prefix_label=False)
215 option_widget = widgets.RadioInput()