From 2246b2a839eda6448804c7bb72ae3086fec08e16 Mon Sep 17 00:00:00 2001 From: allen0099 Date: Sat, 26 Sep 2026 18:18:15 +0000 Subject: [PATCH] feat(proxy): raise ProxyNotSetError from unset manager proxies CacheManagerProxy, SessionManagerProxy and StateManagerProxy raised BackendNotFoundError from get() when no instance was set, although no backend is involved. They now raise ProxyNotSetError, a subclass of BackendNotFoundError, so existing handlers keep working. BackendProxy still raises BackendNotFoundError. The lazy dependencies catch the narrower error, and ProxyNotSetError is exported from the package. Closes #161 --- CHANGELOG.md | 6 ++++++ fastapi_cachex/__init__.py | 2 ++ fastapi_cachex/dependencies.py | 6 +++--- fastapi_cachex/exceptions.py | 9 +++++++++ fastapi_cachex/proxy.py | 12 ++++++++++- fastapi_cachex/state/dependencies.py | 4 ++-- tests/test_exports.py | 3 ++- tests/test_proxybackend.py | 30 ++++++++++++++++++++++++++++ 8 files changed, 65 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e54ae0..5def97a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,12 @@ Note that 0.3.3 was never released; 0.3.4 follows 0.3.2. - **`MemoryBackend.aclose()` stops the cleanup task and waits for it.** `stop_cleanup()` only requests cancellation and stays as it is. ([#181](https://github.com/allen0099/FastAPI-CacheX/issues/181)) +- **`ProxyNotSetError` for an unset manager proxy.** `CacheManagerProxy`, + `SessionManagerProxy` and `StateManagerProxy` raise it from `get()` when no + instance is set, instead of `BackendNotFoundError`, whose name points at a + backend that is not involved. It subclasses `BackendNotFoundError`, so + existing handlers keep catching it. `BackendProxy` is unchanged. + ([#161](https://github.com/allen0099/FastAPI-CacheX/issues/161)) ### Changed diff --git a/fastapi_cachex/__init__.py b/fastapi_cachex/__init__.py index ec89120..5a27056 100644 --- a/fastapi_cachex/__init__.py +++ b/fastapi_cachex/__init__.py @@ -14,6 +14,7 @@ from .exceptions import BackendNotFoundError as BackendNotFoundError from .exceptions import CacheXError as CacheXError from .exceptions import LockTimeoutError as LockTimeoutError +from .exceptions import ProxyNotSetError as ProxyNotSetError from .exceptions import RequestNotFoundError as RequestNotFoundError from .lock import CacheLock as CacheLock from .manager import CacheManager as CacheManager @@ -82,6 +83,7 @@ def _read_version() -> str: "FastAPICacheXSessionMiddleware", "InvalidStateError", "LockTimeoutError", + "ProxyNotSetError", "RequestNotFoundError", "Session", "SessionConfig", diff --git a/fastapi_cachex/dependencies.py b/fastapi_cachex/dependencies.py index d720ea7..bba226b 100644 --- a/fastapi_cachex/dependencies.py +++ b/fastapi_cachex/dependencies.py @@ -6,7 +6,7 @@ from fastapi import Depends from .backends.base import BaseCacheBackend -from .exceptions import BackendNotFoundError +from .exceptions import ProxyNotSetError from .manager import CacheManager from .manager_proxy import CacheManagerProxy from .proxy import BackendProxy @@ -38,7 +38,7 @@ def get_app_cache() -> CacheManager: """ try: return CacheManagerProxy.get() - except BackendNotFoundError: + except ProxyNotSetError: pass # Checked again under the lock: FastAPI runs this sync dependency in a # worker thread, so concurrent first requests would otherwise each build @@ -46,7 +46,7 @@ def get_app_cache() -> CacheManager: with _manager_lock: try: return CacheManagerProxy.get() - except BackendNotFoundError: + except ProxyNotSetError: manager = CacheManager(backend=get_backend_or_fallback()) CacheManagerProxy.set(manager) return manager diff --git a/fastapi_cachex/exceptions.py b/fastapi_cachex/exceptions.py index 552a68d..e956ba8 100644 --- a/fastapi_cachex/exceptions.py +++ b/fastapi_cachex/exceptions.py @@ -13,6 +13,15 @@ class BackendNotFoundError(CacheXError): """Exception raised when a cache backend is not found.""" +class ProxyNotSetError(BackendNotFoundError): + """Exception raised when a manager proxy has no instance set. + + Raised by ``CacheManagerProxy``, ``SessionManagerProxy`` and + ``StateManagerProxy``. It subclasses ``BackendNotFoundError``, which these + proxies raised before 0.3.8, so existing handlers keep catching it. + """ + + class RequestNotFoundError(CacheXError): """Exception raised when a request is not found.""" diff --git a/fastapi_cachex/proxy.py b/fastapi_cachex/proxy.py index 613cd46..bf5b11a 100644 --- a/fastapi_cachex/proxy.py +++ b/fastapi_cachex/proxy.py @@ -5,6 +5,7 @@ import threading import warnings from logging import getLogger +from typing import ClassVar from typing import Generic from typing import NoReturn from typing import TypeVar @@ -12,6 +13,7 @@ from .backends import BaseCacheBackend from .backends import MemoryBackend from .exceptions import BackendNotFoundError +from .exceptions import ProxyNotSetError ProxyInstance = TypeVar("ProxyInstance") @@ -35,6 +37,8 @@ class ProxyBase(Generic[ProxyInstance], metaclass=ProxyMeta): """Abstract base class for proxy classes.""" _instance: ProxyInstance | None = None + # Raised by `get()` while no instance is set. + _not_set_error: ClassVar[type[BackendNotFoundError]] = ProxyNotSetError @classmethod def get(cls) -> ProxyInstance: @@ -42,10 +46,14 @@ def get(cls) -> ProxyInstance: Returns: The current instance + + Raises: + ProxyNotSetError: If no instance is set (``BackendNotFoundError`` + for ``BackendProxy``) """ if cls._instance is None: msg = f"No instance set for proxy {cls.__name__}" - raise BackendNotFoundError(msg) + raise cls._not_set_error(msg) return cls._instance @classmethod @@ -65,6 +73,8 @@ def set(cls, instance: ProxyInstance | None) -> None: class BackendProxy(ProxyBase[BaseCacheBackend]): """FastAPI CacheX Proxy for backend management.""" + _not_set_error = BackendNotFoundError + @staticmethod def get_backend() -> BaseCacheBackend: """Get the current backend instance. diff --git a/fastapi_cachex/state/dependencies.py b/fastapi_cachex/state/dependencies.py index c462ae6..e3f3781 100644 --- a/fastapi_cachex/state/dependencies.py +++ b/fastapi_cachex/state/dependencies.py @@ -4,7 +4,7 @@ from fastapi import Depends -from fastapi_cachex.exceptions import BackendNotFoundError +from fastapi_cachex.exceptions import ProxyNotSetError from .manager import StateManager from .proxy import StateManagerProxy @@ -19,7 +19,7 @@ def get_state_manager() -> StateManager: """ try: return StateManagerProxy.get() - except BackendNotFoundError: + except ProxyNotSetError: manager = StateManager() StateManagerProxy.set(manager) return manager diff --git a/tests/test_exports.py b/tests/test_exports.py index f763b67..2a42aaf 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -7,7 +7,8 @@ @pytest.mark.parametrize( - "name", ["BackendNotFoundError", "CacheXError", "RequestNotFoundError"] + "name", + ["BackendNotFoundError", "CacheXError", "ProxyNotSetError", "RequestNotFoundError"], ) def test_core_exception_is_exported(name: str) -> None: assert name in fastapi_cachex.__all__ diff --git a/tests/test_proxybackend.py b/tests/test_proxybackend.py index fce7803..d655b70 100644 --- a/tests/test_proxybackend.py +++ b/tests/test_proxybackend.py @@ -10,6 +10,10 @@ from fastapi_cachex import cache from fastapi_cachex.backends import MemoryBackend from fastapi_cachex.exceptions import BackendNotFoundError +from fastapi_cachex.exceptions import ProxyNotSetError +from fastapi_cachex.manager_proxy import CacheManagerProxy +from fastapi_cachex.session.proxy import SessionManagerProxy +from fastapi_cachex.state.proxy import StateManagerProxy from fastapi_cachex.types import CacheEntry app = FastAPI() @@ -132,3 +136,29 @@ def test_set_backend_alias_warns_and_delegates(): with pytest.raises(BackendNotFoundError): BackendProxy.get() + + +@pytest.mark.parametrize( + "proxy", [CacheManagerProxy, SessionManagerProxy, StateManagerProxy] +) +def test_manager_proxies_raise_proxy_not_set_error(proxy) -> None: + """An unset manager proxy is not a missing backend (#161). + + `ProxyNotSetError` subclasses `BackendNotFoundError`, which these proxies + raised before, so existing handlers keep catching it. + """ + previous = proxy._instance + proxy.set(None) + try: + with pytest.raises(ProxyNotSetError, match=proxy.__name__) as exc_info: + proxy.get() + assert isinstance(exc_info.value, BackendNotFoundError) + finally: + proxy.set(previous) + + +def test_backend_proxy_still_raises_backend_not_found_error() -> None: + BackendProxy.set(None) + with pytest.raises(BackendNotFoundError) as exc_info: + BackendProxy.get() + assert not isinstance(exc_info.value, ProxyNotSetError)