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