Skip to content

Commit 89a7e17

Browse files
committed
Switch to 80 maxline now that I'm using 3.12 type aliases
1 parent 8101a4e commit 89a7e17

File tree

14 files changed

+223
-140
lines changed

14 files changed

+223
-140
lines changed

.editorconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ indent_size = 4
99

1010
[*.py]
1111
indent_style = space
12-
max_line_length = 88
12+
max_line_length = 90
1313

1414
[*.pyi]
1515
indent_style = space
16-
max_line_length = 88
16+
max_line_length = 90
1717

1818
[*.md]
1919
indent_style = space

async_utils/_cpython_stuff.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
class _HashedSeq(list[Any]):
2222
"""This class guarantees that hash() will be called no more than once
2323
per element. This is important because the lru_cache() will hash
24-
the key multiple times on a cache miss.
25-
"""
24+
the key multiple times on a cache miss."""
2625

2726
__slots__ = ("hashvalue",)
2827

29-
def __init__(self, tup: tuple[Any, ...], hash: Callable[[object], int] = hash): # noqa: A002
28+
def __init__(
29+
self, tup: tuple[Any, ...], hash: Callable[[object], int] = hash
30+
): # noqa: A002
3031
self[:] = tup
3132
self.hashvalue: int = hash(tup)
3233

@@ -47,8 +48,7 @@ def make_key(
4748
as a nested structure that would take more memory.
4849
If there is only a single argument and its data type is known to cache
4950
its hash value, then that argument is returned without a wrapper. This
50-
saves space and improves lookup speed.
51-
"""
51+
saves space and improves lookup speed."""
5252
# All of code below relies on kwds preserving the order input by the user.
5353
# Formerly, we sorted() the kwds before looping. The new way is *much*
5454
# faster; however, it means that f(x=1, y=2) will now be treated as a

async_utils/_lru.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright 2020-present Michael Hall
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
__all__ = ("LRU",)
18+
19+
20+
class LRU[K, V]:
21+
def __init__(self, maxsize: int, /):
22+
self.cache: dict[K, V] = {}
23+
self.maxsize = maxsize
24+
25+
def get[T](self, key: K, default: T, /) -> V | T:
26+
try:
27+
return self[key]
28+
except KeyError:
29+
return default
30+
31+
def __getitem__(self, key: K, /) -> V:
32+
val = self.cache[key] = self.cache.pop(key)
33+
return val
34+
35+
def __setitem__(self, key: K, value: V, /):
36+
self.cache[key] = value
37+
if len(self.cache) > self.maxsize:
38+
self.cache.pop(next(iter(self.cache)))
39+
40+
def remove(self, key: K, /) -> None:
41+
self.cache.pop(key, None)

async_utils/bg_loop.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,19 @@ async def run(self, coro: _FutureLike[_T]) -> _T:
4747
return await asyncio.wrap_future(future)
4848

4949

50-
def run_forever(loop: asyncio.AbstractEventLoop, use_eager_task_factory: bool, /) -> None:
50+
def run_forever(
51+
loop: asyncio.AbstractEventLoop, use_eager_task_factory: bool, /
52+
) -> None:
5153
asyncio.set_event_loop(loop)
5254
if use_eager_task_factory:
5355
loop.set_task_factory(asyncio.eager_task_factory)
5456
try:
5557
loop.run_forever()
5658
finally:
5759
loop.run_until_complete(asyncio.sleep(0))
58-
tasks: set[asyncio.Task[Any]] = {t for t in asyncio.all_tasks(loop) if not t.done()}
60+
tasks: set[asyncio.Task[Any]] = {
61+
t for t in asyncio.all_tasks(loop) if not t.done()
62+
}
5963
for t in tasks:
6064
t.cancel()
6165
loop.run_until_complete(asyncio.sleep(0))
@@ -80,7 +84,9 @@ def run_forever(loop: asyncio.AbstractEventLoop, use_eager_task_factory: bool, /
8084

8185

8286
@contextmanager
83-
def threaded_loop(*, use_eager_task_factory: bool = True) -> Generator[LoopWrapper, None, None]:
87+
def threaded_loop(
88+
*, use_eager_task_factory: bool = True
89+
) -> Generator[LoopWrapper, None, None]:
8490
"""Starts an event loop on a background thread,
8591
and yields an object with scheduling methods for interacting with
8692
the loop.
@@ -89,7 +95,9 @@ def threaded_loop(*, use_eager_task_factory: bool = True) -> Generator[LoopWrapp
8995
loop = asyncio.new_event_loop()
9096
thread = None
9197
try:
92-
thread = threading.Thread(target=run_forever, args=(loop, use_eager_task_factory))
98+
thread = threading.Thread(
99+
target=run_forever, args=(loop, use_eager_task_factory)
100+
)
93101
thread.start()
94102
yield LoopWrapper(loop)
95103
finally:

async_utils/bg_tasks.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ def __init__(self, exit_timeout: float | None) -> None:
3333
self._tasks: set[asyncio.Task[Any]] = set()
3434
self._exit_timeout: float | None = exit_timeout
3535

36-
def create_task(self, coro: _CoroutineLike[_T], *, name: str | None = None, context: Context | None = None) -> Any:
36+
def create_task(
37+
self,
38+
coro: _CoroutineLike[_T],
39+
*,
40+
name: str | None = None,
41+
context: Context | None = None,
42+
) -> Any:
3743
t = asyncio.create_task(coro)
3844
self._tasks.add(t)
3945
t.add_done_callback(self._tasks.discard)
@@ -44,7 +50,9 @@ async def __aenter__(self: Self) -> Self:
4450

4551
async def __aexit__(self, *_dont_care: Any):
4652
while tsks := self._tasks.copy():
47-
_done, _pending = await asyncio.wait(tsks, timeout=self._exit_timeout)
53+
_done, _pending = await asyncio.wait(
54+
tsks, timeout=self._exit_timeout
55+
)
4856
for task in _pending:
4957
task.cancel()
5058
await asyncio.sleep(0)

async_utils/corofunc_cache.py

Lines changed: 39 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -20,65 +20,45 @@
2020
from typing import Any, ParamSpec, TypeVar
2121

2222
from ._cpython_stuff import make_key
23+
from ._lru import LRU
2324

2425
__all__ = ("corocache", "lrucorocache")
2526

2627

2728
P = ParamSpec("P")
28-
T = TypeVar("T")
29+
R = TypeVar("R")
2930

3031

31-
type CoroFunc[**P, T] = Callable[P, Coroutine[Any, Any, T]]
32-
type CoroLike[**P, T] = Callable[P, Awaitable[T]]
33-
34-
35-
class LRU[K, V]:
36-
def __init__(self, maxsize: int, /):
37-
self.cache: dict[K, V] = {}
38-
self.maxsize = maxsize
39-
40-
def get(self, key: K, default: T, /) -> V | T:
41-
if key not in self.cache:
42-
return default
43-
self.cache[key] = self.cache.pop(key)
44-
return self.cache[key]
45-
46-
def __getitem__(self, key: K, /) -> V:
47-
self.cache[key] = self.cache.pop(key)
48-
return self.cache[key]
49-
50-
def __setitem__(self, key: K, value: V, /):
51-
self.cache[key] = value
52-
if len(self.cache) > self.maxsize:
53-
self.cache.pop(next(iter(self.cache)))
54-
55-
def remove(self, key: K, /) -> None:
56-
self.cache.pop(key, None)
32+
type CoroFunc[**P, R] = Callable[P, Coroutine[Any, Any, R]]
33+
type CoroLike[**P, R] = Callable[P, Awaitable[R]]
5734

5835

5936
def corocache(
6037
ttl: float | None = None,
61-
) -> Callable[[CoroLike[P, T]], CoroFunc[P, T]]:
38+
) -> Callable[[CoroLike[P, R]], CoroFunc[P, R]]:
6239
"""Decorator to cache coroutine functions.
6340
64-
This is less powerful than the version in task_cache.py but may work better for
65-
some cases where typing of libraries this interacts with is too restrictive.
41+
This is less powerful than the version in task_cache.py but may work better
42+
for some cases where typing of libraries this interacts with is too restrictive.
6643
67-
Note: This uses the args and kwargs of the original coroutine function as a cache key.
68-
This includes instances (self) when wrapping methods.
69-
Consider not wrapping instance methods, but what those methods call when feasible in cases where this may matter.
44+
Note: This uses the args and kwargs of the original coroutine function as a
45+
cache key. This includes instances (self) when wrapping methods.
46+
Consider not wrapping instance methods, but what those methods call when feasible
47+
in cases where this may matter.
7048
7149
The ordering of args and kwargs matters."""
7250

73-
def wrapper(coro: CoroLike[P, T]) -> CoroFunc[P, T]:
74-
internal_cache: dict[Hashable, asyncio.Future[T]] = {}
51+
def wrapper(coro: CoroLike[P, R]) -> CoroFunc[P, R]:
52+
internal_cache: dict[Hashable, asyncio.Future[R]] = {}
7553

76-
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
54+
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
7755
key = make_key(args, kwargs)
7856
try:
7957
return await internal_cache[key]
8058
except KeyError:
81-
internal_cache[key] = fut = asyncio.ensure_future(coro(*args, **kwargs))
59+
internal_cache[key] = fut = asyncio.ensure_future(
60+
coro(*args, **kwargs)
61+
)
8262
if ttl is not None:
8363
# This results in internal_cache.pop(key, fut) later
8464
# while avoiding a late binding issue with a lambda instead
@@ -96,37 +76,45 @@ async def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
9676
return wrapper
9777

9878

99-
def _lru_evict(ttl: float, cache: LRU[Hashable, Any], key: Hashable, _ignored_fut: object) -> None:
79+
def _lru_evict(
80+
ttl: float, cache: LRU[Hashable, Any], key: Hashable, _ignored_fut: object
81+
) -> None:
10082
asyncio.get_running_loop().call_later(ttl, cache.remove, key)
10183

10284

103-
def lrucorocache(ttl: float | None = None, maxsize: int = 1024) -> Callable[[CoroLike[P, T]], CoroFunc[P, T]]:
85+
def lrucorocache(
86+
ttl: float | None = None, maxsize: int = 1024
87+
) -> Callable[[CoroLike[P, R]], CoroFunc[P, R]]:
10488
"""Decorator to cache coroutine functions.
10589
106-
This is less powerful than the version in task_cache.py but may work better for
107-
some cases where typing of libraries this interacts with is too restrictive.
90+
This is less powerful than the version in task_cache.py but may work better
91+
for some cases where typing of libraries this interacts with is too restrictive.
10892
109-
Note: This uses the args and kwargs of the original coroutine function as a cache key.
110-
This includes instances (self) when wrapping methods.
111-
Consider not wrapping instance methods, but what those methods call when feasible in cases where this may matter.
93+
Note: This uses the args and kwargs of the original coroutine function as a
94+
cache key. This includes instances (self) when wrapping methods.
95+
Consider not wrapping instance methods, but what those methods call when feasible
96+
in cases where this may matter.
11297
11398
The ordering of args and kwargs matters.
11499
115-
futs are evicted by LRU and ttl.
116-
"""
100+
cached results are evicted by LRU and ttl."""
117101

118-
def wrapper(coro: CoroLike[P, T]) -> CoroFunc[P, T]:
119-
internal_cache: LRU[Hashable, asyncio.Future[T]] = LRU(maxsize)
102+
def wrapper(coro: CoroLike[P, R]) -> CoroFunc[P, R]:
103+
internal_cache: LRU[Hashable, asyncio.Future[R]] = LRU(maxsize)
120104

121105
@wraps(coro)
122-
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
106+
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
123107
key = make_key(args, kwargs)
124108
try:
125109
return await internal_cache[key]
126110
except KeyError:
127-
internal_cache[key] = fut = asyncio.ensure_future(coro(*args, **kwargs))
111+
internal_cache[key] = fut = asyncio.ensure_future(
112+
coro(*args, **kwargs)
113+
)
128114
if ttl is not None:
129-
fut.add_done_callback(partial(_lru_evict, ttl, internal_cache, key))
115+
fut.add_done_callback(
116+
partial(_lru_evict, ttl, internal_cache, key)
117+
)
130118
return await fut
131119

132120
return wrapped

async_utils/lockout.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,18 @@ class Lockout:
4343
async with ratelimiter, lockout:
4444
response = await some_request(route, **parameters)
4545
if response.code == 429:
46-
if reset_after := response.headers.get('X-Ratelimit-Reset-After')
47-
lockout.lock_for(reset_after)
46+
if reset := response.headers.get('X-Ratelimit-Reset-After')
47+
lockout.lock_for(reset)
4848
4949
"""
5050

5151
def __repr__(self) -> str:
5252
res = super().__repr__()
53-
extra = "unlocked" if not self._lockouts else f"locked, timestamps={self._lockouts:!r}"
53+
extra = (
54+
"unlocked"
55+
if not self._lockouts
56+
else f"locked, timestamps={self._lockouts:!r}"
57+
)
5458
return f"<{res[1:-1]} [{extra}]>"
5559

5660
def __init__(self) -> None:
@@ -105,7 +109,11 @@ def __init__(self) -> None:
105109

106110
def __repr__(self) -> str:
107111
res = super().__repr__()
108-
extra = "unlocked" if not self._lockouts else f"locked, timestamps={self._lockouts:!r}"
112+
extra = (
113+
"unlocked"
114+
if not self._lockouts
115+
else f"locked, timestamps={self._lockouts:!r}"
116+
)
109117
return f"<{res[1:-1]} [{extra}]>"
110118

111119
def lockout_for(self, seconds: float, /) -> None:

async_utils/priority_sem.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727

2828
_global_lock = threading.Lock()
2929

30-
_priority: contextvars.ContextVar[int] = contextvars.ContextVar("_priority", default=0)
30+
_priority: contextvars.ContextVar[int] = contextvars.ContextVar(
31+
"_priority", default=0
32+
)
3133

3234

3335
class PriorityWaiter(NamedTuple):
@@ -109,8 +111,11 @@ def __repr__(self):
109111

110112
def locked(self) -> bool:
111113
# Must do a comparison based on priority then FIFO
112-
# in the case of existing waiters, not guaranteed to be immediately available
113-
return self._value == 0 or (any(not w.cancelled() for w in (self._waiters or ())))
114+
# in the case of existing waiters
115+
# not guaranteed to be immediately available
116+
return self._value == 0 or (
117+
any(not w.cancelled() for w in (self._waiters or ()))
118+
)
114119

115120
async def __aenter__(self):
116121
prio = _priority.get()

async_utils/ratelimiter.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,16 @@ def __init__(self, rate_limit: int, period: float, granularity: float):
3434
self._monotonics: deque[float] = deque()
3535

3636
async def __aenter__(self):
37-
# The ordering of these conditions matters to avoid an async context switch between
38-
# confirming the ratelimit isn't exhausted and allowing the user code to continue.
39-
while (len(self._monotonics) >= self.rate_limit) and await asyncio.sleep(self.granularity, True):
37+
# The ordering of these conditions matters to avoid an async context
38+
# switch between confirming the ratelimit isn't exhausted and allowing
39+
# the user code to continue.
40+
while (
41+
len(self._monotonics) >= self.rate_limit
42+
) and await asyncio.sleep(self.granularity, True):
4043
now = time.monotonic()
41-
while self._monotonics and (now - self._monotonics[0] > self.period):
44+
while self._monotonics and (
45+
now - self._monotonics[0] > self.period
46+
):
4247
self._monotonics.popleft()
4348

4449
self._monotonics.append(time.monotonic())

async_utils/scheduler.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,17 @@ async def __anext__(self) -> T:
100100
self.__tqueue.task_done()
101101
raise StopAsyncIteration
102102

103-
async def create_task(self, timestamp: float, payload: T, /) -> CancelationToken:
103+
async def create_task(
104+
self, timestamp: float, payload: T, /
105+
) -> CancelationToken:
104106
t = _Task(timestamp, payload)
105107
self.__tasks[t.cancel_token] = t
106108
await self.__tqueue.put(t)
107109
return t.cancel_token
108110

109111
async def cancel_task(self, cancel_token: CancelationToken, /) -> bool:
110-
"""Returns if the task with that CancelationToken. Cancelling an already cancelled task is allowed."""
112+
"""Returns if the task with that CancelationToken. Cancelling an
113+
already cancelled task is allowed and has no additional effect."""
111114
async with self.__l:
112115
try:
113116
task = self.__tasks[cancel_token]

0 commit comments

Comments
 (0)