1"""A list-like view for :rfc:`5545#section-3.8` component property values."""
2
3from __future__ import annotations
4
5import contextlib
6from collections.abc import MutableSequence, Sequence
7from typing import TYPE_CHECKING, Any
8
9if TYPE_CHECKING:
10 from collections.abc import Iterator
11
12 from icalendar.cal.component import Component
13
14
15class PropertyListView(MutableSequence):
16 """A list-like view of an :rfc:`5545#section-3.8` property within a component.
17
18 This view synchronizes modifications, such as ``append``, ``extend``, ``insert``,
19 ``clear``, and ``delete``, with the underlying component dictionary.
20 """
21
22 __hash__ = None
23
24 def __init__(self, component: Component, name: str) -> None:
25 """Initialize the property list view for a given component and property name.
26
27 Parameters:
28 component: The component to view.
29 name: The name of the property to view.
30 """
31 self._component = component
32 self._name = name
33
34 @contextlib.contextmanager
35 def _values(self) -> Iterator[list[Any]]:
36 """Synchronize the value list with the component."""
37 raw = self._component.get(self._name, [])
38 values = [raw] if not isinstance(raw, list) else list(raw)
39 try:
40 yield values
41 finally:
42 if not values:
43 self._component.pop(self._name, None)
44 else:
45 self._component[self._name] = values
46
47 def __getitem__(self, index: Any) -> Any:
48 """Get an item or slice from the property list."""
49 with self._values() as values:
50 return values[index]
51
52 def __setitem__(self, index: Any, value: Any) -> None:
53 """Set an item or slice in the property list."""
54 with self._values() as values:
55 values[index] = value
56
57 def __delitem__(self, index: Any) -> None:
58 """Delete an item or slice from the property list."""
59 with self._values() as values:
60 del values[index]
61
62 def __len__(self) -> int:
63 """Return the number of items in the property list."""
64 raw = self._component.get(self._name, [])
65 if not raw:
66 return 0
67 if not isinstance(raw, list):
68 return 1
69 return len(raw)
70
71 def insert(self, index: int, value: Any) -> None:
72 """Insert an item into the property list at a given index.
73
74 Parameters:
75 index: The position at which to insert the item into the property list.
76 value: The value to insert in the property list at the given index.
77 """
78 with self._values() as values:
79 values.insert(index, value)
80
81 def clear(self) -> None:
82 """Remove all values and the property from the component."""
83 self._component.pop(self._name, None)
84
85 def replace_with(self, values: Any) -> None:
86 """Replace all values in the property with ``values``.
87
88 The values are added with
89 :meth:`Component.add <icalendar.cal.component.Component.add>`,
90 so they are converted to the correct types.
91
92 Parameters:
93 values: The values to replace in the property list.
94 ``None`` removes all values from the property,
95 whereas a single string value or any sequence type
96 replaces all values in the property and is stored as a list.
97 """
98 if values is None:
99 new_values = []
100 elif isinstance(values, Sequence) and not isinstance(values, (str, bytes)):
101 # copy first: ``values`` could be this view or a view of this property
102 new_values = list(values)
103 else:
104 new_values = [values]
105 self.clear()
106 for value in new_values:
107 self._component.add(self._name, value)
108
109 def __eq__(self, other: object) -> bool:
110 """Check equality against another sequence."""
111 if isinstance(other, PropertyListView):
112 return list(self) == list(other)
113 if hasattr(other, "__iter__"):
114 return list(self) == list(other)
115 return False
116
117 def __repr__(self) -> str:
118 """Return the string representation of the property list view."""
119 return f"PropertyListView({list(self)!r})"
120
121 def __add__(self, other: Any) -> list[Any]:
122 """Concatenate this list with a sequence and return a new list."""
123 if isinstance(other, Sequence) and not isinstance(other, (str, bytes)):
124 return list(self) + list(other)
125 return NotImplemented
126
127 def __radd__(self, other: Any) -> list[Any]:
128 """Concatenate a sequence with this list and return a new list."""
129 if isinstance(other, Sequence) and not isinstance(other, (str, bytes)):
130 return list(other) + list(self)
131 return NotImplemented
132
133 def __mul__(self, n: int) -> list[Any]:
134 """Multiply the property list by an integer to return a new repeated list."""
135 if isinstance(n, int):
136 return list(self) * n
137 return NotImplemented
138
139 def __rmul__(self, n: int) -> list[Any]:
140 """Multiply the property list by an integer to return a new repeated list."""
141 if isinstance(n, int):
142 return list(self) * n
143 return NotImplemented
144
145 def copy(self) -> list[Any]:
146 """Return a shallow copy of the property list as a regular list."""
147 return list(self)
148
149
150__all__ = ["PropertyListView"]