|
22 | 22 | '_typeshed.SupportsKeysAndGetItem[KT, VT]', |
23 | 23 | ] |
24 | 24 |
|
| 25 | +OnDuplicate = types.Literal['raise', 'ignore'] |
| 26 | + |
25 | 27 |
|
26 | 28 | class CastedDictBase(types.Dict[KT, VT], abc.ABC): |
27 | 29 | _key_cast: KT_cast |
@@ -173,6 +175,118 @@ def values(self) -> Generator[VT, None, None]: # type: ignore |
173 | 175 | yield self._value_cast(value) |
174 | 176 |
|
175 | 177 |
|
| 178 | +class UniqueList(types.List[VT]): |
| 179 | + ''' |
| 180 | + A list that only allows unique values. Duplicate values are ignored by |
| 181 | + default, but can be configured to raise an exception instead. |
| 182 | +
|
| 183 | + >>> l = UniqueList(1, 2, 3) |
| 184 | + >>> l.append(4) |
| 185 | + >>> l.append(4) |
| 186 | + >>> l.insert(0, 4) |
| 187 | + >>> l.insert(0, 5) |
| 188 | + >>> l[1] = 10 |
| 189 | + >>> l |
| 190 | + [5, 10, 2, 3, 4] |
| 191 | +
|
| 192 | + >>> l = UniqueList(1, 2, 3, on_duplicate='raise') |
| 193 | + >>> l.append(4) |
| 194 | + >>> l.append(4) |
| 195 | + Traceback (most recent call last): |
| 196 | + ... |
| 197 | + ValueError: Duplicate value: 4 |
| 198 | + >>> l.insert(0, 4) |
| 199 | + Traceback (most recent call last): |
| 200 | + ... |
| 201 | + ValueError: Duplicate value: 4 |
| 202 | + >>> 4 in l |
| 203 | + True |
| 204 | + >>> l[0] |
| 205 | + 1 |
| 206 | + >>> l[1] = 4 |
| 207 | + Traceback (most recent call last): |
| 208 | + ... |
| 209 | + ValueError: Duplicate value: 4 |
| 210 | + ''' |
| 211 | + _set: set[VT] |
| 212 | + |
| 213 | + def __init__(self, *args: VT, on_duplicate: OnDuplicate = 'ignore'): |
| 214 | + self.on_duplicate = on_duplicate |
| 215 | + self._set = set() |
| 216 | + super().__init__() |
| 217 | + for arg in args: |
| 218 | + self.append(arg) |
| 219 | + |
| 220 | + def insert(self, index: types.SupportsIndex, value: VT) -> None: |
| 221 | + if value in self._set: |
| 222 | + if self.on_duplicate == 'raise': |
| 223 | + raise ValueError('Duplicate value: %s' % value) |
| 224 | + else: |
| 225 | + return |
| 226 | + |
| 227 | + self._set.add(value) |
| 228 | + super().insert(index, value) |
| 229 | + |
| 230 | + def append(self, value: VT) -> None: |
| 231 | + if value in self._set: |
| 232 | + if self.on_duplicate == 'raise': |
| 233 | + raise ValueError('Duplicate value: %s' % value) |
| 234 | + else: |
| 235 | + return |
| 236 | + |
| 237 | + self._set.add(value) |
| 238 | + super().append(value) |
| 239 | + |
| 240 | + def __contains__(self, item): |
| 241 | + return item in self._set |
| 242 | + |
| 243 | + @types.overload |
| 244 | + @abc.abstractmethod |
| 245 | + def __setitem__(self, index: types.SupportsIndex, value: VT) -> None: |
| 246 | + ... |
| 247 | + |
| 248 | + @types.overload |
| 249 | + @abc.abstractmethod |
| 250 | + def __setitem__(self, index: slice, value: types.Iterable[VT]) -> None: |
| 251 | + ... |
| 252 | + |
| 253 | + def __setitem__(self, indices, values) -> None: |
| 254 | + if isinstance(indices, slice): |
| 255 | + if self.on_duplicate == 'ignore': |
| 256 | + raise RuntimeError( |
| 257 | + 'ignore mode while setting slices introduces ambiguous ' |
| 258 | + 'behaviour and is therefore not supported' |
| 259 | + ) |
| 260 | + |
| 261 | + duplicates = set(values) & self._set |
| 262 | + if duplicates and values != self[indices]: |
| 263 | + raise ValueError('Duplicate values: %s' % duplicates) |
| 264 | + |
| 265 | + self._set.update(values) |
| 266 | + super().__setitem__(indices, values) |
| 267 | + else: |
| 268 | + if values in self._set and values != self[indices]: |
| 269 | + if self.on_duplicate == 'raise': |
| 270 | + raise ValueError('Duplicate value: %s' % values) |
| 271 | + else: |
| 272 | + return |
| 273 | + |
| 274 | + self._set.add(values) |
| 275 | + super().__setitem__(indices, values) |
| 276 | + |
| 277 | + def __delitem__( |
| 278 | + self, |
| 279 | + index: types.Union[types.SupportsIndex, slice] |
| 280 | + ) -> None: |
| 281 | + if isinstance(index, slice): |
| 282 | + for value in self[index]: |
| 283 | + self._set.remove(value) |
| 284 | + else: |
| 285 | + self._set.remove(self[index]) |
| 286 | + |
| 287 | + super().__delitem__(index) |
| 288 | + |
| 289 | + |
176 | 290 | if __name__ == '__main__': |
177 | 291 | import doctest |
178 | 292 |
|
|
0 commit comments