Skip to content

Commit

Permalink
Many code quality improvements, ruff compliance, pyright strict compl…
Browse files Browse the repository at this point in the history
…iance, dropped Python 3.8
  • Loading branch information
wolph committed Sep 20, 2024
1 parent f6d7e13 commit bbbc4e5
Show file tree
Hide file tree
Showing 25 changed files with 462 additions and 194 deletions.
29 changes: 24 additions & 5 deletions _python_utils_tests/test_aio.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from datetime import datetime
import pytest
import asyncio

import pytest

from python_utils import types
from python_utils.aio import acount, acontainer
from python_utils.aio import acontainer, acount, adict


@pytest.mark.asyncio
Expand All @@ -16,8 +15,8 @@ async def mock_sleep(delay: float):

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

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

assert len(sleeps) == 4
assert sum(sleeps) == 4
Expand All @@ -38,5 +37,25 @@ async def empty_gen():
assert await acontainer(async_gen()) == [1, 2, 3]
assert await acontainer(async_gen, set) == {1, 2, 3}
assert await acontainer(async_gen(), set) == {1, 2, 3}
assert await acontainer(async_gen, list) == [1, 2, 3]
assert await acontainer(async_gen(), list) == [1, 2, 3]
assert await acontainer(async_gen, tuple) == (1, 2, 3)
assert await acontainer(async_gen(), tuple) == (1, 2, 3)
assert await acontainer(empty_gen) == []
assert await acontainer(empty_gen()) == []
assert await acontainer(empty_gen, set) == set()
assert await acontainer(empty_gen(), set) == set()
assert await acontainer(empty_gen, list) == list()
assert await acontainer(empty_gen(), list) == list()
assert await acontainer(empty_gen, tuple) == tuple()
assert await acontainer(empty_gen(), tuple) == tuple()


@pytest.mark.asyncio
async def test_adict():
async def async_gen():
yield 1, 2
yield 3, 4
yield 5, 6

assert await adict(async_gen) == {1: 2, 3: 4, 5: 6}
2 changes: 1 addition & 1 deletion _python_utils_tests/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@ def test_batcher():
assert len(batch) == 3

for batch in python_utils.batcher(range(4), 3):
pass
assert batch is not None

assert len(batch) == 1
8 changes: 4 additions & 4 deletions _python_utils_tests/test_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ def relative_import(level: int):


def test_import_globals_without_inspection():
locals_ = {}
globals_ = {'__name__': __name__}
locals_: types.Dict[str, types.Any] = {}
globals_: types.Dict[str, types.Any] = {'__name__': __name__}
import_.import_global(
'python_utils.formatters', locals_=locals_, globals_=globals_
)
assert 'camel_to_underscore' in globals_


def test_import_globals_single_method():
locals_ = {}
globals_ = {'__name__': __name__}
locals_: types.Dict[str, types.Any] = {}
globals_: types.Dict[str, types.Any] = {'__name__': __name__}
import_.import_global(
'python_utils.formatters',
['camel_to_underscore'],
Expand Down
1 change: 0 additions & 1 deletion _python_utils_tests/test_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from python_utils.loguru import Logurud


loguru = pytest.importorskip('loguru')


Expand Down
26 changes: 19 additions & 7 deletions _python_utils_tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def test_timeout_generator(
iterable=iterable,
maximum_interval=maximum_interval,
):
pass
assert i is not None

assert i == result

Expand Down Expand Up @@ -123,10 +123,7 @@ async def generator():


@pytest.mark.asyncio
async def test_aio_generator_timeout_detector_decorator():
# Make pyright happy
i = None

async def test_aio_generator_timeout_detector_decorator_reraise():
# Test regular timeout with reraise
@python_utils.aio_generator_timeout_detector_decorator(timeout=0.05)
async def generator_timeout():
Expand All @@ -135,9 +132,15 @@ async def generator_timeout():
yield i

with pytest.raises(asyncio.TimeoutError):
async for i in generator_timeout():
async for _ in generator_timeout():
pass


@pytest.mark.asyncio
async def test_aio_generator_timeout_detector_decorator_clean_exit():
# Make pyright happy
i = None

# Test regular timeout with clean exit
@python_utils.aio_generator_timeout_detector_decorator(
timeout=0.05, on_timeout=None
Expand All @@ -152,6 +155,9 @@ async def generator_clean():

assert i == 4


@pytest.mark.asyncio
async def test_aio_generator_timeout_detector_decorator_reraise_total():
# Test total timeout with reraise
@python_utils.aio_generator_timeout_detector_decorator(total_timeout=0.1)
async def generator_reraise():
Expand All @@ -160,9 +166,15 @@ async def generator_reraise():
yield i

with pytest.raises(asyncio.TimeoutError):
async for i in generator_reraise():
async for _ in generator_reraise():
pass


@pytest.mark.asyncio
async def test_aio_generator_timeout_detector_decorator_clean_total():
# Make pyright happy
i = None

# Test total timeout with clean exit
@python_utils.aio_generator_timeout_detector_decorator(
total_timeout=0.1, on_timeout=None
Expand Down
22 changes: 11 additions & 11 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# Configuration file for the Sphinx documentation builder.
"""
Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
This file only contains a selection of the most common options. For a full
list see the documentation:
https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
-- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
If extensions (or modules to document with autodoc) are in another directory,
add these directories to sys.path here. If the directory is relative to the
documentation root, use os.path.abspath to make it absolute, like shown here.
#
from datetime import date
"""
import os
import sys
from datetime import date

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

Expand All @@ -27,7 +29,6 @@
# The full version, including alpha/beta/rc tags
release = __about__.__version__


# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
Expand All @@ -50,7 +51,6 @@
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']


# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages. See the documentation for
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ skip-string-normalization = true

[tool.pyright]
# include = ['python_utils']
include = ['python_utils', '_python_utils_tests']
strict = ['python_utils', '_python_utils_tests']
include = ['python_utils', '_python_utils_tests', 'setup.py']
strict = ['python_utils', '_python_utils_tests', 'setup.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'
pythonVersion = '3.9'

12 changes: 12 additions & 0 deletions python_utils/__about__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
"""
This module contains metadata about the `python-utils` package.
Attributes:
__package_name__ (str): The name of the package.
__author__ (str): The author of the package.
__author_email__ (str): The email of the author.
__description__ (str): A brief description of the package.
__url__ (str): The URL of the package's repository.
__version__ (str): The current version of the package.
"""

__package_name__: str = 'python-utils'
__author__: str = 'Rick van Hattem'
__author_email__: str = '[email protected]'
Expand Down
52 changes: 52 additions & 0 deletions python_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,55 @@
"""
This module initializes the `python_utils` package by importing various
submodules and functions.
Submodules:
aio
compat
converters
decorators
formatters
generators
import_
logger
terminal
time
types
Functions:
acount
remap
scale_1024
to_float
to_int
to_str
to_unicode
listify
set_attributes
raise_exception
reraise
camel_to_underscore
timesince
abatcher
batcher
import_global
get_terminal_size
aio_generator_timeout_detector
aio_generator_timeout_detector_decorator
aio_timeout_generator
delta_to_seconds
delta_to_seconds_or_none
format_time
timedelta_to_seconds
timeout_generator
Classes:
CastedDict
LazyCastedDict
UniqueList
Logged
LoggerBase
"""

from . import (
aio,
compat,
Expand Down
75 changes: 68 additions & 7 deletions python_utils/aio.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'''
Asyncio equivalents to regular Python functions.
"""Asyncio equivalents to regular Python functions."""

'''
import asyncio
import itertools
import typing

from . import types

_N = types.TypeVar('_N', int, float)
_T = types.TypeVar('_T')
_K = types.TypeVar('_K')
_V = types.TypeVar('_V')


async def acount(
Expand All @@ -17,7 +18,7 @@ async def acount(
delay: float = 0,
stop: types.Optional[_N] = None,
) -> types.AsyncIterator[_N]:
'''Asyncio version of itertools.count()'''
"""Asyncio version of itertools.count()."""
for item in itertools.count(start, step): # pragma: no branch
if stop is not None and item >= stop:
break
Expand All @@ -26,20 +27,50 @@ async def acount(
await asyncio.sleep(delay)


@typing.overload
async def acontainer(
iterable: types.Union[
types.AsyncIterable[_T],
types.Callable[..., types.AsyncIterable[_T]],
],
container: types.Type[types.Tuple[_T, ...]],
) -> types.Tuple[_T, ...]: ...


@typing.overload
async def acontainer(
iterable: types.Union[
types.AsyncIterable[_T],
types.Callable[..., types.AsyncIterable[_T]],
],
container: types.Type[types.List[_T]] = list,
) -> types.List[_T]: ...


@typing.overload
async def acontainer(
iterable: types.Union[
types.AsyncIterable[_T],
types.Callable[..., types.AsyncIterable[_T]],
],
container: types.Type[types.Set[_T]],
) -> types.Set[_T]: ...


async def acontainer(
iterable: types.Union[
types.AsyncIterable[_T],
types.Callable[..., types.AsyncIterable[_T]],
],
container: types.Callable[[types.Iterable[_T]], types.Iterable[_T]] = list,
) -> types.Iterable[_T]:
'''
Asyncio version of list()/set()/tuple()/etc() using an async for loop
"""
Asyncio version of list()/set()/tuple()/etc() using an async for loop.
So instead of doing `[item async for item in iterable]` you can do
`await acontainer(iterable)`.
'''
"""
iterable_: types.AsyncIterable[_T]
if callable(iterable):
iterable_ = iterable()
Expand All @@ -52,3 +83,33 @@ async def acontainer(
items.append(item)

return container(items)


async def adict(
iterable: types.Union[
types.AsyncIterable[types.Tuple[_K, _V]],
types.Callable[..., types.AsyncIterable[types.Tuple[_K, _V]]],
],
container: types.Callable[
[types.Iterable[types.Tuple[_K, _V]]], types.Mapping[_K, _V]
] = dict,
) -> types.Mapping[_K, _V]:
"""
Asyncio version of dict() using an async for loop.
So instead of doing `{key: value async for key, value in iterable}` you
can do `await adict(iterable)`.
"""
iterable_: types.AsyncIterable[types.Tuple[_K, _V]]
if callable(iterable):
iterable_ = iterable()
else:
iterable_ = iterable

item: types.Tuple[_K, _V]
items: types.List[types.Tuple[_K, _V]] = []
async for item in iterable_:
items.append(item)

return container(items)
Empty file removed python_utils/compat.py
Empty file.
Loading

0 comments on commit bbbc4e5

Please sign in to comment.