Skip to content

Commit bbbc4e5

Browse files
committed
Many code quality improvements, ruff compliance, pyright strict compliance, dropped Python 3.8
1 parent f6d7e13 commit bbbc4e5

25 files changed

Lines changed: 462 additions & 194 deletions

_python_utils_tests/test_aio.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
from datetime import datetime
2-
import pytest
31
import asyncio
42

3+
import pytest
54

65
from python_utils import types
7-
from python_utils.aio import acount, acontainer
6+
from python_utils.aio import acontainer, acount, adict
87

98

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

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

19-
async for i in acount(delay=1, stop=3.5):
20-
print('i', i, datetime.now())
18+
async for _i in acount(delay=1, stop=3.5):
19+
pass
2120

2221
assert len(sleeps) == 4
2322
assert sum(sleeps) == 4
@@ -38,5 +37,25 @@ async def empty_gen():
3837
assert await acontainer(async_gen()) == [1, 2, 3]
3938
assert await acontainer(async_gen, set) == {1, 2, 3}
4039
assert await acontainer(async_gen(), set) == {1, 2, 3}
40+
assert await acontainer(async_gen, list) == [1, 2, 3]
41+
assert await acontainer(async_gen(), list) == [1, 2, 3]
42+
assert await acontainer(async_gen, tuple) == (1, 2, 3)
43+
assert await acontainer(async_gen(), tuple) == (1, 2, 3)
4144
assert await acontainer(empty_gen) == []
4245
assert await acontainer(empty_gen()) == []
46+
assert await acontainer(empty_gen, set) == set()
47+
assert await acontainer(empty_gen(), set) == set()
48+
assert await acontainer(empty_gen, list) == list()
49+
assert await acontainer(empty_gen(), list) == list()
50+
assert await acontainer(empty_gen, tuple) == tuple()
51+
assert await acontainer(empty_gen(), tuple) == tuple()
52+
53+
54+
@pytest.mark.asyncio
55+
async def test_adict():
56+
async def async_gen():
57+
yield 1, 2
58+
yield 3, 4
59+
yield 5, 6
60+
61+
assert await adict(async_gen) == {1: 2, 3: 4, 5: 6}

_python_utils_tests/test_generators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,6 @@ def test_batcher():
6363
assert len(batch) == 3
6464

6565
for batch in python_utils.batcher(range(4), 3):
66-
pass
66+
assert batch is not None
6767

6868
assert len(batch) == 1

_python_utils_tests/test_import.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ def relative_import(level: int):
1414

1515

1616
def test_import_globals_without_inspection():
17-
locals_ = {}
18-
globals_ = {'__name__': __name__}
17+
locals_: types.Dict[str, types.Any] = {}
18+
globals_: types.Dict[str, types.Any] = {'__name__': __name__}
1919
import_.import_global(
2020
'python_utils.formatters', locals_=locals_, globals_=globals_
2121
)
2222
assert 'camel_to_underscore' in globals_
2323

2424

2525
def test_import_globals_single_method():
26-
locals_ = {}
27-
globals_ = {'__name__': __name__}
26+
locals_: types.Dict[str, types.Any] = {}
27+
globals_: types.Dict[str, types.Any] = {'__name__': __name__}
2828
import_.import_global(
2929
'python_utils.formatters',
3030
['camel_to_underscore'],

_python_utils_tests/test_logger.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from python_utils.loguru import Logurud
44

5-
65
loguru = pytest.importorskip('loguru')
76

87

_python_utils_tests/test_time.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def test_timeout_generator(
7979
iterable=iterable,
8080
maximum_interval=maximum_interval,
8181
):
82-
pass
82+
assert i is not None
8383

8484
assert i == result
8585

@@ -123,10 +123,7 @@ async def generator():
123123

124124

125125
@pytest.mark.asyncio
126-
async def test_aio_generator_timeout_detector_decorator():
127-
# Make pyright happy
128-
i = None
129-
126+
async def test_aio_generator_timeout_detector_decorator_reraise():
130127
# Test regular timeout with reraise
131128
@python_utils.aio_generator_timeout_detector_decorator(timeout=0.05)
132129
async def generator_timeout():
@@ -135,9 +132,15 @@ async def generator_timeout():
135132
yield i
136133

137134
with pytest.raises(asyncio.TimeoutError):
138-
async for i in generator_timeout():
135+
async for _ in generator_timeout():
139136
pass
140137

138+
139+
@pytest.mark.asyncio
140+
async def test_aio_generator_timeout_detector_decorator_clean_exit():
141+
# Make pyright happy
142+
i = None
143+
141144
# Test regular timeout with clean exit
142145
@python_utils.aio_generator_timeout_detector_decorator(
143146
timeout=0.05, on_timeout=None
@@ -152,6 +155,9 @@ async def generator_clean():
152155

153156
assert i == 4
154157

158+
159+
@pytest.mark.asyncio
160+
async def test_aio_generator_timeout_detector_decorator_reraise_total():
155161
# Test total timeout with reraise
156162
@python_utils.aio_generator_timeout_detector_decorator(total_timeout=0.1)
157163
async def generator_reraise():
@@ -160,9 +166,15 @@ async def generator_reraise():
160166
yield i
161167

162168
with pytest.raises(asyncio.TimeoutError):
163-
async for i in generator_reraise():
169+
async for _ in generator_reraise():
164170
pass
165171

172+
173+
@pytest.mark.asyncio
174+
async def test_aio_generator_timeout_detector_decorator_clean_total():
175+
# Make pyright happy
176+
i = None
177+
166178
# Test total timeout with clean exit
167179
@python_utils.aio_generator_timeout_detector_decorator(
168180
total_timeout=0.1, on_timeout=None

docs/conf.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
# Configuration file for the Sphinx documentation builder.
1+
"""
2+
Configuration file for the Sphinx documentation builder.
23
#
3-
# This file only contains a selection of the most common options. For a full
4-
# list see the documentation:
5-
# https://www.sphinx-doc.org/en/master/usage/configuration.html
4+
This file only contains a selection of the most common options. For a full
5+
list see the documentation:
6+
https://www.sphinx-doc.org/en/master/usage/configuration.html
67
7-
# -- Path setup --------------------------------------------------------------
8+
-- Path setup --------------------------------------------------------------
89
9-
# If extensions (or modules to document with autodoc) are in another directory,
10-
# add these directories to sys.path here. If the directory is relative to the
11-
# documentation root, use os.path.abspath to make it absolute, like shown here.
10+
If extensions (or modules to document with autodoc) are in another directory,
11+
add these directories to sys.path here. If the directory is relative to the
12+
documentation root, use os.path.abspath to make it absolute, like shown here.
1213
#
13-
from datetime import date
14+
"""
1415
import os
1516
import sys
17+
from datetime import date
1618

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

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

30-
3132
# -- General configuration ---------------------------------------------------
3233

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

53-
5454
# -- Options for HTML output -------------------------------------------------
5555

5656
# The theme to use for HTML and HTML Help pages. See the documentation for

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ skip-string-normalization = true
55

66
[tool.pyright]
77
# include = ['python_utils']
8-
include = ['python_utils', '_python_utils_tests']
9-
strict = ['python_utils', '_python_utils_tests']
8+
include = ['python_utils', '_python_utils_tests', 'setup.py']
9+
strict = ['python_utils', '_python_utils_tests', 'setup.py']
1010
# The terminal file is very OS specific and dependent on imports so we're skipping it from type checking
1111
ignore = ['python_utils/terminal.py']
12-
pythonVersion = '3.8'
12+
pythonVersion = '3.9'
13+

python_utils/__about__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
"""
2+
This module contains metadata about the `python-utils` package.
3+
4+
Attributes:
5+
__package_name__ (str): The name of the package.
6+
__author__ (str): The author of the package.
7+
__author_email__ (str): The email of the author.
8+
__description__ (str): A brief description of the package.
9+
__url__ (str): The URL of the package's repository.
10+
__version__ (str): The current version of the package.
11+
"""
12+
113
__package_name__: str = 'python-utils'
214
__author__: str = 'Rick van Hattem'
315
__author_email__: str = 'Wolph@wol.ph'

python_utils/__init__.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,55 @@
1+
"""
2+
This module initializes the `python_utils` package by importing various
3+
submodules and functions.
4+
5+
Submodules:
6+
aio
7+
compat
8+
converters
9+
decorators
10+
formatters
11+
generators
12+
import_
13+
logger
14+
terminal
15+
time
16+
types
17+
18+
Functions:
19+
acount
20+
remap
21+
scale_1024
22+
to_float
23+
to_int
24+
to_str
25+
to_unicode
26+
listify
27+
set_attributes
28+
raise_exception
29+
reraise
30+
camel_to_underscore
31+
timesince
32+
abatcher
33+
batcher
34+
import_global
35+
get_terminal_size
36+
aio_generator_timeout_detector
37+
aio_generator_timeout_detector_decorator
38+
aio_timeout_generator
39+
delta_to_seconds
40+
delta_to_seconds_or_none
41+
format_time
42+
timedelta_to_seconds
43+
timeout_generator
44+
45+
Classes:
46+
CastedDict
47+
LazyCastedDict
48+
UniqueList
49+
Logged
50+
LoggerBase
51+
"""
52+
153
from . import (
254
aio,
355
compat,

python_utils/aio.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
'''
2-
Asyncio equivalents to regular Python functions.
1+
"""Asyncio equivalents to regular Python functions."""
32

4-
'''
53
import asyncio
64
import itertools
5+
import typing
76

87
from . import types
98

109
_N = types.TypeVar('_N', int, float)
1110
_T = types.TypeVar('_T')
11+
_K = types.TypeVar('_K')
12+
_V = types.TypeVar('_V')
1213

1314

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

2829

30+
@typing.overload
31+
async def acontainer(
32+
iterable: types.Union[
33+
types.AsyncIterable[_T],
34+
types.Callable[..., types.AsyncIterable[_T]],
35+
],
36+
container: types.Type[types.Tuple[_T, ...]],
37+
) -> types.Tuple[_T, ...]: ...
38+
39+
40+
@typing.overload
41+
async def acontainer(
42+
iterable: types.Union[
43+
types.AsyncIterable[_T],
44+
types.Callable[..., types.AsyncIterable[_T]],
45+
],
46+
container: types.Type[types.List[_T]] = list,
47+
) -> types.List[_T]: ...
48+
49+
50+
@typing.overload
51+
async def acontainer(
52+
iterable: types.Union[
53+
types.AsyncIterable[_T],
54+
types.Callable[..., types.AsyncIterable[_T]],
55+
],
56+
container: types.Type[types.Set[_T]],
57+
) -> types.Set[_T]: ...
58+
59+
2960
async def acontainer(
3061
iterable: types.Union[
3162
types.AsyncIterable[_T],
3263
types.Callable[..., types.AsyncIterable[_T]],
3364
],
3465
container: types.Callable[[types.Iterable[_T]], types.Iterable[_T]] = list,
3566
) -> types.Iterable[_T]:
36-
'''
37-
Asyncio version of list()/set()/tuple()/etc() using an async for loop
67+
"""
68+
Asyncio version of list()/set()/tuple()/etc() using an async for loop.
3869
3970
So instead of doing `[item async for item in iterable]` you can do
4071
`await acontainer(iterable)`.
4172
42-
'''
73+
"""
4374
iterable_: types.AsyncIterable[_T]
4475
if callable(iterable):
4576
iterable_ = iterable()
@@ -52,3 +83,33 @@ async def acontainer(
5283
items.append(item)
5384

5485
return container(items)
86+
87+
88+
async def adict(
89+
iterable: types.Union[
90+
types.AsyncIterable[types.Tuple[_K, _V]],
91+
types.Callable[..., types.AsyncIterable[types.Tuple[_K, _V]]],
92+
],
93+
container: types.Callable[
94+
[types.Iterable[types.Tuple[_K, _V]]], types.Mapping[_K, _V]
95+
] = dict,
96+
) -> types.Mapping[_K, _V]:
97+
"""
98+
Asyncio version of dict() using an async for loop.
99+
100+
So instead of doing `{key: value async for key, value in iterable}` you
101+
can do `await adict(iterable)`.
102+
103+
"""
104+
iterable_: types.AsyncIterable[types.Tuple[_K, _V]]
105+
if callable(iterable):
106+
iterable_ = iterable()
107+
else:
108+
iterable_ = iterable
109+
110+
item: types.Tuple[_K, _V]
111+
items: types.List[types.Tuple[_K, _V]] = []
112+
async for item in iterable_:
113+
items.append(item)
114+
115+
return container(items)

0 commit comments

Comments
 (0)