cachebox

repository·main·Indexed 19 days ago

https://github.com/awolverp/cachebox

A high-performance Python caching and memoizing library written in Rust. It provides thread-safe caching for synchronous and asynchronous functions via the @cachebox.cached decorator and supports multiple eviction policies including LRU, LFU, FIFO, RR, TTL, and VTTL. Key features include automatic copying of mutable objects to prevent side effects, proactive memory reclamation via background sweeper threads, and various key generation strategies.

Tokens
13.1K
Snippets
46
Records
62
Agent score
64%

What's inside cachebox

  1. Key Features of Cachebox

    main

    Cachebox provides several advantages for high-scale Python applications:

    • Performance: 10–50x faster than other caching libraries.
    • Memory Efficiency: Consumes approximately 50% of the memory used by a standard Python dictionary.
    • Concurrency: All operations are thread-safe via internal locking.
    • Simplicity: Zero Python dependencies (written in Rust).
    • Rich Feature Set: Includes 7 caching algorithms, TTL (Time-To-Live) support, decorators, and callbacks.
    • Compatibility: Supports Python 3.10+ on both CPython and PyPy.
  2. Changes to `__eq__` error handling and cache equality (v4 → v5)

    main

    Two significant changes occurred in version 5 regarding how caches behave during comparisons and error handling:

    1. Error Propagation: In v4, errors raised inside a custom __eq__ method were swallowed and converted to KeyError. In v5, these errors (e.g., NotImplementedError) propagate normally.
    2. Order Independence: In v4, cache equality was order-dependent. In v5, cache equality follows standard dictionary semantics, meaning two caches with the same keys and values are considered equal regardless of insertion order.
    # v5: Cache equality is now dict-like (order-independent)
    c1 = cachebox.FIFOCache(10)
    c2 = cachebox.FIFOCache(10)
    
    c1.insert(1, 'a'); c1.insert(2, 'b')
    c2.insert(2, 'b'); c2.insert(1, 'a')
    
    print(c1 == c2)  # Returns True in v5
  3. Understand TTL behavior in Frozen caches

    main

    Using the Frozen wrapper does not prevent TTL (Time-To-Live) expiration in TTLCache or VTTLCache. Items will still expire naturally according to their TTL even if the cache is frozen.

    from cachebox import Frozen, TTLCache
    import time
    
    cache  = TTLCache(0, ttl=1, iterable={1: "a"})
    frozen = Frozen(cache)
    time.sleep(1)
    print(len(frozen))  # 0 — expired despite being frozen
  4. Avoid mutation side effects with cachebox

    main

    Unlike functools.lru_cache or other standard Python caching libraries, cachebox automatically copies mutable objects like dict, list, and set. This prevents accidental mutation of cached values when the caller modifies the returned object.

    @cachebox.cached(cachebox.LRUCache(maxsize=128))
    def make_dict(name: str, age: int) -> dict:
       return {"name": name, "age": age}
    
    d = make_dict("cachebox", 10)
    d["new-key"] = "new-value"  # This mutation does NOT affect the cached version
    
    d2 = make_dict("cachebox", 10)
    assert d2 == {"name": "cachebox", "age": 10}  # d2 remains clean
  5. Prevent Cache Stampedes with @cached

    main

    A cache stampede occurs when multiple concurrent requests for the same missing key trigger redundant computations. The @cachebox.cached decorator prevents this by default using a per-key lock (lock=True).

    • Sync functions: Uses threading.Lock.
    • Async functions: Uses asyncio.Lock automatically.
    • Custom locks: You can pass any object implementing contextlib.AbstractContextManager (sync) or AbstractAsyncContextManager (async).

    Warning: Passing a synchronous lock to an async function (or vice versa) raises a TypeError at decoration time.

    When to disable the lock (lock=False or lock=None):

    • Recursive functions: The default non-reentrant lock will cause a deadlock if a cached function calls itself.
    • Cheap computations: If recomputing is nearly free, the lock overhead might not be worth it.
    • Single-threaded environments: No concurrency means no stampedes.
    • Already-serialized callers: If your architecture already guarantees single-caller access per key.

    Note: Disabling the lock does not make the cache unsafe; internal Rust mutexes still protect reads and writes. It only allows multiple threads to compute the same missing value simultaneously.

    import threading
    import cachebox
    
    # Using a re-entrant lock (RLock) to allow recursion
    @cachebox.cached(cachebox.LRUCache(maxsize=256), lock=threading.RLock)
    def factorial(n: int) -> int:
        return 1 if n <= 1 else n * factorial(n - 1)
    
    # Disabling the lock for cheap computations
    @cachebox.cached(cachebox.LRUCache(maxsize=256), lock=False)
    def cheap_func(x: int):
        return x * 2
  6. Quickstart with Cachebox

    main

    Cachebox is a high-performance Python caching library written in Rust. You can use it to cache the results of expensive functions using the @cachebox.cached decorator. This allows you to specify a cache strategy (like LRUCache) and a maxsize to control memory usage. Once a function is decorated, the first call executes the function body, and subsequent calls with the same arguments are served instantly from the cache.

    import cachebox
    
    # Use the @cachebox.cached decorator with a specific cache strategy
    @cachebox.cached(cachebox.LRUCache(maxsize=128))
    def get_user(user_id: int) -> dict:
        # This represents an expensive operation like a DB query
        return db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    # First call executes the function
    user = get_user(42)
    
    # Subsequent calls with the same ID are served from cache instantly
    user = get_user(42)
  7. Bypass the cache for a single call

    main

    To execute a cached function without reading from or writing to the cache for a specific invocation, pass cachebox__ignore=True as a keyword argument to the function call. This does not affect subsequent calls.

    import cachebox
    
    @cachebox.cached(cachebox.LRUCache(128))
    def add(a, b):
        print("computing...")
        return a + b
    
    add(1, 2)  # computing...
    add(1, 2)  # returned from cache
    
    # Bypass cache for this call
    add(1, 2, cachebox__ignore=True)
    # computing...
  8. Persist a cache to disk using pickle

    main

    You can save and load cache instances using Python's pickle module.

    import cachebox, pickle
    
    cache = cachebox.LRUCache(100, {i: i for i in range(50)})
    
    # Save to disk
    with open("cache.pkl", "wb") as f:
        pickle.dump(cache, f)
    
    # Load from disk
    with open("cache.pkl", "rb") as f:
        loaded = pickle.load(f)
    
    assert cache == loaded
  9. Migrate `@cached` from `copy_level` to `postprocess` (v5 → v6)

    main

    In version 6, the copy_level parameter in the @cachebox.cached decorator has been deprecated and no longer has any effect. To achieve similar behavior or gain more control over how results are handled, use the postprocess parameter with cachebox.postprocess_copy.

    # v6 replacement for copy_level
    @cachebox.cached(cachebox.RRCache(10), postprocess=cachebox.postprocess_copy)
    def add(a: int, b: int) -> dict:
        return {a: b}
  10. Copy a cache instance

    main

    All cache classes support Python's copy module for both shallow and deep copying.

    import cachebox
    import copy
    
    cache = cachebox.LRUCache(100, {i: i for i in range(10)})
    
    shallow = copy.copy(cache)       # shallow copy
    deep    = copy.deepcopy(cache)   # deep copy
  11. Replace `maxmemory` with `getsizeof` for weighted caching (v5 → v6)

    main

    The maxmemory parameter has been removed in version 6 to prioritize performance. To implement memory-based or size-based limits, use the getsizeof parameter. This parameter accepts a callable that computes the size of a key-value pair.

    As a result of this change, the .memory property has also been removed. Use .current_size() and .remaining_size() instead to inspect cache capacity.

    import sys
    
    # Define a size calculator
    def getsizeof(key, val):
        return sys.getsizeof(key) + sys.getsizeof(val)
    
    # Use getsizeof instead of maxmemory
    cache = cachebox.LRUCache(maxsize=1000, getsizeof=getsizeof)
    
    # Access size information via methods
    print(cache.current_size())
    print(cache.remaining_size())
  12. Install cachebox via pip or uv

    main

    You can install cachebox using standard Python package managers. cachebox has zero Python dependencies, and the Rust extension is provided as a pre-built wheel for all major platforms and Python versions.

    It is recommended to use virtual environments for managing your installation.

    # Using pip
    $ pip install -U cachebox
    
    # Using uv
    $ uv add cachebox