From ba3aa6774b93ec1287f92c7843c05c810631aa82 Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Fri, 14 Aug 2026 00:44:25 +0100 Subject: [PATCH 1/2] core: gracefully reject unsupported annotations --- src/cachew/marshall/cachew.py | 5 +++-- src/cachew/tests/test_cachew.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/cachew/marshall/cachew.py b/src/cachew/marshall/cachew.py index beecdf7..567034a 100644 --- a/src/cachew/marshall/cachew.py +++ b/src/cachew/marshall/cachew.py @@ -377,15 +377,16 @@ def build_schema(Type) -> Schema: if is_dictish: (ft, tt) = args fts = build_schema(ft) + if not isinstance(fts, SPrimitive): + raise TypeNotSupported(type_=Type, reason='dictionary key type must be primitive') tts = build_schema(tt) - assert isinstance(fts, SPrimitive) return SDict( type=origin, ft=fts, tt=tts, ) - raise RuntimeError(f"unsupported: {Type=} {origin=} {args=}") + raise TypeNotSupported(type_=Type, reason=f'generic type with origin {origin} is unsupported') ######### tests diff --git a/src/cachew/tests/test_cachew.py b/src/cachew/tests/test_cachew.py index abcb751..a61d57b 100644 --- a/src/cachew/tests/test_cachew.py +++ b/src/cachew/tests/test_cachew.py @@ -307,6 +307,38 @@ def fun2() -> Iterable[UGood | UBad]: yield UGood(x=2) +def test_unsupported_annotation_follows_error_policy( + *, + tmp_path: Path, + restore_settings, +) -> None: + """ + Unsupported annotations must not bypass Cachew's strict or defensive setup policy. + Strict mode must reject decoration without running the source, while defensive mode must return the original function and leave it uncached. + """ + + source_calls = 0 + + def source() -> Iterator[Callable[[int], str]]: + nonlocal source_calls + source_calls += 1 + yield str + + cache_path = tmp_path / 'cache' + settings.THROW_ON_ERROR = True + with pytest.raises(CachewException, match='failed to infer cache type'): + cachew(cache_path=cache_path)(source) + assert source_calls == 0 + + settings.THROW_ON_ERROR = False + decorated = cachew(cache_path=cache_path)(source) + assert decorated is source + assert list(decorated()) == [str] + assert list(decorated()) == [str] + assert source_calls == 2 + assert cache_path.exists() is False + + class BB(NamedTuple): xx: int yy: int From ed5d0c517592face45ef98d19e2e11edce01b537 Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Fri, 14 Aug 2026 00:48:59 +0100 Subject: [PATCH 2/2] chore: fix ty failure after ty update --- src/cachew/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cachew/__init__.py b/src/cachew/__init__.py index 8383e30..95f46b9 100644 --- a/src/cachew/__init__.py +++ b/src/cachew/__init__.py @@ -31,7 +31,7 @@ def orjson_dumps(*args: Any, **kwargs: Any) -> bytes: # type: ignore[misc] # sqlite needs a blob return json.dumps(*args, **kwargs).encode('utf8') - orjson_loads = json.loads # type: ignore[assignment] # ty: ignore[invalid-assignment] + orjson_loads = json.loads # type: ignore[assignment] import platformdirs