Extra Methods in Fleche

When you decorate a function with @fleche, it’s not just a cached version of the original function. It also gains several helper methods that allow you to interact with the cache and the function’s metadata directly.

[5]:
from fleche import fleche
import time
[6]:
@fleche
def slow_add(a, b):
    """Adds two numbers slowly."""
    time.sleep(1)
    return a + b
() ()

1. .call(*args, **kwargs)

The .call method returns a Call object. This object captures all the information about a potential call to the function, including the function name, arguments, and metadata, without actually executing the function.

[7]:
call_info = slow_add.call(10, b=20)
print(f"Function Name: {call_info.name}")
print(f"Arguments: {call_info.arguments}")
print(f"Call Object: {call_info}")
Function Name: slow_add
Arguments: {'a': 10, 'b': 20}
Call Object: Call(name='slow_add', arguments={'a': 10, 'b': 20}, metadata={}, module='__main__', version=None, code_digest=None, result=None)

2. .digest(*args, **kwargs)

The .digest method returns the unique cache key (a SHA-256 hash) that fleche uses to identify this specific call in the cache. This is derived from the function name, arguments, and other metadata.

[8]:
key = slow_add.digest(10, b=20)
print(f"Cache Key: {key}")
Cache Key: 44a75ca3f502c3f7d86c9cb057840f8e1d07494e69f16f3e396bb4ae2a9d9a3e

3. .contains(*args, **kwargs)

You can use .contains to check if a result for a specific set of arguments is already present in the cache, without triggering the function execution.

[9]:
print(f"Is (10, 20) in cache? {slow_add.contains(10, b=20)}")

# Now we run it to populate the cache
slow_add(10, b=20)

print(f"Is (10, 20) in cache now? {slow_add.contains(10, b=20)}")
Is (10, 20) in cache? False
Is (10, 20) in cache now? True

4. .load(*args, **kwargs)

The .load method allows you to explicitly retrieve a result from the cache. If the result is not in the cache, it will raise a KeyError instead of executing the function.

[10]:
result = slow_add.load(10, b=20)
print(f"Loaded result: {result}")

try:
    slow_add.load(1, 2)
except KeyError:
    print("Result for (1, 2) not in cache.")
Loaded result: 30
Result for (1, 2) not in cache.

5. .rerun(*args, **kwargs)

The .rerun method allows you to force a re-execution of the function, even if the result is already in the cache. It will save the new result to the cache, and recursively force any nested @fleche calls to re-execute as well.

[11]:
start = time.time()
res = slow_add.rerun(10, b=20)
end = time.time()
print(f"Rerun Result: {res} (took {end - start:.2f} seconds forcing execution)")

# subsequent calls will be fast again
start = time.time()
res2 = slow_add(10, b=20)
end = time.time()
print(f"Normal call Result: {res2} (took {end - start:.2f} seconds)")
Rerun Result: 30 (took 1.00 seconds forcing execution)
Normal call Result: 30 (took 0.00 seconds)

6. .__wrapped__

Because fleche uses functools.wraps, the original undecorated function is always available via the .__wrapped__ attribute. This is useful if you want to bypass the cache entirely. Unlike .rerun, calling the .__wrapped__ function does not save the result to the cache, nor does it force nested cached functions to re-execute.

[12]:
start = time.time()
res = slow_add.__wrapped__(10, 20)
end = time.time()
print(f"Result: {res} (took {end - start:.2f} seconds bypassing cache)")
Result: 30 (took 1.00 seconds bypassing cache)

Gotcha: Using Helpers with Decorated Methods

When @fleche is applied to a class method, the helper methods do not automatically bind self the way a normal method call would. obj.method.query is the same unbound function as MyClass.method.query — no partial application of self=obj happens.

You must pass the instance explicitly as the first positional argument (or as self=):

class MyClass:
    def __init__(self, val):
        self.val = val

    def __digest__(self):
        from fleche.digest import Digest
        return Digest(str(self.val))

    @fleche
    def compute(self, x):
        return self.val + x

obj = MyClass(10)
obj.compute(5)  # populates cache

# Correct — pass self explicitly
obj.compute.contains(obj, x=5)  # True
obj.compute.digest(obj, x=5)    # cache key string
obj.compute.query(self=obj, x=5)  # also works as keyword

# Incorrect — raises TypeError: missing argument 'self'
# obj.compute.contains(x=5)