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"]