Skip to content

Commit 5ed3147

Browse files
committed
Merge branch 'release/3.5.0'
2 parents 7fec514 + d71b9b7 commit 5ed3147

File tree

8 files changed

+201
-3
lines changed

8 files changed

+201
-3
lines changed

.coveragerc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ exclude_lines =
2222
if 0:
2323
if __name__ == .__main__.:
2424
if typing.TYPE_CHECKING:
25+
if types.TYPE_CHECKING:
26+
@overload
27+
@types.overload
28+
@typing.overload

.github/workflows/codeql.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: "CodeQL"
2+
3+
on:
4+
push:
5+
branches: [ "develop" ]
6+
pull_request:
7+
branches: [ "develop" ]
8+
schedule:
9+
- cron: "46 1 * * 3"
10+
11+
jobs:
12+
analyze:
13+
name: Analyze
14+
runs-on: ubuntu-latest
15+
permissions:
16+
actions: read
17+
contents: read
18+
security-events: write
19+
20+
strategy:
21+
fail-fast: false
22+
matrix:
23+
language: [ python ]
24+
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v3
28+
29+
- name: Initialize CodeQL
30+
uses: github/codeql-action/init@v2
31+
with:
32+
languages: ${{ matrix.language }}
33+
queries: +security-and-quality
34+
35+
- name: Autobuild
36+
uses: github/codeql-action/autobuild@v2
37+
38+
- name: Perform CodeQL Analysis
39+
uses: github/codeql-action/analyze@v2
40+
with:
41+
category: "/language:${{ matrix.language }}"
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/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
)
88
__url__: str = 'https://github.com/WoLpH/python-utils'
99
# Omit type info due to automatic versioning script
10-
__version__ = '3.4.5'
10+
__version__ = '3.5.0'

python_utils/containers.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,120 @@ def values(self) -> Generator[VT, None, None]: # type: ignore
173173
yield self._value_cast(value)
174174

175175

176+
class UniqueList(types.List[VT]):
177+
'''
178+
A list that only allows unique values. Duplicate values are ignored by
179+
default, but can be configured to raise an exception instead.
180+
181+
>>> l = UniqueList(1, 2, 3)
182+
>>> l.append(4)
183+
>>> l.append(4)
184+
>>> l.insert(0, 4)
185+
>>> l.insert(0, 5)
186+
>>> l[1] = 10
187+
>>> l
188+
[5, 10, 2, 3, 4]
189+
190+
>>> l = UniqueList(1, 2, 3, on_duplicate='raise')
191+
>>> l.append(4)
192+
>>> l.append(4)
193+
Traceback (most recent call last):
194+
...
195+
ValueError: Duplicate value: 4
196+
>>> l.insert(0, 4)
197+
Traceback (most recent call last):
198+
...
199+
ValueError: Duplicate value: 4
200+
>>> 4 in l
201+
True
202+
>>> l[0]
203+
1
204+
>>> l[1] = 4
205+
Traceback (most recent call last):
206+
...
207+
ValueError: Duplicate value: 4
208+
'''
209+
210+
_set: set[VT]
211+
212+
def __init__(
213+
self,
214+
*args: VT,
215+
on_duplicate: types.Literal['raise', 'ignore'] = 'ignore',
216+
):
217+
self.on_duplicate = on_duplicate
218+
self._set = set()
219+
super().__init__()
220+
for arg in args:
221+
self.append(arg)
222+
223+
def insert(self, index: types.SupportsIndex, value: VT) -> None:
224+
if value in self._set:
225+
if self.on_duplicate == 'raise':
226+
raise ValueError('Duplicate value: %s' % value)
227+
else:
228+
return
229+
230+
self._set.add(value)
231+
super().insert(index, value)
232+
233+
def append(self, value: VT) -> None:
234+
if value in self._set:
235+
if self.on_duplicate == 'raise':
236+
raise ValueError('Duplicate value: %s' % value)
237+
else:
238+
return
239+
240+
self._set.add(value)
241+
super().append(value)
242+
243+
def __contains__(self, item):
244+
return item in self._set
245+
246+
@types.overload
247+
def __setitem__(self, indices: types.SupportsIndex, values: VT) -> None:
248+
...
249+
250+
@types.overload
251+
def __setitem__(self, indices: slice, values: types.Iterable[VT]) -> None:
252+
...
253+
254+
def __setitem__(self, indices, values) -> None:
255+
if isinstance(indices, slice):
256+
if self.on_duplicate == 'ignore':
257+
raise RuntimeError(
258+
'ignore mode while setting slices introduces ambiguous '
259+
'behaviour and is therefore not supported'
260+
)
261+
262+
duplicates = set(values) & self._set
263+
if duplicates and values != self[indices]:
264+
raise ValueError('Duplicate values: %s' % duplicates)
265+
266+
self._set.update(values)
267+
super().__setitem__(indices, values)
268+
else:
269+
if values in self._set and values != self[indices]:
270+
if self.on_duplicate == 'raise':
271+
raise ValueError('Duplicate value: %s' % values)
272+
else:
273+
return
274+
275+
self._set.add(values)
276+
super().__setitem__(indices, values)
277+
278+
def __delitem__(
279+
self, 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

python_utils/types.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import datetime
22
import decimal
3+
import sys
34
from typing import * # type: ignore # pragma: no cover
45

56
# import * does not import Pattern
67
from typing import Pattern
78

9+
if sys.version_info >= (3, 8): # pragma: no cover
10+
from typing import Literal, SupportsIndex
11+
else: # pragma: no cover
12+
from typing_extensions import Literal, SupportsIndex
13+
814
# Quickhand for optional because it gets so much use. If only Python had
915
# support for an optional type shorthand such as `SomeType?` instead of
1016
# `Optional[SomeType]`.
@@ -33,7 +39,9 @@
3339
None,
3440
]
3541

36-
assert Pattern
42+
assert Pattern is not None # type: ignore
43+
assert Literal is not None
44+
assert SupportsIndex is not None
3745

3846
__all__ = [
3947
'OptionalScope',
@@ -52,6 +60,7 @@
5260
'ForwardRef',
5361
'Generic',
5462
'Literal',
63+
'SupportsIndex',
5564
'Optional',
5665
'ParamSpec',
5766
'Protocol',

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ sign = 1
3232
[flake8]
3333
per-file-ignores =
3434
python_utils/types.py: F403,F405
35-
ignore = W391, W503, E741, E203
35+
ignore = W391, W503, E741, E203, F811
3636
exclude =
3737
docs
3838

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
),
3030
package_data={'python_utils': ['py.typed']},
3131
long_description=long_description,
32+
install_requires=['typing_extensions;python_version<"3.8"'],
3233
tests_require=['pytest'],
3334
extras_require={
3435
'loguru': [

0 commit comments

Comments
 (0)