from __future__ import annotations
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, TypeVar
from icalendar.parser_tools import to_unicode
if TYPE_CHECKING:
from collections.abc import Iterable, Mapping
try:
from typing import Self
except ImportError:
from typing_extensions import Self
KT = TypeVar("KT")
VT = TypeVar("VT")
[docs]
def canonsort_keys(
keys: Iterable[KT], canonical_order: Iterable[KT] | None = None
) -> list[KT]:
"""Sort leading keys according to a canonical order.
Keys specified in ``canonical_order`` appear first in that order.
Remaining keys appear alphabetically at the end.
Parameters:
keys: The keys to sort.
canonical_order: The preferred order for leading keys.
Keys not in this sequence are sorted alphabetically after
the canonical ones. If ``None``, all keys are sorted
alphabetically.
Returns:
A new list of keys sorted by canonical order first, then alphabetically.
Example:
.. code-block:: pycon
>>> from icalendar.caselessdict import canonsort_keys
>>> canonsort_keys(["C", "A", "B"], ["B", "C"])
['B', 'C', 'A']
"""
canonical_map = {k: i for i, k in enumerate(canonical_order or [])}
head = [k for k in keys if k in canonical_map]
tail = [k for k in keys if k not in canonical_map]
return sorted(head, key=lambda k: canonical_map[k]) + sorted(tail)
[docs]
def canonsort_items(
dict1: Mapping[KT, VT], canonical_order: Iterable[KT] | None = None
) -> list[tuple[KT, VT]]:
"""Sort items from a mapping according to a canonical key order.
Parameters:
dict1: The mapping whose items to sort.
canonical_order: The preferred order for leading keys.
If ``None``, all keys are sorted alphabetically.
Returns:
A list of ``(key, value)`` tuples sorted by canonical order.
Example:
.. code-block:: pycon
>>> from icalendar.caselessdict import canonsort_items
>>> canonsort_items({"C": 3, "A": 1, "B": 2}, ["B", "C"])
[('B', 2), ('C', 3), ('A', 1)]
"""
return [(k, dict1[k]) for k in canonsort_keys(dict1.keys(), canonical_order)]
[docs]
class CaselessDict(OrderedDict):
"""A case-insensitive dictionary that uses strings as keys.
All keys are stored in uppercase internally, but values retain
their original case. Keys can be provided as ``str`` or ``bytes``.
They are converted to Unicode via :func:`~icalendar.parser_tools.to_unicode`,
then uppercased before storage.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Parameters:
*args: Positional arguments passed to :class:`~collections.OrderedDict`.
**kwargs: Keyword arguments passed to :class:`~collections.OrderedDict`.
Example:
Create a new ``CaselessDict`` and normalize existing keys to uppercase.
.. code-block:: pycon
>>> from icalendar.caselessdict import CaselessDict
>>> d = CaselessDict(summary="Meeting")
>>> d["SUMMARY"]
'Meeting'
>>> "summary" in d
True
"""
super().__init__(*args, **kwargs)
for key, value in self.items():
key_upper = to_unicode(key).upper()
if key != key_upper:
super().__delitem__(key)
self[key_upper] = value
__hash__ = None
def __getitem__(self, key: Any) -> Any:
"""Get the item from the ``CaselessDict`` instance by
``key``, case-insensitively.
Parameters:
key: The key to look up, case-insensitively.
Returns:
The (key, value) pair associated with the uppercased key.
Raises:
KeyError: If the key is not found.
"""
key = to_unicode(key)
return super().__getitem__(key.upper())
def __setitem__(self, key: Any, value: Any) -> None:
"""Set a (key, value) pair, storing the key in uppercase.
Parameters:
key: The key of the pair, case-insensitive.
value: The value to associate with the key.
"""
key = to_unicode(key)
super().__setitem__(key.upper(), value)
def __delitem__(self, key: Any) -> None:
"""Delete a (key, value) pair by its case-insensitive key.
Parameters:
key: The key to delete, case-insensitively.
Raises:
KeyError: If the key is not found.
"""
key = to_unicode(key)
super().__delitem__(key.upper())
def __contains__(self, key: Any) -> bool:
"""Check whether a key exists in the mapping, case-insensitively.
Parameters:
key: The key to check case-insensitively.
Returns:
``True`` if the uppercased key exists, else ``False``.
"""
key = to_unicode(key)
return super().__contains__(key.upper())
[docs]
def get(self, key: Any, default: Any = None) -> Any:
"""Return the ``key``, optionally with a ``default`` value.
Parameters:
key: The key to look up, case-insensitively.
default: The value to return if the key is not found.
Returns:
The value for the key, if present, else the value specified by ``default``.
"""
key = to_unicode(key)
return super().get(key.upper(), default)
[docs]
def setdefault(self, key: Any, value: Any = None) -> Any:
"""Create the (key, value) pair, optionally with a ``value``.
Once set, to change default value use :meth:`update`.
Parameters:
key: The key to look up or create, case-insensitively.
value: The default value to set, if given, else ``None``.
Returns:
The value for the key.
"""
key = to_unicode(key)
return super().setdefault(key.upper(), value)
[docs]
def pop(self, key: Any, default: Any = None) -> Any:
"""Remove and return the value for ``key``, or ``default`` if not found.
Parameters:
key: The key to remove, case-insensitively.
default: The value to return if the key is not found.
Returns:
The removed value, or the value of ``default``.
"""
key = to_unicode(key)
return super().pop(key.upper(), default)
[docs]
def popitem(self) -> tuple[Any, Any]:
"""Remove and return the last inserted (key, value) pair.
Returns:
A (key, value) tuple.
Raises:
KeyError: If the dictionary is empty.
"""
return super().popitem()
[docs]
def has_key(self, key: Any) -> bool:
"""Check whether a key exists, case-insensitively.
This is a legacy method. Use ``key in dict`` instead.
Parameters:
key: The key to check, case-insensitively.
Returns:
``True`` if the key exists, else ``False``.
"""
key = to_unicode(key)
return super().__contains__(key.upper())
[docs]
def update(self, *args: Any, **kwargs: Any) -> None:
"""Update the dictionary with (key, value) pairs, normalizing keys to uppercase.
Multiple keys that differ only in case will overwrite each other.
Only the last value is retained.
Parameters:
*args: Mappings or iterables of (key, value) pairs.
**kwargs: Additional (key, value) pairs.
"""
# Multiple keys where key1.upper() == key2.upper() will be lost.
mappings = list(args) + [kwargs]
for mapping in mappings:
if hasattr(mapping, "items"):
mapping = iter(mapping.items()) # noqa: PLW2901
for key, value in mapping:
self[key] = value
[docs]
def copy(self) -> Self:
"""Return a shallow copy of the dictionary.
Returns:
A new instance of the same type with the same contents.
"""
return type(self)(super().copy())
def __repr__(self) -> str:
"""Return a string representation of the dictionary.
Returns:
A string in the form ``CaselessDict({...})``.
"""
return f"{type(self).__name__}({dict(self)})"
def __eq__(self, other: object) -> bool:
"""Check equality with another dictionary.
Two ``CaselessDict`` instances are equal if they contain the same
(key, value) pairs after uppercasing keys. Comparison with a regular
``dict`` also works.
Parameters:
other: The object to compare.
Returns:
``True`` if equal, ``NotImplemented`` if ``other`` is not a ``dict``.
"""
if not isinstance(other, dict):
return NotImplemented
return self is other or dict(self.items()) == dict(other.items())
def __ne__(self, other: object) -> bool:
"""Check inequality with another dictionary.
Parameters:
other: The object to compare.
Returns:
``True`` if not equal, else ``False``.
"""
return not self == other
# A list of keys that must appear first in sorted_keys and sorted_items;
# must be uppercase.
canonical_order = None
[docs]
def sorted_keys(self) -> list[str]:
"""Sort keys according to the canonical order for this class.
Keys listed in :attr:`canonical_order` appear first in that order.
Remaining keys appear alphabetically at the end.
Returns:
A sorted list of keys.
"""
return canonsort_keys(self.keys(), self.canonical_order)
[docs]
def sorted_items(self) -> list[tuple[Any, Any]]:
"""Sort items according to the canonical order for this class.
Items whose keys are listed in :attr:`canonical_order` appear first
in that order. Remaining items appear alphabetically by key.
Returns:
A sorted list of (key, value) tuples.
"""
return canonsort_items(self, self.canonical_order)
__all__ = ["CaselessDict", "canonsort_items", "canonsort_keys"]