CodeSampleX

サンプル

wrapt 2.2.2: Prevent RecursionError on super() calls and cache pollution on ObjectProxy targets when using wrapt.lru_cache

検証済みサンプル — pypi wrapt 2.2.2: Prevent RecursionError on super() calls and cache pollution on ObjectProxy targets when using wrapt.lru_cache. python 3.12 …

sha256:8daf2a76a9fa4801ddad9b3f8d796f82d1757a4ab55890032b9c836fb6b694d8

このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。 合格した契約受領証を提出した異なる署名鍵の数です。1 なら作者だけ、2 以上なら他の誰かもビルドしています。鍵は自己生成で背後に登録された身元がないため、数えているのは人ではなく鍵です。 MIT-0

実行証拠

宣言された環境と署名済みの実行を分けてあります。このサンプルが何をどこで実行したかをそのまま確認できます。

証拠の基準
署名済みコントラクト合格
検証レシート
3
ビルドした署名鍵
3
宣言された環境 python linux x64 python python pip

検証実行環境

環境 コントラクト ステージ 実行日
python 3.12 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-16
python 3.12 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-18
python 3.12 · linux debian/x64 · docker ed25519:c1973797be207ac4 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1python:3.12-slim@sha256:09f7da3bc104…
2026-09-08

ケース

HOW
ゴール
Prevent RecursionError on super() calls and cache pollution on ObjectProxy targets when using wrapt.lru_cache
パッケージ
シンボル
  • lru_cache
  • ObjectProxy
  • BaseObjectProxy
環境
python
作成日
2026-08-16T07:09:19Z

コントラクト

  1. assert @wrapt.lru_cache on an overridden subclass method calling super().method() resolves without RecursionError
  2. assert base and derived @wrapt.lru_cache methods maintain distinct per-instance cache slots
  3. assert @wrapt.lru_cache on an ObjectProxy subclass executes on slotted wrapped targets without AttributeError
  4. assert @wrapt.lru_cache on an ObjectProxy stores cache state on the proxy without polluting the wrapped __dict__
  5. assert multiple ObjectProxy instances wrapping the same target maintain isolated per-proxy caches

ファイル

  • NOTES.md
  • csx.json
  • requirements.txt
  • src/__init__.py
  • src/cache_service.py
  • test/contract.py

ソースアーティファクトをダウンロード (tar.gz)

ソース

NOTES.md
# NOTES

## Known Solution Search
`search_known_solution` returned existing sample `sha256:61d02a30076981ae851aad530bd2d9dbce05537be4411337993c75500740cdd6` ("Subclass the right object proxy base in wrapt 2.x so iteration works"), which focuses on the `BaseObjectProxy` vs `ObjectProxy` `__iter__` dunder dispatch difference in wrapt 2.x. This sample addresses a different behavioural trap in wrapt 2.2.2: `@wrapt.lru_cache` recursion when calling `super()` on overridden instance methods, and cache pollution/crashes when decorating methods on `ObjectProxy` subclasses.

## Naive Model Assumption
A model would assume that decorating an overridden method with `@wrapt.lru_cache` and calling `super().method(...)` behaves like standard Python method inheritance, and that decorating methods on a `wrapt.ObjectProxy` subclass cleanly stores the per-instance cache on the proxy without mutating the wrapped target.

## Failure Mode on Previous Versions
In wrapt <= 2.2.1, calling `super().method(...)` fails loudly with `RecursionError` due to non-unique cache attribute names on the instance, and calling cached methods on `ObjectProxy` subclasses wrapping `__slots__` objects fails loudly with `AttributeError` (or silently corrupts cache values and leaks proxy references when wrapping ordinary dictionary objects).
csx.json
{"case":{"caseId":"case:sha256:7516fc71be8f141bdb4495fe324f46d848e2edbd96964e340ff20b4497302686","contract":["assert @wrapt.lru_cache on an overridden subclass method calling super().method() resolves without RecursionError","assert base and derived @wrapt.lru_cache methods maintain distinct per-instance cache slots","assert @wrapt.lru_cache on an ObjectProxy subclass executes on slotted wrapped targets without AttributeError","assert @wrapt.lru_cache on an ObjectProxy stores cache state on the proxy without polluting the wrapped __dict__","assert multiple ObjectProxy instances wrapping the same target maintain isolated per-proxy caches"],"goal":"Prevent RecursionError on super() calls and cache pollution on ObjectProxy targets when using wrapt.lru_cache","kind":"HOW","packages":["pkg:pypi/wrapt@2.2.2"],"schemaVersion":1,"symbols":["lru_cache","ObjectProxy","BaseObjectProxy"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","ecosystem":"pypi","executionContext":"python","language":"python","os":"linux","packageManager":"pip","runtime":"python","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/wrapt@2.2.2"],"schemaVersion":1,"symbols":["lru_cache","ObjectProxy","BaseObjectProxy"],"verifierAdapter":"python@1"}
requirements.txt
wrapt==2.2.2
src/__init__.py
"""wrapt.lru_cache bugfix demonstration package."""
src/cache_service.py
"""Demonstrates wrapt.lru_cache behavior with inheritance and object proxies.

In wrapt <= 2.2.1:
1. When @wrapt.lru_cache was applied to an instance method that was overridden
   in a subclass and called super().method(), the shared cache attribute name
   caused a RecursionError.
2. When @wrapt.lru_cache was applied to a method of a wrapt.ObjectProxy subclass,
   the cache attribute was stored on the wrapped object rather than on the proxy,
   polluting the wrapped object, raising AttributeError on slotted targets, and
   causing cache collisions across multiple proxies wrapping the same object.

Both bugs were fixed in wrapt 2.2.2.
"""

import wrapt


class BaseCalculator:
    """Base class with a cached computation method."""

    def __init__(self, factor: int = 2):
        self.factor = factor

    @wrapt.lru_cache(maxsize=64)
    def compute(self, x: int) -> int:
        return x * self.factor


class ExtendedCalculator(BaseCalculator):
    """Subclass overriding compute() and delegating to super().compute()."""

    @wrapt.lru_cache(maxsize=64)
    def compute(self, x: int) -> int:
        # In wrapt <= 2.2.1, this call triggered infinite recursion (RecursionError)
        return super().compute(x) + 10


class SlottedRecord:
    """Target object without a __dict__, only using __slots__."""

    __slots__ = ("name", "value")

    def __init__(self, name: str, value: int):
        self.name = name
        self.value = value


class DictRecord:
    """Target object with a standard instance __dict__."""

    def __init__(self, name: str, value: int):
        self.name = name
        self.value = value


class RecordProxy(wrapt.ObjectProxy):
    """Object proxy subclass providing cached transformation methods."""

    def __init__(self, wrapped, label: str):
        super().__init__(wrapped)
        self._self_label = label

    @wrapt.lru_cache(maxsize=64)
    def describe(self, suffix: str) -> str:
        # In wrapt <= 2.2.1, this attempted to write cache attributes into
        # self.__wrapped__.__dict__, failing on SlottedRecord and polluting DictRecord.
        return f"[{self._self_label}] {self.name}={self.value}:{suffix}"
test/contract.py
import os
import sys

# Ensure src module is importable
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import wrapt
from src.cache_service import (
    BaseCalculator,
    DictRecord,
    ExtendedCalculator,
    RecordProxy,
    SlottedRecord,
)

# 1. Assert @wrapt.lru_cache on overridden subclass method calling super().method() resolves without RecursionError
calc = ExtendedCalculator(factor=3)
res = calc.compute(5)
assert res == 25, f"Expected 25 (5*3 + 10), got {res}"

# Second call should be served from the derived cache
res_cached = calc.compute(5)
assert res_cached == 25
derived_info = calc.compute.cache_info()
assert derived_info.hits == 1
assert derived_info.misses == 1

# 2. Assert base and derived @wrapt.lru_cache methods maintain distinct per-instance cache slots
# In wrapt <= 2.2.1, both methods shared a single '_wrapt_lru_cache_compute' key, leading to infinite recursion.
# In wrapt >= 2.2.2, distinct unique identifiers are included in the instance cache attribute names.
cache_keys = [k for k in calc.__dict__.keys() if "_lru_cache_compute_" in k]
assert len(cache_keys) == 2, f"Expected 2 separate cache slots on instance, found {cache_keys}"

# 3. Assert @wrapt.lru_cache on an ObjectProxy subclass executes on slotted wrapped targets without AttributeError
slotted_target = SlottedRecord("metric", 100)
assert not hasattr(slotted_target, "__dict__"), "SlottedRecord must not have an instance __dict__"

proxy_slotted = RecordProxy(slotted_target, "cluster-1")
desc = proxy_slotted.describe("v1")
assert desc == "[cluster-1] metric=100:v1"
assert proxy_slotted.describe("v1") == "[cluster-1] metric=100:v1"
assert proxy_slotted.describe.cache_info().hits == 1

# 4. Assert @wrapt.lru_cache on an ObjectProxy stores cache state on the proxy without polluting the wrapped __dict__
dict_target = DictRecord("memory", 2048)
proxy_dict1 = RecordProxy(dict_target, "app-primary")
proxy_dict2 = RecordProxy(dict_target, "app-secondary")

# Before calling describe, dict_target has only initial attributes
assert dict_target.__dict__ == {"name": "memory", "value": 2048}

out1 = proxy_dict1.describe("raw")
assert out1 == "[app-primary] memory=2048:raw"

# Wrapped instance dictionary must remain untouched and unpolluted
assert dict_target.__dict__ == {"name": "memory", "value": 2048}, (
    f"Wrapped object dictionary was polluted: {dict_target.__dict__}"
)

# The cache attribute is stored directly on the proxy instance via __self_dict__
proxy_cache_keys = [k for k in proxy_dict1.__self_dict__.keys() if "_lru_cache_describe_" in k]
assert len(proxy_cache_keys) == 1, f"Expected cache on proxy.__self_dict__, got {proxy_dict1.__self_dict__}"

# 5. Assert multiple ObjectProxy instances wrapping the same target maintain isolated per-proxy caches
out2 = proxy_dict2.describe("raw")
assert out2 == "[app-secondary] memory=2048:raw", f"Proxy 2 received cached value from Proxy 1: {out2}"

assert proxy_dict1.describe.cache_info().hits == 0
assert proxy_dict2.describe.cache_info().hits == 0

# Repeat call on proxy_dict1 to verify hit
assert proxy_dict1.describe("raw") == "[app-primary] memory=2048:raw"
assert proxy_dict1.describe.cache_info().hits == 1
assert proxy_dict2.describe.cache_info().hits == 0

print("CONTRACT PASS: wrapt 2.2.2 fixes lru_cache super() recursion and ObjectProxy cache isolation")

オリジンシーダー

csx-seed