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

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

103 statements  

1import itertools 

2 

3from wtforms.utils import unset_value 

4 

5from .. import widgets 

6from .core import Field 

7from .core import UnboundField 

8 

9__all__ = ("FieldList",) 

10 

11 

12class FieldList(Field): 

13 """ 

14 Encapsulate an ordered list of multiple instances of the same field type, 

15 keeping data as a list. 

16 

17 >>> authors = FieldList(StringField('Name', [validators.DataRequired()])) 

18 

19 :param unbound_field: 

20 A partially-instantiated field definition, just like that would be 

21 defined on a form directly. 

22 :param min_entries: 

23 if provided, always have at least this many entries on the field, 

24 creating blank ones if the provided input does not specify a sufficient 

25 amount. 

26 :param max_entries: 

27 accept no more than this many entries as input, even if more exist in 

28 formdata. 

29 :param separator: 

30 A string which will be suffixed to this field's name to create the 

31 prefix to enclosed list entries. The default is fine for most uses. 

32 """ 

33 

34 widget = widgets.ListWidget() 

35 

36 def __init__( 

37 self, 

38 unbound_field, 

39 label=None, 

40 validators=None, 

41 min_entries=0, 

42 max_entries=None, 

43 separator="-", 

44 default=(), 

45 **kwargs, 

46 ): 

47 super().__init__(label, validators, default=default, **kwargs) 

48 if self.filters: 

49 raise TypeError( 

50 "FieldList does not accept any filters. Instead, define" 

51 " them on the enclosed field." 

52 ) 

53 assert isinstance( 

54 unbound_field, UnboundField 

55 ), "Field must be unbound, not a field class" 

56 self.unbound_field = unbound_field 

57 self.min_entries = min_entries 

58 self.max_entries = max_entries 

59 self.last_index = -1 

60 self._prefix = kwargs.get("_prefix", "") 

61 self._separator = separator 

62 self._field_separator = unbound_field.kwargs.get("separator", "-") 

63 

64 def process(self, formdata, data=unset_value, extra_filters=None): 

65 if extra_filters: 

66 raise TypeError( 

67 "FieldList does not accept any filters. Instead, define" 

68 " them on the enclosed field." 

69 ) 

70 

71 self.entries = [] 

72 if data is unset_value or not data: 

73 try: 

74 data = self.default() 

75 except TypeError: 

76 data = self.default 

77 

78 self.object_data = data 

79 

80 if formdata: 

81 indices = sorted(set(self._extract_indices(self.name, formdata))) 

82 if self.max_entries: 

83 indices = indices[: self.max_entries] 

84 

85 idata = iter(data) 

86 for index in indices: 

87 try: 

88 obj_data = next(idata) 

89 except StopIteration: 

90 obj_data = unset_value 

91 self._add_entry(formdata, obj_data, index=index) 

92 else: 

93 for obj_data in data: 

94 self._add_entry(formdata, obj_data) 

95 

96 while len(self.entries) < self.min_entries: 

97 self._add_entry(formdata) 

98 

99 def _extract_indices(self, prefix, formdata): 

100 """ 

101 Yield indices of any keys with given prefix. 

102 

103 formdata must be an object which will produce keys when iterated. For 

104 example, if field 'foo' contains keys 'foo-0-bar', 'foo-1-baz', then 

105 the numbers 0 and 1 will be yielded, but not necessarily in order. 

106 """ 

107 offset = len(prefix) + 1 

108 for k in formdata: 

109 if k.startswith(prefix): 

110 k = k[offset:].split(self._field_separator, 1)[0] 

111 if k.isdigit(): 

112 yield int(k) 

113 

114 def validate(self, form, extra_validators=()): 

115 """ 

116 Validate this FieldList. 

117 

118 Note that FieldList validation differs from normal field validation in 

119 that FieldList validates all its enclosed fields first before running any 

120 of its own validators. 

121 """ 

122 self.errors = [] 

123 

124 # Run validators on all entries within 

125 for subfield in self.entries: 

126 subfield.validate(form) 

127 self.errors.append(subfield.errors) 

128 

129 if not any(x for x in self.errors): 

130 self.errors = [] 

131 

132 chain = itertools.chain(self.validators, extra_validators) 

133 self._run_validation_chain(form, chain) 

134 

135 return len(self.errors) == 0 

136 

137 def populate_obj(self, obj, name): 

138 values = getattr(obj, name, None) 

139 try: 

140 ivalues = iter(values) 

141 except TypeError: 

142 ivalues = iter([]) 

143 

144 candidates = itertools.chain(ivalues, itertools.repeat(None)) 

145 _fake = type("_fake", (object,), {}) 

146 output = [] 

147 for field, data in zip(self.entries, candidates): 

148 fake_obj = _fake() 

149 fake_obj.data = data 

150 field.populate_obj(fake_obj, "data") 

151 output.append(fake_obj.data) 

152 

153 setattr(obj, name, output) 

154 

155 def _add_entry(self, formdata=None, data=unset_value, index=None): 

156 assert ( 

157 not self.max_entries or len(self.entries) < self.max_entries 

158 ), "You cannot have more than max_entries entries in this FieldList" 

159 if index is None: 

160 index = self.last_index + 1 

161 self.last_index = index 

162 name = f"{self.short_name}{self._separator}{index}" 

163 id = f"{self.id}{self._separator}{index}" 

164 field = self.unbound_field.bind( 

165 form=None, 

166 name=name, 

167 prefix=self._prefix, 

168 id=id, 

169 _meta=self.meta, 

170 translations=self._translations, 

171 ) 

172 field.process(formdata, data) 

173 self.entries.append(field) 

174 return field 

175 

176 def append_entry(self, data=unset_value): 

177 """ 

178 Create a new entry with optional default data. 

179 

180 Entries added in this way will *not* receive formdata however, and can 

181 only receive object data. 

182 """ 

183 return self._add_entry(data=data) 

184 

185 def pop_entry(self): 

186 """Removes the last entry from the list and returns it.""" 

187 entry = self.entries.pop() 

188 self.last_index -= 1 

189 return entry 

190 

191 def __iter__(self): 

192 return iter(self.entries) 

193 

194 def __len__(self): 

195 return len(self.entries) 

196 

197 def __getitem__(self, index): 

198 return self.entries[index] 

199 

200 @property 

201 def data(self): 

202 return [f.data for f in self.entries]