Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/utils/datastructures.py: 65%

1 import copy

2 from collections.abc import Mapping

3

4

5 class OrderedSet:

6 """

7 A set which keeps the ordering of the inserted items.

8 """

9

10 def __init__(self, iterable=None):

11 self.dict = dict.fromkeys(iterable or ())

12

13 def add(self, item):

14 self.dict[item] = None

15

16 def remove(self, item):

17 del self.dict[item]

18

19 def discard(self, item):

20 try:

21 self.remove(item)

22 except KeyError:

23 pass

24

25 def __iter__(self):

26 return iter(self.dict)

27

28 def __reversed__(self):

29 return reversed(self.dict)

30

31 def __contains__(self, item):

32 return item in self.dict

33

34 def __bool__(self):

35 return bool(self.dict)

36

37 def __len__(self):

38 return len(self.dict)

39

40 def __repr__(self):

41 data = repr(list(self.dict)) if self.dict else ""

42 return f"{self.__class__.__qualname__}({data})"

43

44

45 class MultiValueDictKeyError(KeyError):

46 pass

47

48

49 class MultiValueDict(dict):

50 """

51 A subclass of dictionary customized to handle multiple values for the

52 same key.

53

54 >>> d = MultiValueDict({'name': ['Adrian', 'Simon'], 'position': ['Developer']})

55 >>> d['name']

56 'Simon'

57 >>> d.getlist('name')

58 ['Adrian', 'Simon']

59 >>> d.getlist('doesnotexist')

60 []

61 >>> d.getlist('doesnotexist', ['Adrian', 'Simon'])

62 ['Adrian', 'Simon']

63 >>> d.get('lastname', 'nonexistent')

64 'nonexistent'

65 >>> d.setlist('lastname', ['Holovaty', 'Willison'])

66

67 This class exists to solve the irritating problem raised by cgi.parse_qs,

68 which returns a list for every key, even though most web forms submit

69 single name-value pairs.

70 """

71

72 def __init__(self, key_to_list_mapping=()):

73 super().__init__(key_to_list_mapping)

74

75 def __repr__(self):

76 return f"<{self.__class__.__name__}: {super().__repr__()}>"

77

78 def __getitem__(self, key):

79 """

80 Return the last data value for this key, or [] if it's an empty list;

81 raise KeyError if not found.

82 """

83 try:

84 list_ = super().__getitem__(key)

85 except KeyError:

86 raise MultiValueDictKeyError(key)

87 try:

88 return list_[-1]

89 except IndexError:

90 return []

91

92 def __setitem__(self, key, value):

93 super().__setitem__(key, [value])

94

95 def __copy__(self):

96 return self.__class__([(k, v[:]) for k, v in self.lists()])

97

98 def __deepcopy__(self, memo):

99 result = self.__class__()

100 memo[id(self)] = result

101 for key, value in dict.items(self):

102 dict.__setitem__(

103 result, copy.deepcopy(key, memo), copy.deepcopy(value, memo)

104 )

105 return result

106

107 def __getstate__(self):

108 return {**self.__dict__, "_data": {k: self._getlist(k) for k in self}}

109

110 def __setstate__(self, obj_dict):

111 data = obj_dict.pop("_data", {})

112 for k, v in data.items():

113 self.setlist(k, v)

114 self.__dict__.update(obj_dict)

115

116 def get(self, key, default=None):

117 """

118 Return the last data value for the passed key. If key doesn't exist

119 or value is an empty list, return `default`.

120 """

121 try:

122 val = self[key]

123 except KeyError:

124 return default

125 if val == []:

126 return default

127 return val

128

129 def _getlist(self, key, default=None, force_list=False):

130 """

131 Return a list of values for the key.

132

133 Used internally to manipulate values list. If force_list is True,

134 return a new copy of values.

135 """

136 try:

137 values = super().__getitem__(key)

138 except KeyError:

139 if default is None:

140 return []

141 return default

142 else:

143 if force_list:

144 values = list(values) if values is not None else None

145 return values

146

147 def getlist(self, key, default=None):

148 """

149 Return the list of values for the key. If key doesn't exist, return a

150 default value.

151 """

152 return self._getlist(key, default, force_list=True)

153

154 def setlist(self, key, list_):

155 super().__setitem__(key, list_)

156

157 def setdefault(self, key, default=None):

158 if key not in self:

159 self[key] = default

160 # Do not return default here because __setitem__() may store

161 # another value -- QueryDict.__setitem__() does. Look it up.

162 return self[key]

163

164 def setlistdefault(self, key, default_list=None):

165 if key not in self:

166 if default_list is None:

167 default_list = []

168 self.setlist(key, default_list)

169 # Do not return default_list here because setlist() may store

170 # another value -- QueryDict.setlist() does. Look it up.

171 return self._getlist(key)

172

173 def appendlist(self, key, value):

174 """Append an item to the internal list associated with key."""

175 self.setlistdefault(key).append(value)

176

177 def items(self):

178 """

179 Yield (key, value) pairs, where value is the last item in the list

180 associated with the key.

181 """

182 for key in self:

183 yield key, self[key]

184

185 def lists(self):

186 """Yield (key, list) pairs."""

187 return iter(super().items())

188

189 def values(self):

190 """Yield the last value on every key list."""

191 for key in self:

192 yield self[key]

193

194 def copy(self):

195 """Return a shallow copy of this object."""

196 return copy.copy(self)

197

198 def update(self, *args, **kwargs):

199 """Extend rather than replace existing key lists."""

200 if len(args) > 1:

201 raise TypeError("update expected at most 1 argument, got %d" % len(args))

202 if args:

203 arg = args[0]

204 if isinstance(arg, MultiValueDict):

205 for key, value_list in arg.lists():

206 self.setlistdefault(key).extend(value_list)

207 else:

208 if isinstance(arg, Mapping):

209 arg = arg.items()

210 for key, value in arg:

211 self.setlistdefault(key).append(value)

212 for key, value in kwargs.items():

213 self.setlistdefault(key).append(value)

214

215 def dict(self):

216 """Return current object as a dict with singular values."""

217 return {key: self[key] for key in self}

218

219

220 class ImmutableList(tuple):

221 """

222 A tuple-like object that raises useful errors when it is asked to mutate.

223

224 Example::

225

226 >>> a = ImmutableList(range(5), warning="You cannot mutate this.")

227 >>> a[3] = '4'

228 Traceback (most recent call last):

229 ...

230 AttributeError: You cannot mutate this.

231 """

232

233 def __new__(cls, *args, warning="ImmutableList object is immutable.", **kwargs):

234 self = tuple.__new__(cls, *args, **kwargs)

235 self.warning = warning

236 return self

237

238 def complain(self, *args, **kwargs):

239 raise AttributeError(self.warning)

240

241 # All list mutation functions complain.

242 __delitem__ = complain

243 __delslice__ = complain

244 __iadd__ = complain

245 __imul__ = complain

246 __setitem__ = complain

247 __setslice__ = complain

248 append = complain

249 extend = complain

250 insert = complain

251 pop = complain

252 remove = complain

253 sort = complain

254 reverse = complain

255

256

257 class DictWrapper(dict):

258 """

259 Wrap accesses to a dictionary so that certain values (those starting with

260 the specified prefix) are passed through a function before being returned.

261 The prefix is removed before looking up the real value.

262

263 Used by the SQL construction code to ensure that values are correctly

264 quoted before being used.

265 """

266

267 def __init__(self, data, func, prefix):

268 super().__init__(data)

269 self.func = func

270 self.prefix = prefix

271

272 def __getitem__(self, key):

273 """

274 Retrieve the real value after stripping the prefix string (if

275 present). If the prefix is present, pass the value through self.func

276 before returning, otherwise return the raw value.

277 """

278 use_func = key.startswith(self.prefix)

279 key = key.removeprefix(self.prefix)

280 value = super().__getitem__(key)

281 if use_func:

282 return self.func(value)

283 return value

284

285

286 class CaseInsensitiveMapping(Mapping):

287 """

288 Mapping allowing case-insensitive key lookups. Original case of keys is

289 preserved for iteration and string representation.

290

291 Example::

292

293 >>> ci_map = CaseInsensitiveMapping({'name': 'Jane'})

294 >>> ci_map['Name']

295 Jane

296 >>> ci_map['NAME']

297 Jane

298 >>> ci_map['name']

299 Jane

300 >>> ci_map # original case preserved

301 {'name': 'Jane'}

302 """

303

304 def __init__(self, data):

305 self._store = {k.lower(): (k, v) for k, v in self._unpack_items(data)}

306

307 def __getitem__(self, key):

308 return self._store[key.lower()][1]

309

310 def __len__(self):

311 return len(self._store)

312

313 def __eq__(self, other):

314 return isinstance(other, Mapping) and {

315 k.lower(): v for k, v in self.items()

316 } == {k.lower(): v for k, v in other.items()}

317

318 def __iter__(self):

319 return (original_key for original_key, value in self._store.values())

320

321 def __repr__(self):

322 return repr(dict(self._store.values()))

323

324 def copy(self):

325 return self

326

327 @staticmethod

328 def _unpack_items(data):

329 # Explicitly test for dict first as the common case for performance,

330 # avoiding abc's __instancecheck__ and _abc_instancecheck for the

331 # general Mapping case.

332 if isinstance(data, dict | Mapping):

333 yield from data.items()

334 return

335 for i, elem in enumerate(data):

336 if len(elem) != 2:

337 raise ValueError(

338 f"dictionary update sequence element #{i} has length {len(elem)}; "

339 "2 is required."

340 )

341 if not isinstance(elem[0], str):

342 raise ValueError(

343 "Element key %r invalid, only strings are allowed" % elem[0]

344 )

345 yield elem