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