Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/caselessdict.py: 69%

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

78 statements  

1from __future__ import annotations 

2 

3from collections import OrderedDict 

4from typing import TYPE_CHECKING, Any, TypeVar 

5 

6from icalendar.parser_tools import to_unicode 

7 

8if TYPE_CHECKING: 

9 from collections.abc import Iterable, Mapping 

10 

11try: 

12 from typing import Self 

13except ImportError: 

14 from typing_extensions import Self 

15 

16KT = TypeVar("KT") 

17VT = TypeVar("VT") 

18_VT = TypeVar("_VT") 

19 

20 

21def canonsort_keys( 

22 keys: Iterable[KT], canonical_order: Iterable[KT] | None = None 

23) -> list[KT]: 

24 """Sort leading keys according to a canonical order. 

25 

26 Keys specified in ``canonical_order`` appear first in that order. 

27 Remaining keys appear alphabetically at the end. 

28 

29 Parameters: 

30 keys: The keys to sort. 

31 canonical_order: The preferred order for leading keys. 

32 Keys not in this sequence are sorted alphabetically after 

33 the canonical ones. If ``None``, all keys are sorted 

34 alphabetically. 

35 

36 Returns: 

37 A new list of keys sorted by canonical order first, then alphabetically. 

38 

39 Example: 

40 .. code-block:: pycon 

41 

42 >>> from icalendar.caselessdict import canonsort_keys 

43 >>> canonsort_keys(["C", "A", "B"], ["B", "C"]) 

44 ['B', 'C', 'A'] 

45 """ 

46 canonical_map = {k: i for i, k in enumerate(canonical_order or [])} 

47 head = [k for k in keys if k in canonical_map] 

48 tail = [k for k in keys if k not in canonical_map] 

49 return sorted(head, key=lambda k: canonical_map[k]) + sorted(tail) 

50 

51 

52def canonsort_items( 

53 dict1: Mapping[KT, VT], canonical_order: Iterable[KT] | None = None 

54) -> list[tuple[KT, VT]]: 

55 """Sort items from a mapping according to a canonical key order. 

56 

57 Parameters: 

58 dict1: The mapping whose items to sort. 

59 canonical_order: The preferred order for leading keys. 

60 If ``None``, all keys are sorted alphabetically. 

61 

62 Returns: 

63 A list of ``(key, value)`` tuples sorted by canonical order. 

64 

65 Example: 

66 .. code-block:: pycon 

67 

68 >>> from icalendar.caselessdict import canonsort_items 

69 >>> canonsort_items({"C": 3, "A": 1, "B": 2}, ["B", "C"]) 

70 [('B', 2), ('C', 3), ('A', 1)] 

71 """ 

72 return [(k, dict1[k]) for k in canonsort_keys(dict1.keys(), canonical_order)] 

73 

74 

75class CaselessDict(OrderedDict[str, _VT]): 

76 """A case-insensitive dictionary that uses strings as keys. 

77 

78 All keys are stored in uppercase internally, but values retain 

79 their original case. Keys can be provided as ``str`` or ``bytes``. 

80 They are converted to Unicode via :func:`~icalendar.parser_tools.to_unicode`, 

81 then uppercased before storage. 

82 """ 

83 

84 def __init__(self, *args: Any, **kwargs: Any) -> None: 

85 """Parameters: 

86 *args: Positional arguments passed to :class:`~collections.OrderedDict`. 

87 **kwargs: Keyword arguments passed to :class:`~collections.OrderedDict`. 

88 

89 Example: 

90 

91 Create a new ``CaselessDict`` and normalize existing keys to uppercase. 

92 

93 .. code-block:: pycon 

94 

95 >>> from icalendar.caselessdict import CaselessDict 

96 >>> d = CaselessDict(summary="Meeting") 

97 >>> d["SUMMARY"] 

98 'Meeting' 

99 >>> "summary" in d 

100 True 

101 """ 

102 super().__init__(*args, **kwargs) 

103 for key, value in self.items(): 

104 key_upper = to_unicode(key).upper() 

105 if key != key_upper: 

106 super().__delitem__(key) 

107 self[key_upper] = value 

108 

109 __hash__ = None 

110 

111 def __getitem__(self, key: str) -> _VT: 

112 """Get the item from the ``CaselessDict`` instance by 

113 ``key``, case-insensitively. 

114 

115 Parameters: 

116 key: The key to look up, case-insensitively. 

117 

118 Returns: 

119 The (key, value) pair associated with the uppercased key. 

120 

121 Raises: 

122 KeyError: If the key is not found. 

123 """ 

124 key = to_unicode(key) 

125 return super().__getitem__(key.upper()) 

126 

127 def __setitem__(self, key: str, value: _VT) -> None: 

128 """Set a (key, value) pair, storing the key in uppercase. 

129 

130 Parameters: 

131 key: The key of the pair, case-insensitive. 

132 value: The value to associate with the key. 

133 """ 

134 key = to_unicode(key) 

135 super().__setitem__(key.upper(), value) 

136 

137 def __delitem__(self, key: str) -> None: 

138 """Delete a (key, value) pair by its case-insensitive key. 

139 

140 Parameters: 

141 key: The key to delete, case-insensitively. 

142 

143 Raises: 

144 KeyError: If the key is not found. 

145 """ 

146 key = to_unicode(key) 

147 super().__delitem__(key.upper()) 

148 

149 def __contains__(self, key: object) -> bool: 

150 """Check whether a key exists in the mapping, case-insensitively. 

151 

152 Parameters: 

153 key: The key to check case-insensitively. 

154 

155 Returns: 

156 ``True`` if the uppercased key exists, else ``False``. 

157 """ 

158 key = to_unicode(key) 

159 return super().__contains__(key.upper()) 

160 

161 def get(self, key: str, default: _VT | None = None) -> _VT | None: 

162 """Return the ``key``, optionally with a ``default`` value. 

163 

164 Parameters: 

165 key: The key to look up, case-insensitively. 

166 default: The value to return if the key is not found. 

167 

168 Returns: 

169 The value for the key, if present, else the value specified by ``default``. 

170 """ 

171 key = to_unicode(key) 

172 return super().get(key.upper(), default) 

173 

174 def setdefault(self, key: str, value: _VT | None = None) -> _VT: 

175 """Create the (key, value) pair, optionally with a ``value``. 

176 

177 Once set, to change default value use :meth:`update`. 

178 

179 Parameters: 

180 key: The key to look up or create, case-insensitively. 

181 value: The default value to set, if given, else ``None``. 

182 

183 Returns: 

184 The value for the key. 

185 """ 

186 key = to_unicode(key) 

187 return super().setdefault(key.upper(), value) 

188 

189 def pop(self, key: str, default: _VT | None = None) -> _VT | None: 

190 """Remove and return the value for ``key``, or ``default`` if not found. 

191 

192 Parameters: 

193 key: The key to remove, case-insensitively. 

194 default: The value to return if the key is not found. 

195 

196 Returns: 

197 The removed value, or the value of ``default``. 

198 """ 

199 key = to_unicode(key) 

200 return super().pop(key.upper(), default) 

201 

202 def popitem(self) -> tuple[str, _VT]: 

203 """Remove and return the last inserted (key, value) pair. 

204 

205 Returns: 

206 A (key, value) tuple. 

207 

208 Raises: 

209 KeyError: If the dictionary is empty. 

210 """ 

211 return super().popitem() 

212 

213 def has_key(self, key: str) -> bool: 

214 """Check whether a key exists, case-insensitively. 

215 

216 This is a legacy method. Use ``key in dict`` instead. 

217 

218 Parameters: 

219 key: The key to check, case-insensitively. 

220 

221 Returns: 

222 ``True`` if the key exists, else ``False``. 

223 """ 

224 key = to_unicode(key) 

225 return super().__contains__(key.upper()) 

226 

227 def update(self, *args: Any, **kwargs: Any) -> None: 

228 """Update the dictionary with (key, value) pairs, normalizing keys to uppercase. 

229 

230 Multiple keys that differ only in case will overwrite each other. 

231 Only the last value is retained. 

232 

233 Parameters: 

234 *args: Mappings or iterables of (key, value) pairs. 

235 **kwargs: Additional (key, value) pairs. 

236 """ 

237 # Multiple keys where key1.upper() == key2.upper() will be lost. 

238 mappings = list(args) + [kwargs] 

239 for mapping in mappings: 

240 if hasattr(mapping, "items"): 

241 mapping = iter(mapping.items()) # noqa: PLW2901 

242 for key, value in mapping: 

243 self[key] = value 

244 

245 def copy(self) -> Self: 

246 """Return a shallow copy of the dictionary. 

247 

248 Returns: 

249 A new instance of the same type with the same contents. 

250 """ 

251 return type(self)(super().copy()) 

252 

253 def __repr__(self) -> str: 

254 """Return a string representation of the dictionary. 

255 

256 Returns: 

257 A string in the form ``CaselessDict({...})``. 

258 """ 

259 return f"{type(self).__name__}({dict(self)})" 

260 

261 def __eq__(self, other: object) -> bool: 

262 """Check equality with another dictionary. 

263 

264 Two ``CaselessDict`` instances are equal if they contain the same 

265 (key, value) pairs after uppercasing keys. Comparison with a regular 

266 ``dict`` also works. 

267 

268 Parameters: 

269 other: The object to compare. 

270 

271 Returns: 

272 ``True`` if equal, ``NotImplemented`` if ``other`` is not a ``dict``. 

273 """ 

274 if not isinstance(other, dict): 

275 return NotImplemented 

276 return self is other or dict(self.items()) == dict(other.items()) 

277 

278 def __ne__(self, other: object) -> bool: 

279 """Check inequality with another dictionary. 

280 

281 Parameters: 

282 other: The object to compare. 

283 

284 Returns: 

285 ``True`` if not equal, else ``False``. 

286 """ 

287 return not self == other 

288 

289 # A list of keys that must appear first in sorted_keys and sorted_items; 

290 # must be uppercase. 

291 canonical_order = None 

292 

293 def sorted_keys(self) -> list[str]: 

294 """Sort keys according to the canonical order for this class. 

295 

296 Keys listed in :attr:`canonical_order` appear first in that order. 

297 Remaining keys appear alphabetically at the end. 

298 

299 Returns: 

300 A sorted list of keys. 

301 """ 

302 return canonsort_keys(self.keys(), self.canonical_order) 

303 

304 def sorted_items(self) -> list[tuple[str, _VT]]: 

305 """Sort items according to the canonical order for this class. 

306 

307 Items whose keys are listed in :attr:`canonical_order` appear first 

308 in that order. Remaining items appear alphabetically by key. 

309 

310 Returns: 

311 A sorted list of (key, value) tuples. 

312 """ 

313 return canonsort_items(self, self.canonical_order) 

314 

315 

316__all__ = ["CaselessDict", "canonsort_items", "canonsort_keys"]