From f48a27c9623cfb80bb77d425af99780f4d46a567 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sat, 8 Aug 2026 19:07:08 -0700 Subject: [PATCH] fix(core): stop `key in cache` falling back to the legacy sequence protocol ProgramCacheResource documents, at length, that it deliberately has no __contains__: There is intentionally no ``__contains__``: the obvious ``if key in cache: data = cache[key]`` idiom is racy across processes ... and exposing ``__contains__`` invites that pattern. But the class defines __getitem__ and neither __iter__ nor __contains__, so CPython falls back to the legacy sequence-iteration protocol: `key in cache` becomes `cache[0], cache[1], ...` compared against the *values*. On the shipped backends that surfaces as a baffling error from a lookup the user never wrote: b"k" in InMemoryProgramCache() TypeError: cache keys must be bytes or str, got int and `list(cache)` / `dict(cache)` fail the same way. Worse, ProgramCacheResource is public and meant to be subclassed. For a backend whose __getitem__ accepts integers -- a list-backed cache, say -- the fallback answers silently and inverted: b"v" in cache -> True # b"v" is a VALUE b"k" in cache -> False # b"k" IS a key Set ``__iter__ = None``, the standard way to opt out of that protocol. `in` and iteration now raise the plain ``TypeError: argument of type 'InMemoryProgramCache' is not iterable``, which points at the real mistake, and the documented design actually holds. Everything else is untouched: __getitem__, get(), len(), clear(), update() with a mapping or with pairs, and the context-manager form all behave exactly as before. update() iterates its argument, not self. --- .../cuda/core/utils/_program_cache/_abc.py | 13 ++++++++ cuda_core/docs/source/release/1.2.0-notes.rst | 8 +++++ cuda_core/tests/test_program_cache.py | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/cuda_core/cuda/core/utils/_program_cache/_abc.py b/cuda_core/cuda/core/utils/_program_cache/_abc.py index 82233540eb3..b9289ad0494 100644 --- a/cuda_core/cuda/core/utils/_program_cache/_abc.py +++ b/cuda_core/cuda/core/utils/_program_cache/_abc.py @@ -111,8 +111,21 @@ class ProgramCacheResource(abc.ABC): ``__contains__`` invites that pattern. ``get`` answers both questions in one filesystem-level operation, so a successful return always carries the bytes. + + ``key in cache`` therefore raises ``TypeError``, and a cache + is not iterable. """ + # Opt out of the legacy sequence-iteration protocol. Defining + # ``__getitem__`` without ``__iter__`` makes ``key in cache`` fall back to + # ``cache[0], cache[1], ...`` compared against the *values* -- which + # defeats the "no ``__contains__``" design above: on these backends it + # raises a baffling "cache keys must be bytes or str, got int", and on a + # subclass whose ``__getitem__`` accepts integers it silently answers about + # values instead of keys. ``__iter__ = None`` restores the plain + # ``TypeError: argument of type '...' is not iterable``. + __iter__ = None + @abc.abstractmethod def __getitem__(self, key: bytes | str) -> bytes: """Retrieve the cached binary bytes. diff --git a/cuda_core/docs/source/release/1.2.0-notes.rst b/cuda_core/docs/source/release/1.2.0-notes.rst index 120d2c2a253..ac993a9d636 100644 --- a/cuda_core/docs/source/release/1.2.0-notes.rst +++ b/cuda_core/docs/source/release/1.2.0-notes.rst @@ -73,6 +73,14 @@ Fixes and enhancements Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted. (`#2439 `__) +- ``key in cache`` on a :class:`~cuda.core.utils.ProgramCacheResource` now + raises ``TypeError`` instead of walking the cache as a legacy sequence. + The class deliberately provides no ``__contains__``, but defining + ``__getitem__`` without ``__iter__`` made ``in`` fall back to + ``cache[0], cache[1], ...`` compared against the values. Use + :meth:`~cuda.core.utils.ProgramCacheResource.get`, which answers presence and + retrieval in one operation. + Deprecation Notices ------------------- diff --git a/cuda_core/tests/test_program_cache.py b/cuda_core/tests/test_program_cache.py index a8d3fc85f7e..6ae16fbbe1a 100644 --- a/cuda_core/tests/test_program_cache.py +++ b/cuda_core/tests/test_program_cache.py @@ -38,6 +38,37 @@ def test_program_cache_resource_requires_core_methods(): assert "__contains__" not in ProgramCacheResource.__abstractmethods__ +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize("factory", ["inmemory", "filestream"]) +def test_program_cache_is_not_iterable_and_rejects_in(tmp_path, factory): + """The ABC documents having no ``__contains__``, but defining + ``__getitem__`` without ``__iter__`` handed ``key in cache`` to the legacy + sequence protocol: it walked ``cache[0], cache[1], ...`` and compared + against the *values*. On these backends that surfaced as a baffling + "cache keys must be bytes or str, got int"; on a subclass whose + ``__getitem__`` accepts integers it silently answered about values instead + of keys. + """ + from cuda.core.utils import FileStreamProgramCache, InMemoryProgramCache + + cache = InMemoryProgramCache() if factory == "inmemory" else FileStreamProgramCache(tmp_path / "fc") + with cache: + cache[b"k"] = b"v" + + with pytest.raises(TypeError, match="not iterable"): + b"k" in cache # noqa: B015 + with pytest.raises(TypeError, match="not iterable"): + iter(cache) + with pytest.raises(TypeError, match="not iterable"): + list(cache) + + # The supported lookups are unaffected. + assert cache[b"k"] == b"v" + assert cache.get(b"k") == b"v" + assert cache.get(b"absent") is None + assert len(cache) == 1 + + def _build_empty_subclass(): from cuda.core.utils import ProgramCacheResource