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

77 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 

19 

20def canonsort_keys( 

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

22) -> list[KT]: 

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

24 

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

26 Remaining keys appear alphabetically at the end. 

27 

28 Parameters: 

29 keys: The keys to sort. 

30 canonical_order: The preferred order for leading keys. 

31 Keys not in this sequence are sorted alphabetically after 

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

33 alphabetically. 

34 

35 Returns: 

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

37 

38 Example: 

39 .. code-block:: pycon 

40 

41 >>> from icalendar.caselessdict import canonsort_keys 

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

43 ['B', 'C', 'A'] 

44 """ 

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

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

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

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

49 

50 

51def canonsort_items( 

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

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

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

55 

56 Parameters: 

57 dict1: The mapping whose items to sort. 

58 canonical_order: The preferred order for leading keys. 

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

60 

61 Returns: 

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

63 

64 Example: 

65 .. code-block:: pycon 

66 

67 >>> from icalendar.caselessdict import canonsort_items 

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

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

70 """ 

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

72 

73 

74class CaselessDict(OrderedDict): 

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

76 

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

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

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

80 then uppercased before storage. 

81 """ 

82 

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

84 """Parameters: 

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

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

87 

88 Example: 

89 

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

91 

92 .. code-block:: pycon 

93 

94 >>> from icalendar.caselessdict import CaselessDict 

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

96 >>> d["SUMMARY"] 

97 'Meeting' 

98 >>> "summary" in d 

99 True 

100 """ 

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

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

103 key_upper = to_unicode(key).upper() 

104 if key != key_upper: 

105 super().__delitem__(key) 

106 self[key_upper] = value 

107 

108 __hash__ = None 

109 

110 def __getitem__(self, key: Any) -> Any: 

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

112 ``key``, case-insensitively. 

113 

114 Parameters: 

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

116 

117 Returns: 

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

119 

120 Raises: 

121 KeyError: If the key is not found. 

122 """ 

123 key = to_unicode(key) 

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

125 

126 def __setitem__(self, key: Any, value: Any) -> None: 

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

128 

129 Parameters: 

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

131 value: The value to associate with the key. 

132 """ 

133 key = to_unicode(key) 

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

135 

136 def __delitem__(self, key: Any) -> None: 

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

138 

139 Parameters: 

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

141 

142 Raises: 

143 KeyError: If the key is not found. 

144 """ 

145 key = to_unicode(key) 

146 super().__delitem__(key.upper()) 

147 

148 def __contains__(self, key: Any) -> bool: 

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

150 

151 Parameters: 

152 key: The key to check case-insensitively. 

153 

154 Returns: 

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

156 """ 

157 key = to_unicode(key) 

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

159 

160 def get(self, key: Any, default: Any = None) -> Any: 

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

162 

163 Parameters: 

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

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

166 

167 Returns: 

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

169 """ 

170 key = to_unicode(key) 

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

172 

173 def setdefault(self, key: Any, value: Any = None) -> Any: 

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

175 

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

177 

178 Parameters: 

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

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

181 

182 Returns: 

183 The value for the key. 

184 """ 

185 key = to_unicode(key) 

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

187 

188 def pop(self, key: Any, default: Any = None) -> Any: 

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

190 

191 Parameters: 

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

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

194 

195 Returns: 

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

197 """ 

198 key = to_unicode(key) 

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

200 

201 def popitem(self) -> tuple[Any, Any]: 

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

203 

204 Returns: 

205 A (key, value) tuple. 

206 

207 Raises: 

208 KeyError: If the dictionary is empty. 

209 """ 

210 return super().popitem() 

211 

212 def has_key(self, key: Any) -> bool: 

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

214 

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

216 

217 Parameters: 

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

219 

220 Returns: 

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

222 """ 

223 key = to_unicode(key) 

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

225 

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

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

228 

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

230 Only the last value is retained. 

231 

232 Parameters: 

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

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

235 """ 

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

237 mappings = list(args) + [kwargs] 

238 for mapping in mappings: 

239 if hasattr(mapping, "items"): 

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

241 for key, value in mapping: 

242 self[key] = value 

243 

244 def copy(self) -> Self: 

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

246 

247 Returns: 

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

249 """ 

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

251 

252 def __repr__(self) -> str: 

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

254 

255 Returns: 

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

257 """ 

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

259 

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

261 """Check equality with another dictionary. 

262 

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

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

265 ``dict`` also works. 

266 

267 Parameters: 

268 other: The object to compare. 

269 

270 Returns: 

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

272 """ 

273 if not isinstance(other, dict): 

274 return NotImplemented 

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

276 

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

278 """Check inequality with another dictionary. 

279 

280 Parameters: 

281 other: The object to compare. 

282 

283 Returns: 

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

285 """ 

286 return not self == other 

287 

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

289 # must be uppercase. 

290 canonical_order = None 

291 

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

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

294 

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

296 Remaining keys appear alphabetically at the end. 

297 

298 Returns: 

299 A sorted list of keys. 

300 """ 

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

302 

303 def sorted_items(self) -> list[tuple[Any, Any]]: 

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

305 

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

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

308 

309 Returns: 

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

311 """ 

312 return canonsort_items(self, self.canonical_order) 

313 

314 

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