Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/wtforms/fields/choices.py: 29%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

122 statements  

1import itertools 

2 

3from wtforms import widgets 

4from wtforms.fields.core import Field 

5from wtforms.validators import ValidationError 

6 

7__all__ = ( 

8 "SelectField", 

9 "SelectMultipleField", 

10 "RadioField", 

11) 

12 

13 

14class SelectFieldBase(Field): 

15 option_widget = widgets.Option() 

16 

17 """ 

18 Base class for fields which can be iterated to produce options. 

19 

20 This isn't a field, but an abstract base class for fields which want to 

21 provide this functionality. 

22 """ 

23 

24 def __init__(self, label=None, validators=None, option_widget=None, **kwargs): 

25 super().__init__(label, validators, **kwargs) 

26 

27 if option_widget is not None: 

28 self.option_widget = option_widget 

29 

30 def iter_choices(self): 

31 """ 

32 Provides data for choice widget rendering. Must return a sequence or 

33 iterable of (value, label, selected, render_kw) tuples. 

34 """ 

35 raise NotImplementedError() 

36 

37 def has_groups(self): 

38 return False 

39 

40 def iter_groups(self): 

41 raise NotImplementedError() 

42 

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, choice in enumerate(self.iter_choices()): 

53 if len(choice) == 4: 

54 value, label, checked, render_kw = choice 

55 else: 

56 value, label, checked = choice 

57 render_kw = {} 

58 

59 opt = self._Option(label=label, id=f"{self.id}-{i}", **opts, **render_kw) 

60 opt.process(None, value) 

61 opt.checked = checked 

62 yield opt 

63 

64 class _Option(Field): 

65 checked = False 

66 

67 def _value(self): 

68 return str(self.data) 

69 

70 

71class SelectField(SelectFieldBase): 

72 widget = widgets.Select() 

73 

74 def __init__( 

75 self, 

76 label=None, 

77 validators=None, 

78 coerce=str, 

79 choices=None, 

80 validate_choice=True, 

81 **kwargs, 

82 ): 

83 super().__init__(label, validators, **kwargs) 

84 self.coerce = coerce 

85 if callable(choices): 

86 choices = choices() 

87 if choices is not None: 

88 self.choices = choices if isinstance(choices, dict) else list(choices) 

89 else: 

90 self.choices = None 

91 self.validate_choice = validate_choice 

92 

93 def iter_choices(self): 

94 if not self.choices: 

95 choices = [] 

96 elif isinstance(self.choices, dict): 

97 choices = list(itertools.chain.from_iterable(self.choices.values())) 

98 else: 

99 choices = self.choices 

100 

101 return self._choices_generator(choices) 

102 

103 def has_groups(self): 

104 return isinstance(self.choices, dict) 

105 

106 def iter_groups(self): 

107 if isinstance(self.choices, dict): 

108 for label, choices in self.choices.items(): 

109 yield (label, self._choices_generator(choices)) 

110 

111 def _choices_generator(self, choices): 

112 if not choices: 

113 _choices = [] 

114 

115 elif isinstance(choices[0], list | tuple): 

116 _choices = choices 

117 

118 else: 

119 _choices = zip(choices, choices, strict=False) 

120 

121 for value, label, *other_args in _choices: 

122 selected = self.coerce(value) == self.data 

123 render_kw = other_args[0] if len(other_args) else {} 

124 yield (value, label, selected, render_kw) 

125 

126 def process_data(self, value): 

127 try: 

128 # If value is None, don't coerce to a value 

129 self.data = self.coerce(value) if value is not None else None 

130 except (ValueError, TypeError): 

131 self.data = None 

132 

133 def process_formdata(self, valuelist): 

134 if not valuelist: 

135 return 

136 

137 try: 

138 self.data = self.coerce(valuelist[0]) 

139 except ValueError as exc: 

140 raise ValueError(self.gettext("Invalid Choice: could not coerce.")) from exc 

141 

142 def pre_validate(self, form): 

143 if not self.validate_choice: 

144 return 

145 

146 if self.choices is None: 

147 raise TypeError(self.gettext("Choices cannot be None.")) 

148 

149 for _, _, match, *_ in self.iter_choices(): 

150 if match: 

151 break 

152 else: 

153 raise ValidationError(self.gettext("Not a valid choice.")) 

154 

155 

156class SelectMultipleField(SelectField): 

157 """ 

158 No different from a normal select field, except this one can take (and 

159 validate) multiple choices. You'll need to specify the HTML `size` 

160 attribute to the select field when rendering. 

161 """ 

162 

163 widget = widgets.Select(multiple=True) 

164 

165 def _choices_generator(self, choices): 

166 if not choices: 

167 _choices = [] 

168 

169 elif isinstance(choices[0], list | tuple): 

170 _choices = choices 

171 

172 else: 

173 _choices = zip(choices, choices, strict=False) 

174 

175 for value, label, *other_args in _choices: 

176 selected = self.data is not None and self.coerce(value) in self.data 

177 render_kw = other_args[0] if len(other_args) else {} 

178 yield (value, label, selected, render_kw) 

179 

180 def process_data(self, value): 

181 try: 

182 self.data = list(self.coerce(v) for v in value) 

183 except (ValueError, TypeError): 

184 self.data = None 

185 

186 def process_formdata(self, valuelist): 

187 try: 

188 self.data = list(self.coerce(x) for x in valuelist) 

189 except ValueError as exc: 

190 raise ValueError( 

191 self.gettext( 

192 "Invalid choice(s): one or more data inputs could not be coerced." 

193 ) 

194 ) from exc 

195 

196 def pre_validate(self, form): 

197 if not self.validate_choice or not self.data: 

198 return 

199 

200 if self.choices is None: 

201 raise TypeError(self.gettext("Choices cannot be None.")) 

202 

203 acceptable = [self.coerce(choice[0]) for choice in self.iter_choices()] 

204 if any(data not in acceptable for data in self.data): 

205 unacceptable = [ 

206 str(data) for data in set(self.data) if data not in acceptable 

207 ] 

208 raise ValidationError( 

209 self.ngettext( 

210 "'%(value)s' is not a valid choice for this field.", 

211 "'%(value)s' are not valid choices for this field.", 

212 len(unacceptable), 

213 ) 

214 % dict(value="', '".join(unacceptable)) 

215 ) 

216 

217 

218class RadioField(SelectField): 

219 """ 

220 Like a SelectField, except displays a list of radio buttons. 

221 

222 Iterating the field will produce subfields (each containing a label as 

223 well) in order to allow custom rendering of the individual radio fields. 

224 """ 

225 

226 widget = widgets.ListWidget(prefix_label=False) 

227 option_widget = widgets.RadioInput()