Skip to content

Commit f2e28be

Browse files
Merge branch 'master' into paramspec
2 parents c8156f9 + ba376e8 commit f2e28be

File tree

7 files changed

+52
-29
lines changed

7 files changed

+52
-29
lines changed

.github/workflows/auto-merge.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
steps:
1313
- name: Dependabot metadata
1414
id: metadata
15-
uses: dependabot/fetch-metadata@v2.1.0
15+
uses: dependabot/fetch-metadata@v2.2.0
1616
with:
1717
github-token: "${{ secrets.GITHUB_TOKEN }}"
1818
- name: Enable auto-merge for Dependabot PRs

.github/workflows/ci-cd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ jobs:
145145
uses: pypa/gh-action-pypi-publish@release/v1
146146
147147
- name: Sign the dists with Sigstore
148-
uses: sigstore/gh-action-sigstore-python@v2.1.0
148+
uses: sigstore/gh-action-sigstore-python@v3.0.0
149149
with:
150150
inputs: >-
151151
./dist/*.tar.gz

async_lru/__init__.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -175,22 +175,24 @@ def _task_done_callback(
175175
) -> None:
176176
self.__tasks.discard(task)
177177

178-
cache_item = self.__cache.get(key)
179-
if self.__ttl is not None and cache_item is not None:
180-
loop = asyncio.get_running_loop()
181-
cache_item.later_call = loop.call_later(
182-
self.__ttl, self.__cache.pop, key, None
183-
)
184-
185178
if task.cancelled():
186179
fut.cancel()
180+
self.__cache.pop(key, None)
187181
return
188182

189183
exc = task.exception()
190184
if exc is not None:
191185
fut.set_exception(exc)
186+
self.__cache.pop(key, None)
192187
return
193188

189+
cache_item = self.__cache.get(key)
190+
if self.__ttl is not None and cache_item is not None:
191+
loop = asyncio.get_running_loop()
192+
cache_item.later_call = loop.call_later(
193+
self.__ttl, self.__cache.pop, key, None
194+
)
195+
194196
fut.set_result(task.result())
195197

196198
async def __call__(self, /, *fn_args: _P.args, **fn_kwargs: _P.kwargs) -> _R:
@@ -204,19 +206,11 @@ async def __call__(self, /, *fn_args: _P.args, **fn_kwargs: _P.kwargs) -> _R:
204206
cache_item = self.__cache.get(key)
205207

206208
if cache_item is not None:
209+
self._cache_hit(key)
207210
if not cache_item.fut.done():
208-
self._cache_hit(key)
209211
return await asyncio.shield(cache_item.fut)
210212

211-
exc = cache_item.fut._exception
212-
213-
if exc is None:
214-
self._cache_hit(key)
215-
return cache_item.fut.result()
216-
else:
217-
# exception here
218-
cache_item = self.__cache.pop(key)
219-
cache_item.cancel()
213+
return cache_item.fut.result()
220214

221215
fut = loop.create_future()
222216
coro = self.__wrapped__(*fn_args, **fn_kwargs)

requirements-dev.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
-r requirements.txt
22

3-
flake8==7.0.0
3+
flake8==7.1.0
44
flake8-bandit==4.1.1
55
flake8-bugbear==24.4.26
66
flake8-import-order==0.18.2
7-
flake8-requirements==2.2.0
8-
mypy==1.10.0; implementation_name=="cpython"
7+
flake8-requirements==2.2.1
8+
mypy==1.11.1; implementation_name=="cpython"

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-e .
22

3-
coverage==7.5.1
4-
pytest==8.2.1
5-
pytest-asyncio==0.23.7
3+
coverage==7.6.0
4+
pytest==8.3.2
5+
pytest-asyncio==0.23.8
66
pytest-cov==5.0.0
77
pytest-timeout==2.3.1

tests/test_close.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ async def coro(val: int) -> int:
3131

3232
await close
3333

34-
check_lru(coro, hits=0, misses=5, cache=5, tasks=0)
34+
check_lru(coro, hits=0, misses=5, cache=0, tasks=0)
3535
assert coro.cache_parameters()["closed"]
3636

3737
with pytest.raises(asyncio.CancelledError):
3838
await gather
3939

40-
check_lru(coro, hits=0, misses=5, cache=5, tasks=0)
40+
check_lru(coro, hits=0, misses=5, cache=0, tasks=0)
4141
assert coro.cache_parameters()["closed"]
4242

4343
# double call is no-op

tests/test_exception.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import asyncio
2+
import gc
3+
import sys
24
from typing import Callable
35

46
import pytest
@@ -16,12 +18,39 @@ async def coro(val: int) -> None:
1618

1719
ret = await asyncio.gather(*coros, return_exceptions=True)
1820

19-
check_lru(coro, hits=2, misses=1, cache=1, tasks=0)
21+
check_lru(coro, hits=2, misses=1, cache=0, tasks=0)
2022

2123
for item in ret:
2224
assert isinstance(item, ZeroDivisionError)
2325

2426
with pytest.raises(ZeroDivisionError):
2527
await coro(1)
2628

27-
check_lru(coro, hits=2, misses=2, cache=1, tasks=0)
29+
check_lru(coro, hits=2, misses=2, cache=0, tasks=0)
30+
31+
32+
@pytest.mark.xfail(
33+
reason="Memory leak is not fixed for PyPy3.9",
34+
condition=sys.implementation.name == "pypy",
35+
)
36+
async def test_alru_exception_reference_cleanup(check_lru: Callable[..., None]) -> None:
37+
class CustomClass:
38+
...
39+
40+
@alru_cache()
41+
async def coro(val: int) -> None:
42+
_ = CustomClass() # object we are verifying not to leak
43+
1 / 0
44+
45+
coros = [coro(v) for v in range(1000)]
46+
47+
await asyncio.gather(*coros, return_exceptions=True)
48+
49+
check_lru(coro, hits=0, misses=1000, cache=0, tasks=0)
50+
51+
await asyncio.sleep(0.00001)
52+
gc.collect()
53+
54+
assert (
55+
len([obj for obj in gc.get_objects() if isinstance(obj, CustomClass)]) == 0
56+
), "Only objects in the cache should be left in memory."

0 commit comments

Comments
 (0)