Skip to content

Commit

Permalink
Merge branch 'release/3.5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
wolph committed Feb 8, 2023
2 parents 7fec514 + d71b9b7 commit 5ed3147
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ exclude_lines =
if 0:
if __name__ == .__main__.:
if typing.TYPE_CHECKING:
if types.TYPE_CHECKING:
@overload
@types.overload
@typing.overload
41 changes: 41 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: "CodeQL"

on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
schedule:
- cron: "46 1 * * 3"

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
language: [ python ]

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality

- name: Autobuild
uses: github/codeql-action/autobuild@v2

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"
29 changes: 29 additions & 0 deletions _python_utils_tests/test_containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from python_utils import containers


def test_unique_list_ignore():
a = containers.UniqueList()
a.append(1)
a.append(1)
assert a == [1]

a = containers.UniqueList(*range(20))
with pytest.raises(RuntimeError):
a[10:20:2] = [1, 2, 3, 4, 5]

a[3] = 5


def test_unique_list_raise():
a = containers.UniqueList(*range(20), on_duplicate='raise')
with pytest.raises(ValueError):
a[10:20:2] = [1, 2, 3, 4, 5]

a[10:20:2] = [21, 22, 23, 24, 25]
with pytest.raises(ValueError):
a[3] = 5

del a[10]
del a[5:15]
2 changes: 1 addition & 1 deletion python_utils/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
)
__url__: str = 'https://github.com/WoLpH/python-utils'
# Omit type info due to automatic versioning script
__version__ = '3.4.5'
__version__ = '3.5.0'
114 changes: 114 additions & 0 deletions python_utils/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,120 @@ def values(self) -> Generator[VT, None, None]: # type: ignore
yield self._value_cast(value)


class UniqueList(types.List[VT]):
'''
A list that only allows unique values. Duplicate values are ignored by
default, but can be configured to raise an exception instead.
>>> l = UniqueList(1, 2, 3)
>>> l.append(4)
>>> l.append(4)
>>> l.insert(0, 4)
>>> l.insert(0, 5)
>>> l[1] = 10
>>> l
[5, 10, 2, 3, 4]
>>> l = UniqueList(1, 2, 3, on_duplicate='raise')
>>> l.append(4)
>>> l.append(4)
Traceback (most recent call last):
...
ValueError: Duplicate value: 4
>>> l.insert(0, 4)
Traceback (most recent call last):
...
ValueError: Duplicate value: 4
>>> 4 in l
True
>>> l[0]
1
>>> l[1] = 4
Traceback (most recent call last):
...
ValueError: Duplicate value: 4
'''

_set: set[VT]

def __init__(
self,
*args: VT,
on_duplicate: types.Literal['raise', 'ignore'] = 'ignore',
):
self.on_duplicate = on_duplicate
self._set = set()
super().__init__()
for arg in args:
self.append(arg)

def insert(self, index: types.SupportsIndex, value: VT) -> None:
if value in self._set:
if self.on_duplicate == 'raise':
raise ValueError('Duplicate value: %s' % value)
else:
return

self._set.add(value)
super().insert(index, value)

def append(self, value: VT) -> None:
if value in self._set:
if self.on_duplicate == 'raise':
raise ValueError('Duplicate value: %s' % value)
else:
return

self._set.add(value)
super().append(value)

def __contains__(self, item):
return item in self._set

@types.overload
def __setitem__(self, indices: types.SupportsIndex, values: VT) -> None:
...

@types.overload
def __setitem__(self, indices: slice, values: types.Iterable[VT]) -> None:
...

def __setitem__(self, indices, values) -> None:
if isinstance(indices, slice):
if self.on_duplicate == 'ignore':
raise RuntimeError(
'ignore mode while setting slices introduces ambiguous '
'behaviour and is therefore not supported'
)

duplicates = set(values) & self._set
if duplicates and values != self[indices]:
raise ValueError('Duplicate values: %s' % duplicates)

self._set.update(values)
super().__setitem__(indices, values)
else:
if values in self._set and values != self[indices]:
if self.on_duplicate == 'raise':
raise ValueError('Duplicate value: %s' % values)
else:
return

self._set.add(values)
super().__setitem__(indices, values)

def __delitem__(
self, index: types.Union[types.SupportsIndex, slice]
) -> None:
if isinstance(index, slice):
for value in self[index]:
self._set.remove(value)
else:
self._set.remove(self[index])

super().__delitem__(index)


if __name__ == '__main__':
import doctest

Expand Down
11 changes: 10 additions & 1 deletion python_utils/types.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import datetime
import decimal
import sys
from typing import * # type: ignore # pragma: no cover

# import * does not import Pattern
from typing import Pattern

if sys.version_info >= (3, 8): # pragma: no cover
from typing import Literal, SupportsIndex
else: # pragma: no cover
from typing_extensions import Literal, SupportsIndex

# Quickhand for optional because it gets so much use. If only Python had
# support for an optional type shorthand such as `SomeType?` instead of
# `Optional[SomeType]`.
Expand Down Expand Up @@ -33,7 +39,9 @@
None,
]

assert Pattern
assert Pattern is not None # type: ignore
assert Literal is not None
assert SupportsIndex is not None

__all__ = [
'OptionalScope',
Expand All @@ -52,6 +60,7 @@
'ForwardRef',
'Generic',
'Literal',
'SupportsIndex',
'Optional',
'ParamSpec',
'Protocol',
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ sign = 1
[flake8]
per-file-ignores =
python_utils/types.py: F403,F405
ignore = W391, W503, E741, E203
ignore = W391, W503, E741, E203, F811
exclude =
docs

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
),
package_data={'python_utils': ['py.typed']},
long_description=long_description,
install_requires=['typing_extensions;python_version<"3.8"'],
tests_require=['pytest'],
extras_require={
'loguru': [
Expand Down

0 comments on commit 5ed3147

Please sign in to comment.