Skip to content

Commit 3c722f3

Browse files
committed
Added unique list container
1 parent ed71d9e commit 3c722f3

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ exclude_lines =
2222
if 0:
2323
if __name__ == .__main__.:
2424
if typing.TYPE_CHECKING:
25+
if types.TYPE_CHECKING:
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
3+
from python_utils import containers
4+
5+
6+
def test_unique_list_ignore():
7+
a = containers.UniqueList()
8+
a.append(1)
9+
a.append(1)
10+
assert a == [1]
11+
12+
a = containers.UniqueList(*range(20))
13+
with pytest.raises(RuntimeError):
14+
a[10:20:2] = [1, 2, 3, 4, 5]
15+
16+
a[3] = 5
17+
18+
19+
def test_unique_list_raise():
20+
a = containers.UniqueList(*range(20), on_duplicate='raise')
21+
with pytest.raises(ValueError):
22+
a[10:20:2] = [1, 2, 3, 4, 5]
23+
24+
a[10:20:2] = [21, 22, 23, 24, 25]
25+
with pytest.raises(ValueError):
26+
a[3] = 5
27+
28+
del a[10]
29+
del a[5:15]

python_utils/containers.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
'_typeshed.SupportsKeysAndGetItem[KT, VT]',
2323
]
2424

25+
OnDuplicate = types.Literal['raise', 'ignore']
26+
2527

2628
class CastedDictBase(types.Dict[KT, VT], abc.ABC):
2729
_key_cast: KT_cast
@@ -173,6 +175,118 @@ def values(self) -> Generator[VT, None, None]: # type: ignore
173175
yield self._value_cast(value)
174176

175177

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+
176290
if __name__ == '__main__':
177291
import doctest
178292

0 commit comments

Comments
 (0)