ProvenPin Install

Boltons retry decorator and cached property helpers under pytest: working versions (tested 2026-10-16)

A signature-preserving retry decorator composed with boltons.funcutils.wraps, plus cacheutils cachedproperty and cachedmethod memoization tested offline with pytest.

passed6 passed · 0 failed2026-10-16 00:01 UTC
Status
passed
Last passed
2026-10-16 00:01 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions2
boltons@26.2.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38826982604

The code

Exactly the files that passed. Your agent can pull them with get_recipe("boltons-funcutils-retry") (install), or see them on GitHub.

requirements.txt
boltons==26.2.0
pytest==9.1.1
helpers.py
import time

from boltons.cacheutils import cachedmethod, cachedproperty
from boltons.funcutils import wraps


class RetryError(Exception):
    """Raised when a retried callable exhausts every attempt."""


def retry(attempts=3, retry_on=Exception, delay=0.0, sleep=time.sleep):
    """Signature-preserving retry decorator built on boltons.funcutils.wraps.

    Calls the wrapped function up to *attempts* times, retrying only when an
    exception matching *retry_on* is raised. The final failure is re-raised as
    a RetryError whose __cause__ is the last original exception.
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last = None
            for attempt in range(attempts):
                try:
                    return func(*args, **kwargs)
                except retry_on as exc:
                    last = exc
                    if attempt < attempts - 1:
                        sleep(delay)
            raise RetryError(f"{func.__name__} failed after {attempts} attempts") from last
        return wrapper
    return decorator


class Rollup:
    """In-memory aggregator showing boltons cachedproperty and cachedmethod."""

    def __init__(self, values):
        self.values = list(values)
        self.calls = {"total": 0, "bucket": 0}
        self._buckets = {}

    @cachedproperty
    def total(self):
        self.calls["total"] += 1
        return sum(self.values)

    @cachedmethod("_buckets")
    def bucket(self, width):
        self.calls["bucket"] += 1
        return self.total // width


class Slotted:
    __slots__ = ("x",)

    def __init__(self, x):
        self.x = x

    @cachedproperty
    def doubled(self):
        return self.x * 2
tests/test_helpers.py
import inspect

import pytest

from helpers import RetryError, Rollup, Slotted, retry


def test_retry_retries_until_success():
    calls = []

    @retry(attempts=3)
    def flaky():
        calls.append(1)
        if len(calls) < 3:
            raise OSError("transient")
        return "ok"

    assert flaky() == "ok"
    assert len(calls) == 3


def test_retry_exhausts_and_chains_cause():
    @retry(attempts=2)
    def always_fails():
        raise OSError("down")

    with pytest.raises(RetryError) as excinfo:
        always_fails()
    assert isinstance(excinfo.value.__cause__, OSError)


def test_retry_preserves_signature_via_wraps():
    @retry(attempts=1)
    def add(x, y=0):
        return x + y

    assert str(inspect.signature(add)) == "(x, y=0)"


def test_cachedproperty_caches_and_delattr_invalidates():
    r = Rollup([1, 2, 3])

    assert r.total == 6
    assert r.total == 6
    assert r.calls["total"] == 1

    del r.total
    assert r.total == 6
    assert r.calls["total"] == 2


def test_cachedproperty_needs_instance_dict():
    s = Slotted(4)
    with pytest.raises(AttributeError):
        _ = s.doubled


def test_cachedmethod_is_per_instance_and_needs_hashable_args():
    a, b = Rollup([10, 10]), Rollup([10, 10])

    assert a.bucket(4) == 5
    assert a.bucket(4) == 5
    assert a.calls["bucket"] == 1
    assert b.calls["bucket"] == 0

    b.bucket(4)
    assert b.calls["bucket"] == 1
    assert len(a._buckets) == 1 and len(b._buckets) == 1

    with pytest.raises(TypeError):
        a.bucket([4])
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.