Skip to content

Commit 40c09be

Browse files
committed
- Now the cached function can accept a function as cache, allowing it to replace cachedmethod function.
Also this can give you more control. - `cachedmethod` decorator deprected due to [#35](#35)
1 parent 68b2578 commit 40c09be

File tree

18 files changed

+155
-69
lines changed

18 files changed

+155
-69
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## v5.1.0 - 2025-10-31
9+
### Changed
10+
- Now the `cached` function can accept a function as `cache`, allowing it to replace `cachedmethod` function.
11+
Also this can give you more control.
12+
13+
### Deprecated
14+
- `cachedmethod` decorator deprected due to [#35](https://github.com/awolverp/cachebox/issues/35)
15+
16+
### Thanks
17+
- Special thanks to [@liblaf](https://github.com/liblaf)
18+
819
## v5.0.4 - 2025-10-20
920
### Changed
1021
- Support all platforms for Python 3.14, 3.14t and 3.13t (#34; thanks to @chirizxc)

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cachebox"
3-
version = "5.0.4"
3+
version = "5.1.0"
44
edition = "2021"
55
description = "The fastest memoizing and caching Python library written in Rust"
66
readme = "README.md"

python/cachebox/utils.py

Lines changed: 74 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,14 @@ def make_typed_key(args: tuple, kwds: dict):
267267

268268
def _cached_wrapper(
269269
func,
270-
cache: BaseCacheImpl,
270+
cache: typing.Union[BaseCacheImpl, typing.Callable],
271271
key_maker: typing.Callable[[tuple, dict], typing.Hashable],
272272
clear_reuse: bool,
273273
callback: typing.Optional[typing.Callable[[int, typing.Any, typing.Any], typing.Any]],
274274
copy_level: int,
275275
is_method: bool,
276276
):
277+
is_method = cache_is_function = inspect.isfunction(cache)
277278
_key_maker = (lambda args, kwds: key_maker(args[1:], kwds)) if is_method else key_maker
278279

279280
hits = 0
@@ -287,11 +288,12 @@ def _wrapped(*args, **kwds):
287288
if kwds.pop("cachebox__ignore", False):
288289
return func(*args, **kwds)
289290

291+
_cache = cache(args[0]) if cache_is_function else cache
290292
key = _key_maker(args, kwds)
291293

292294
# try to get result from cache
293295
try:
294-
result = cache[key]
296+
result = _cache[key]
295297
except KeyError:
296298
pass
297299
else:
@@ -310,7 +312,7 @@ def _wrapped(*args, **kwds):
310312
raise cached_error
311313

312314
try:
313-
result = cache[key]
315+
result = _cache[key]
314316
hits += 1
315317
event = EVENT_HIT
316318
except KeyError:
@@ -323,7 +325,7 @@ def _wrapped(*args, **kwds):
323325
raise e
324326

325327
else:
326-
cache[key] = result
328+
_cache[key] = result
327329
misses += 1
328330
event = EVENT_MISS
329331

@@ -332,34 +334,39 @@ def _wrapped(*args, **kwds):
332334

333335
return _copy_if_need(result, level=copy_level)
334336

335-
_wrapped.cache = cache
337+
if not cache_is_function:
338+
_wrapped.cache = cache
339+
_wrapped.cache_info = lambda: CacheInfo(
340+
hits, misses, cache.maxsize, len(cache), cache.capacity()
341+
)
342+
336343
_wrapped.callback = callback
337-
_wrapped.cache_info = lambda: CacheInfo(
338-
hits, misses, cache.maxsize, len(cache), cache.capacity()
339-
)
340344

341-
def cache_clear() -> None:
342-
nonlocal misses, hits, locks, exceptions
343-
cache.clear(reuse=clear_reuse)
344-
misses = 0
345-
hits = 0
346-
locks.clear()
347-
exceptions.clear()
345+
if not cache_is_function:
346+
347+
def cache_clear() -> None:
348+
nonlocal misses, hits, locks, exceptions
349+
cache.clear(reuse=clear_reuse)
350+
misses = 0
351+
hits = 0
352+
locks.clear()
353+
exceptions.clear()
348354

349-
_wrapped.cache_clear = cache_clear
355+
_wrapped.cache_clear = cache_clear
350356

351357
return _wrapped
352358

353359

354360
def _async_cached_wrapper(
355361
func,
356-
cache: BaseCacheImpl,
362+
cache: typing.Union[BaseCacheImpl, typing.Callable],
357363
key_maker: typing.Callable[[tuple, dict], typing.Hashable],
358364
clear_reuse: bool,
359365
callback: typing.Optional[typing.Callable[[int, typing.Any, typing.Any], typing.Any]],
360366
copy_level: int,
361367
is_method: bool,
362368
):
369+
is_method = cache_is_function = inspect.isfunction(cache)
363370
_key_maker = (lambda args, kwds: key_maker(args[1:], kwds)) if is_method else key_maker
364371

365372
hits = 0
@@ -375,11 +382,12 @@ async def _wrapped(*args, **kwds):
375382
if kwds.pop("cachebox__ignore", False):
376383
return await func(*args, **kwds)
377384

385+
_cache = cache(args[0]) if cache_is_function else cache
378386
key = _key_maker(args, kwds)
379387

380388
# try to get result from cache
381389
try:
382-
result = cache[key]
390+
result = _cache[key]
383391
except KeyError:
384392
pass
385393
else:
@@ -400,7 +408,7 @@ async def _wrapped(*args, **kwds):
400408
raise cached_error
401409

402410
try:
403-
result = cache[key]
411+
result = _cache[key]
404412
hits += 1
405413
event = EVENT_HIT
406414
except KeyError:
@@ -413,7 +421,7 @@ async def _wrapped(*args, **kwds):
413421
raise e
414422

415423
else:
416-
cache[key] = result
424+
_cache[key] = result
417425
misses += 1
418426
event = EVENT_MISS
419427

@@ -424,21 +432,25 @@ async def _wrapped(*args, **kwds):
424432

425433
return _copy_if_need(result, level=copy_level)
426434

427-
_wrapped.cache = cache
435+
if not cache_is_function:
436+
_wrapped.cache = cache
437+
_wrapped.cache_info = lambda: CacheInfo(
438+
hits, misses, cache.maxsize, len(cache), cache.capacity()
439+
)
440+
428441
_wrapped.callback = callback
429-
_wrapped.cache_info = lambda: CacheInfo(
430-
hits, misses, cache.maxsize, len(cache), cache.capacity()
431-
)
432442

433-
def cache_clear() -> None:
434-
nonlocal misses, hits, locks, exceptions
435-
cache.clear(reuse=clear_reuse)
436-
misses = 0
437-
hits = 0
438-
locks.clear()
439-
exceptions.clear()
443+
if not cache_is_function:
440444

441-
_wrapped.cache_clear = cache_clear
445+
def cache_clear() -> None:
446+
nonlocal misses, hits, locks, exceptions
447+
cache.clear(reuse=clear_reuse)
448+
misses = 0
449+
hits = 0
450+
locks.clear()
451+
exceptions.clear()
452+
453+
_wrapped.cache_clear = cache_clear
442454

443455
return _wrapped
444456

@@ -456,7 +468,8 @@ def cached(
456468
Wraps a function to automatically cache and retrieve its results based on input parameters.
457469
458470
Args:
459-
cache (BaseCacheImpl, dict, optional): Cache implementation to store results. Defaults to FIFOCache.
471+
cache (BaseCacheImpl, dict, callable): Cache implementation to store results. Defaults to FIFOCache.
472+
Can be a function that got `self` and should return cache.
460473
key_maker (Callable, optional): Function to generate cache keys from function arguments. Defaults to make_key.
461474
clear_reuse (bool, optional): Whether to reuse cache during clearing. Defaults to False.
462475
callback (Callable, optional): Function called on cache hit/miss events. Defaults to None.
@@ -465,7 +478,7 @@ def cached(
465478
Returns:
466479
Callable: Decorated function with caching capabilities.
467480
468-
Example::
481+
Example for functions::
469482
470483
@cachebox.cached(cachebox.LRUCache(128))
471484
def sum_as_string(a, b):
@@ -476,15 +489,29 @@ def sum_as_string(a, b):
476489
assert len(sum_as_string.cache) == 1
477490
sum_as_string.cache_clear()
478491
assert len(sum_as_string.cache) == 0
492+
493+
Example for methods::
494+
495+
class A:
496+
def __init__(self, num):
497+
self.num = num
498+
self._cache = cachebox.FIFOCache(0)
499+
500+
@cachebox.cached(lambda self: self._cache)
501+
def method(self, n):
502+
return self.num * n
503+
504+
instance = A(10)
505+
assert A.method(2) == 20
479506
"""
480507
if cache is None:
481508
cache = FIFOCache(0)
482509

483510
if type(cache) is dict:
484511
cache = FIFOCache(0, cache)
485512

486-
if not isinstance(cache, BaseCacheImpl):
487-
raise TypeError("we expected cachebox caches, got %r" % (cache,))
513+
if not isinstance(cache, BaseCacheImpl) and not inspect.isfunction(cache):
514+
raise TypeError("we expected cachebox caches or function, got %r" % (cache,))
488515

489516
def decorator(func: FT) -> FT:
490517
if inspect.iscoroutinefunction(func):
@@ -509,6 +536,9 @@ def cachedmethod(
509536
copy_level: int = 1,
510537
) -> typing.Callable[[FT], FT]:
511538
"""
539+
**This function is deperecated due to issue [#35](https://github.com/awolverp/cachebox/issues/35)**.
540+
Use `cached` method instead.
541+
512542
Decorator to create a method-specific memoized cache for function results.
513543
514544
Similar to `cached()`, but ignores `self` parameter when generating cache keys.
@@ -523,6 +553,14 @@ def cachedmethod(
523553
Returns:
524554
Callable: Decorated method with method-specific caching capabilities.
525555
"""
556+
import warnings
557+
558+
warnings.warn(
559+
"cachedmethod is deprecated, use cached instead. see issue https://github.com/awolverp/cachebox/issues/35",
560+
DeprecationWarning,
561+
stacklevel=2,
562+
)
563+
526564
if cache is None:
527565
cache = FIFOCache(0)
528566

python/tests/test_utils.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from cachebox import (
22
Frozen,
33
LRUCache,
4+
TTLCache,
45
cached,
56
make_typed_key,
67
make_key,
7-
cachedmethod,
88
EVENT_HIT,
99
EVENT_MISS,
1010
is_cached,
@@ -151,22 +151,25 @@ class TestCachedMethod:
151151
def __init__(self, num) -> None:
152152
self.num = num
153153

154-
@cachedmethod(None)
154+
@cached(None)
155155
def method(self, char: str):
156156
assert type(self) is TestCachedMethod
157157
return char * self.num
158158

159159
cls = TestCachedMethod(10)
160160
assert cls.method("a") == ("a" * 10)
161161

162+
cls = TestCachedMethod(2)
163+
assert cls.method("a") == ("a" * 2)
164+
162165

163166
@pytest.mark.asyncio
164167
async def test_async_cachedmethod():
165168
class TestCachedMethod:
166169
def __init__(self, num) -> None:
167170
self.num = num
168171

169-
@cachedmethod(LRUCache(0))
172+
@cached(LRUCache(0))
170173
async def method(self, char: str):
171174
assert type(self) is TestCachedMethod
172175
return char * self.num
@@ -302,3 +305,19 @@ def new(num: int):
302305

303306
a = MyClass.new(1)
304307
assert isinstance(a, int) and a == 1
308+
309+
310+
def test_new_cached_method():
311+
class Test:
312+
def __init__(self, num) -> None:
313+
self.num = num
314+
self._cache = TTLCache(20, 10)
315+
316+
@cached(lambda self: self._cache)
317+
def method(self, char: str):
318+
assert type(self) is Test
319+
return char * self.num
320+
321+
for i in range(10):
322+
cls = Test(i)
323+
assert cls.method("a") == ("a" * i)

src/bridge/cache.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use crate::common::ObservedIterator;
33
use crate::common::PreHashObject;
44

55
#[cfg_attr(Py_3_9, pyo3::pyclass(module = "cachebox._core", frozen))]
6-
#[cfg_attr(not(Py_3_9), pyo3::pyclass(module = "cachebox._core", frozen, immutable_type))]
6+
#[cfg_attr(
7+
not(Py_3_9),
8+
pyo3::pyclass(module = "cachebox._core", frozen, immutable_type)
9+
)]
710
pub struct Cache {
811
raw: crate::common::Mutex<crate::policies::nopolicy::NoPolicy>,
912
}

src/bridge/fifocache.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use crate::common::ObservedIterator;
33
use crate::common::PreHashObject;
44

55
#[cfg_attr(Py_3_9, pyo3::pyclass(module = "cachebox._core", frozen))]
6-
#[cfg_attr(not(Py_3_9), pyo3::pyclass(module = "cachebox._core", frozen, immutable_type))]
6+
#[cfg_attr(
7+
not(Py_3_9),
8+
pyo3::pyclass(module = "cachebox._core", frozen, immutable_type)
9+
)]
710
pub struct FIFOCache {
811
raw: crate::common::Mutex<crate::policies::fifo::FIFOPolicy>,
912
}

src/bridge/lfucache.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use crate::common::ObservedIterator;
33
use crate::common::PreHashObject;
44

55
#[cfg_attr(Py_3_9, pyo3::pyclass(module = "cachebox._core", frozen))]
6-
#[cfg_attr(not(Py_3_9), pyo3::pyclass(module = "cachebox._core", frozen, immutable_type))]
6+
#[cfg_attr(
7+
not(Py_3_9),
8+
pyo3::pyclass(module = "cachebox._core", frozen, immutable_type)
9+
)]
710
pub struct LFUCache {
811
raw: crate::common::Mutex<crate::policies::lfu::LFUPolicy>,
912
}

src/bridge/lrucache.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use crate::common::ObservedIterator;
33
use crate::common::PreHashObject;
44

55
#[cfg_attr(Py_3_9, pyo3::pyclass(module = "cachebox._core", frozen))]
6-
#[cfg_attr(not(Py_3_9), pyo3::pyclass(module = "cachebox._core", frozen, immutable_type))]
6+
#[cfg_attr(
7+
not(Py_3_9),
8+
pyo3::pyclass(module = "cachebox._core", frozen, immutable_type)
9+
)]
710
pub struct LRUCache {
811
raw: crate::common::Mutex<crate::policies::lru::LRUPolicy>,
912
}

0 commit comments

Comments
 (0)