Skip to content

Commit

Permalink
Merge branch 'release/3.6.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
wolph committed May 29, 2023
2 parents bfe4693 + e68d6e4 commit dbfcb6a
Show file tree
Hide file tree
Showing 23 changed files with 328 additions and 177 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
timeout-minutes: 4
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev']
python-version: ['3.8', '3.9', '3.10', '3.11']

steps:
- uses: actions/checkout@v3
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
/docs/_build
/cover
/.eggs
/.*
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ Links
- Documentation: https://python-utils.readthedocs.io/en/latest/
- My blog: https://wol.ph/

Security contact information
------------------------------------------------------------------------------

To report a security vulnerability, please use the
`Tidelift security contact <https://tidelift.com/security>`_.
Tidelift will coordinate the fix and disclosure.

Requirements for installing:
------------------------------------------------------------------------------

Expand Down
22 changes: 22 additions & 0 deletions _python_utils_tests/test_aio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from datetime import datetime
import pytest
import asyncio

from python_utils import types
from python_utils.aio import acount


@pytest.mark.asyncio
async def test_acount(monkeypatch: pytest.MonkeyPatch):
sleeps: types.List[float] = []

async def mock_sleep(delay: float):
sleeps.append(delay)

monkeypatch.setattr(asyncio, 'sleep', mock_sleep)

async for i in acount(delay=1, stop=3.5):
print('i', i, datetime.now())

assert len(sleeps) == 4
assert sum(sleeps) == 4
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from datetime import date
import os
import sys

sys.path.insert(0, os.path.abspath('..'))

from python_utils import __about__
Expand Down Expand Up @@ -63,4 +64,3 @@
# html_static_path = ['_static']

intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}

11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[tool.black]
line-length = 79
target-version = ['py37', 'py38', 'py39', 'py310', 'py311']
skip-string-normalization = true

[tool.pyright]
include = ['python_utils']
strict = ['python_utils', '_python_utils_tests/test_aio.py']
# The terminal file is very OS specific and dependent on imports so we're skipping it from type checking
ignore = ['python_utils/terminal.py']
pythonVersion = '3.8'
8 changes: 0 additions & 8 deletions pyrightconfig.json

This file was deleted.

2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ addopts =
--doctest-modules
--cov python_utils
--cov-report term-missing
--mypy
; --mypy

doctest_optionflags =
ALLOW_UNICODE
Expand Down
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.5.2'
__version__ = '3.6.0'
12 changes: 7 additions & 5 deletions python_utils/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@

from . import types

_N = types.TypeVar('_N', int, float)


async def acount(
start: float = 0,
step: float = 1,
start: _N = 0,
step: _N = 1,
delay: float = 0,
stop: types.Optional[float] = None,
) -> types.AsyncIterator[float]:
stop: types.Optional[_N] = None,
) -> types.AsyncIterator[_N]:
'''Asyncio version of itertools.count()'''
for item in itertools.count(start, step): # pragma: no branch
if stop is not None and item >= stop:
break

yield item
yield types.cast(_N, item)
await asyncio.sleep(delay)
99 changes: 73 additions & 26 deletions python_utils/containers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# pyright: reportIncompatibleMethodOverride=false
import abc
import typing
from typing import Any, Generator
import collections

from . import types

Expand All @@ -14,44 +15,51 @@
#: A type alias for a dictionary with keys of type KT and values of type VT.
DT = types.Dict[KT, VT]
#: A type alias for the casted type of a dictionary key.
KT_cast = types.Optional[types.Callable[[Any], KT]]
KT_cast = types.Optional[types.Callable[..., KT]]
#: A type alias for the casted type of a dictionary value.
VT_cast = types.Optional[types.Callable[[Any], VT]]
VT_cast = types.Optional[types.Callable[..., VT]]
#: A type alias for the hashable values of the `UniqueList`
HT = types.TypeVar('HT', bound=types.Hashable)
#: A type alias for a regular generic type
T = types.TypeVar('T')

# Using types.Union instead of | since Python 3.7 doesn't fully support it
DictUpdateArgs = types.Union[
types.Mapping,
types.Iterable[types.Union[types.Tuple[Any, Any], types.Mapping]],
types.Mapping[KT, VT],
types.Iterable[types.Tuple[KT, VT]],
types.Iterable[types.Mapping[KT, VT]],
'_typeshed.SupportsKeysAndGetItem[KT, VT]',
]

OnDuplicate = types.Literal['ignore', 'raise']


class CastedDictBase(types.Dict[KT, VT], abc.ABC):
_key_cast: KT_cast
_value_cast: VT_cast
_key_cast: KT_cast[KT]
_value_cast: VT_cast[VT]

def __init__(
self,
key_cast: KT_cast = None,
value_cast: VT_cast = None,
*args: DictUpdateArgs,
key_cast: KT_cast[KT] = None,
value_cast: VT_cast[VT] = None,
*args: DictUpdateArgs[KT, VT],
**kwargs: VT,
) -> None:
self._value_cast = value_cast
self._key_cast = key_cast
self.update(*args, **kwargs)

def update(self, *args: DictUpdateArgs, **kwargs: VT) -> None:
def update(
self, *args: DictUpdateArgs[types.Any, types.Any], **kwargs: types.Any
) -> None:
if args:
kwargs.update(*args)

if kwargs:
for key, value in kwargs.items():
self[key] = value

def __setitem__(self, key: Any, value: Any) -> None:
def __setitem__(self, key: types.Any, value: types.Any) -> None:
if self._key_cast is not None:
key = self._key_cast(key)

Expand Down Expand Up @@ -93,7 +101,7 @@ class CastedDict(CastedDictBase[KT, VT]):
{1: 2, '3': '4', '5': '6', '7': '8'}
'''

def __setitem__(self, key: Any, value: Any) -> None:
def __setitem__(self, key: typing.Any, value: typing.Any) -> None:
if self._value_cast is not None:
value = self._value_cast(value)

Expand Down Expand Up @@ -146,13 +154,13 @@ class LazyCastedDict(CastedDictBase[KT, VT]):
'4'
'''

def __setitem__(self, key: Any, value: Any) -> None:
def __setitem__(self, key: types.Any, value: types.Any):
if self._key_cast is not None:
key = self._key_cast(key)

super().__setitem__(key, value)

def __getitem__(self, key: Any) -> VT:
def __getitem__(self, key: types.Any) -> VT:
if self._key_cast is not None:
key = self._key_cast(key)

Expand All @@ -165,14 +173,14 @@ def __getitem__(self, key: Any) -> VT:

def items( # type: ignore
self,
) -> Generator[types.Tuple[KT, VT], None, None]:
) -> types.Generator[types.Tuple[KT, VT], None, None]:
if self._value_cast is None:
yield from super().items()
else:
for key, value in super().items():
yield key, self._value_cast(value)

def values(self) -> Generator[VT, None, None]: # type: ignore
def values(self) -> types.Generator[VT, None, None]: # type: ignore
if self._value_cast is None:
yield from super().values()
else:
Expand Down Expand Up @@ -219,7 +227,7 @@ class UniqueList(types.List[HT]):
def __init__(
self,
*args: HT,
on_duplicate: types.Literal['raise', 'ignore'] = 'ignore',
on_duplicate: OnDuplicate = 'ignore',
):
self.on_duplicate = on_duplicate
self._set = set()
Expand Down Expand Up @@ -247,7 +255,7 @@ def append(self, value: HT) -> None:
self._set.add(value)
super().append(value)

def __contains__(self, item):
def __contains__(self, item: HT) -> bool: # type: ignore
return item in self._set

@types.overload
Expand All @@ -258,29 +266,37 @@ def __setitem__(self, indices: types.SupportsIndex, values: HT) -> None:
def __setitem__(self, indices: slice, values: types.Iterable[HT]) -> None:
...

def __setitem__(self, indices, values) -> None:
def __setitem__(
self,
indices: types.Union[slice, types.SupportsIndex],
values: types.Union[types.Iterable[HT], HT],
) -> None:
if isinstance(indices, slice):
values = types.cast(types.Iterable[HT], values)
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)
duplicates: types.Set[HT] = set(values) & self._set
if duplicates and values != list(self[indices]):
raise ValueError(f'Duplicate values: {duplicates}')

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

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

super().__setitem__(
types.cast(slice, indices), types.cast(types.List[HT], values)
)

def __delitem__(
self, index: types.Union[types.SupportsIndex, slice]
Expand All @@ -294,6 +310,37 @@ def __delitem__(
super().__delitem__(index)


class SlicableDeque(types.Generic[T], collections.deque): # type: ignore
@types.overload
def __getitem__(self, index: types.SupportsIndex) -> T:
...

@types.overload
def __getitem__(self, index: slice) -> 'SlicableDeque[T]':
...

def __getitem__(
self, index: types.Union[types.SupportsIndex, slice]
) -> types.Union[T, 'SlicableDeque[T]']:
'''
Return the item or slice at the given index.
>>> d = SlicableDeque[int]([1, 2, 3, 4, 5])
>>> d[1:4]
SlicableDeque([2, 3, 4])
>>> d = SlicableDeque[str](['a', 'b', 'c'])
>>> d[-2:]
SlicableDeque(['b', 'c'])
'''
if isinstance(index, slice):
start, stop, step = index.indices(len(self))
return self.__class__(self[i] for i in range(start, stop, step))
else:
return types.cast(T, super().__getitem__(index))


if __name__ == '__main__':
import doctest

Expand Down
Loading

0 comments on commit dbfcb6a

Please sign in to comment.