Skip to content

Commit 0448374

Browse files
committed
Merge branch 'release/3.3.0'
2 parents 91e12fe + 102e603 commit 0448374

File tree

11 files changed

+433
-108
lines changed

11 files changed

+433
-108
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
3+
import python_utils
4+
5+
6+
@pytest.mark.asyncio
7+
async def test_abatcher():
8+
async for batch in python_utils.abatcher(python_utils.acount(stop=9), 3):
9+
assert len(batch) == 3
10+
11+
async for batch in python_utils.abatcher(python_utils.acount(stop=2), 3):
12+
assert len(batch) == 2
13+
14+
15+
@pytest.mark.asyncio
16+
async def test_abatcher_timed():
17+
batches = []
18+
async for batch in python_utils.abatcher(
19+
python_utils.acount(stop=10, delay=0.08),
20+
interval=0.2
21+
):
22+
batches.append(batch)
23+
24+
assert len(batches) == 3
25+
assert sum(len(batch) for batch in batches) == 10
26+
27+
28+
def test_batcher():
29+
batch = []
30+
for batch in python_utils.batcher(range(9), 3):
31+
assert len(batch) == 3
32+
33+
for batch in python_utils.batcher(range(4), 3):
34+
pass
35+
36+
assert len(batch) == 1

_python_utils_tests/test_time.py

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1+
import asyncio
12
import itertools
23
from datetime import timedelta
34

45
import pytest
56

6-
from python_utils import aio
7-
from python_utils import time
7+
import python_utils
88

99

1010
@pytest.mark.parametrize(
1111
'timeout,interval,interval_multiplier,maximum_interval,iterable,result', [
12-
(0.1, 0.06, 0.5, 0.1, aio.acount, 2),
13-
(0.2, 0.06, 0.5, 0.1, aio.acount(), 4),
14-
(0.3, 0.06, 1.0, None, aio.acount, 5),
12+
(0.2, 0.1, 0.4, 0.2, python_utils.acount, 2),
13+
(0.3, 0.1, 0.4, 0.2, python_utils.acount(), 3),
14+
(0.3, 0.06, 1.0, None, python_utils.acount, 5),
1515
(timedelta(seconds=0.1), timedelta(seconds=0.06),
16-
2.0, timedelta(seconds=0.1), aio.acount, 2),
16+
2.0, timedelta(seconds=0.1), python_utils.acount, 2),
1717
])
1818
@pytest.mark.asyncio
1919
async def test_aio_timeout_generator(timeout, interval, interval_multiplier,
2020
maximum_interval, iterable, result):
2121
i = None
22-
async for i in time.aio_timeout_generator(
23-
timeout, interval, iterable,
24-
maximum_interval=maximum_interval):
22+
async for i in python_utils.aio_timeout_generator(
23+
timeout, interval, iterable,
24+
maximum_interval=maximum_interval
25+
):
2526
pass
2627

2728
assert i == result
@@ -41,13 +42,103 @@ async def test_aio_timeout_generator(timeout, interval, interval_multiplier,
4142
def test_timeout_generator(timeout, interval, interval_multiplier,
4243
maximum_interval, iterable, result):
4344
i = None
44-
for i in time.timeout_generator(
45-
timeout=timeout,
46-
interval=interval,
47-
interval_multiplier=interval_multiplier,
48-
iterable=iterable,
49-
maximum_interval=maximum_interval,
45+
for i in python_utils.timeout_generator(
46+
timeout=timeout,
47+
interval=interval,
48+
interval_multiplier=interval_multiplier,
49+
iterable=iterable,
50+
maximum_interval=maximum_interval,
5051
):
5152
pass
5253

5354
assert i == result
55+
56+
57+
@pytest.mark.asyncio
58+
async def test_aio_generator_timeout_detector():
59+
async def generator():
60+
for i in range(10):
61+
await asyncio.sleep(i / 100.0)
62+
yield i
63+
64+
detector = python_utils.aio_generator_timeout_detector
65+
# Test regular timeout with reraise
66+
with pytest.raises(asyncio.TimeoutError):
67+
async for i in detector(generator(), 0.05):
68+
pass
69+
70+
# Test regular timeout with clean exit
71+
async for i in detector(generator(), 0.05, on_timeout=None):
72+
pass
73+
74+
assert i == 4
75+
76+
# Test total timeout with reraise
77+
with pytest.raises(asyncio.TimeoutError):
78+
async for i in detector(generator(), total_timeout=0.1):
79+
pass
80+
81+
# Test total timeout with clean exit
82+
async for i in detector(generator(), total_timeout=0.1, on_timeout=None):
83+
pass
84+
85+
assert i == 4
86+
87+
# Test stop iteration
88+
async for i in detector(generator(), on_timeout=None):
89+
pass
90+
91+
92+
@pytest.mark.asyncio
93+
async def test_aio_generator_timeout_detector_decorator():
94+
# Test regular timeout with reraise
95+
@python_utils.aio_generator_timeout_detector_decorator(timeout=0.05)
96+
async def generator():
97+
for i in range(10):
98+
await asyncio.sleep(i / 100.0)
99+
yield i
100+
101+
with pytest.raises(asyncio.TimeoutError):
102+
async for i in generator():
103+
pass
104+
105+
# Test regular timeout with clean exit
106+
@python_utils.aio_generator_timeout_detector_decorator(
107+
timeout=0.05,
108+
on_timeout=None
109+
)
110+
async def generator():
111+
for i in range(10):
112+
await asyncio.sleep(i / 100.0)
113+
yield i
114+
115+
async for i in generator():
116+
pass
117+
118+
assert i == 4
119+
120+
# Test total timeout with reraise
121+
@python_utils.aio_generator_timeout_detector_decorator(total_timeout=0.1)
122+
async def generator():
123+
for i in range(10):
124+
await asyncio.sleep(i / 100.0)
125+
yield i
126+
127+
with pytest.raises(asyncio.TimeoutError):
128+
async for i in generator():
129+
pass
130+
131+
# Test total timeout with clean exit
132+
@python_utils.aio_generator_timeout_detector_decorator(
133+
total_timeout=0.1,
134+
on_timeout=None
135+
)
136+
async def generator():
137+
for i in range(10):
138+
await asyncio.sleep(i / 100.0)
139+
yield i
140+
141+
async for i in generator():
142+
pass
143+
144+
assert i == 4

python_utils/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
'with the standard Python install')
77
__url__: str = 'https://github.com/WoLpH/python-utils'
88
# Omit type info due to automatic versioning script
9-
__version__ = '3.2.3'
9+
__version__ = '3.3.0'

python_utils/__init__.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,39 @@
1-
from . import aio
2-
from . import compat
3-
from . import converters
4-
from . import decorators
5-
from . import formatters
6-
from . import import_
7-
from . import logger
8-
from . import terminal
9-
from . import time
10-
from . import types
11-
1+
from . import (
2+
aio,
3+
compat,
4+
converters,
5+
decorators,
6+
formatters,
7+
generators,
8+
import_,
9+
logger,
10+
terminal,
11+
time,
12+
types,
13+
)
1214
from .aio import acount
13-
from .converters import remap
14-
from .converters import scale_1024
15-
from .converters import to_float
16-
from .converters import to_int
17-
from .converters import to_str
18-
from .converters import to_unicode
19-
from .decorators import listify
20-
from .decorators import set_attributes
21-
from .formatters import camel_to_underscore
22-
from .formatters import timesince
15+
from .converters import remap, scale_1024, to_float, to_int, to_str, to_unicode
16+
from .decorators import listify, set_attributes
17+
from .exceptions import raise_exception, reraise
18+
from .formatters import camel_to_underscore, timesince
19+
from .generators import abatcher, batcher
2320
from .import_ import import_global
24-
from .terminal import get_terminal_size
25-
from .time import format_time
26-
from .time import timedelta_to_seconds
27-
from .time import timeout_generator
28-
from .time import aio_timeout_generator
2921
from .logger import Logged, LoggerBase
22+
from .terminal import get_terminal_size
23+
from .time import (
24+
aio_generator_timeout_detector,
25+
aio_generator_timeout_detector_decorator,
26+
aio_timeout_generator,
27+
delta_to_seconds,
28+
delta_to_seconds_or_none,
29+
format_time,
30+
timedelta_to_seconds,
31+
timeout_generator,
32+
)
3033

3134
__all__ = [
3235
'aio',
36+
'generators',
3337
'compat',
3438
'converters',
3539
'decorators',
@@ -55,7 +59,15 @@
5559
'format_time',
5660
'timeout_generator',
5761
'acount',
62+
'abatcher',
63+
'batcher',
5864
'aio_timeout_generator',
65+
'aio_generator_timeout_detector_decorator',
66+
'aio_generator_timeout_detector',
67+
'delta_to_seconds',
68+
'delta_to_seconds_or_none',
69+
'reraise',
70+
'raise_exception',
5971
'Logged',
6072
'LoggerBase',
6173
]

python_utils/aio.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
'''
2+
Asyncio equivalents to regular Python functions.
3+
4+
'''
15
import asyncio
26
import itertools
37

48

5-
async def acount(start=0, step=1, delay=0):
9+
async def acount(start=0, step=1, delay=0, stop=None):
610
'''Asyncio version of itertools.count()'''
711
for item in itertools.count(start, step): # pragma: no branch
12+
if stop is not None and item >= stop:
13+
break
14+
815
yield item
916
await asyncio.sleep(delay)

python_utils/converters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def to_int(
8383
raise TypeError('unknown argument for regexp parameter: %r' % regexp)
8484

8585
try:
86-
if regexp:
86+
if regexp and input_:
8787
match = regexp.search(input_)
8888
if match:
8989
input_ = match.groups()[-1]

python_utils/exceptions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import typing
2+
3+
4+
def raise_exception(
5+
exception_class: typing.Type[Exception],
6+
*args: typing.Any,
7+
**kwargs: typing.Any,
8+
) -> typing.Callable:
9+
'''
10+
Returns a function that raises an exception of the given type with the
11+
given arguments.
12+
13+
>>> raise_exception(ValueError, 'spam')('eggs')
14+
Traceback (most recent call last):
15+
...
16+
ValueError: spam
17+
'''
18+
19+
def raise_(*args_: typing.Any, **kwargs_: typing.Any) -> typing.Any:
20+
raise exception_class(*args, **kwargs)
21+
22+
return raise_
23+
24+
25+
def reraise(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
26+
raise

python_utils/generators.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import time
2+
3+
import python_utils
4+
from python_utils import types
5+
6+
7+
async def abatcher(
8+
generator: types.AsyncGenerator,
9+
batch_size: types.Optional[int] = None,
10+
interval: types.Optional[types.delta_type] = None,
11+
):
12+
'''
13+
Asyncio generator wrapper that returns items with a given batch size or
14+
interval (whichever is reached first).
15+
'''
16+
batch: list = []
17+
18+
assert batch_size or interval, 'Must specify either batch_size or interval'
19+
20+
if interval:
21+
interval_s = python_utils.delta_to_seconds(interval)
22+
next_yield = time.perf_counter() + interval_s
23+
else:
24+
interval_s = 0
25+
next_yield = 0
26+
27+
while True:
28+
try:
29+
item = await generator.__anext__()
30+
except StopAsyncIteration:
31+
if batch:
32+
yield batch
33+
break
34+
else:
35+
batch.append(item)
36+
37+
if batch_size is not None and len(batch) == batch_size:
38+
yield batch
39+
batch = []
40+
41+
if interval and batch and time.perf_counter() > next_yield:
42+
yield batch
43+
batch = []
44+
# Always set the next yield time to the current time. If the
45+
# loop is running slow due to blocking functions we do not
46+
# want to burst too much
47+
next_yield = time.perf_counter() + interval_s
48+
49+
50+
def batcher(iterable, batch_size):
51+
'''
52+
Generator wrapper that returns items with a given batch size
53+
'''
54+
batch = []
55+
for item in iterable:
56+
batch.append(item)
57+
if len(batch) == batch_size:
58+
yield batch
59+
batch = []
60+
61+
if batch:
62+
yield batch

python_utils/logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class Logged(LoggerBase):
8888
'spam'
8989
'''
9090

91-
logger: logging.Logger
91+
logger: logging.Logger # pragma: no cover
9292

9393
@classmethod
9494
def __get_name(cls, *name_parts: str) -> str:

0 commit comments

Comments
 (0)