Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions fastapi_cachex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -82,6 +83,7 @@ def _read_version() -> str:
"FastAPICacheXSessionMiddleware",
"InvalidStateError",
"LockTimeoutError",
"ProxyNotSetError",
"RequestNotFoundError",
"Session",
"SessionConfig",
Expand Down
6 changes: 3 additions & 3 deletions fastapi_cachex/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -38,15 +38,15 @@ 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
# and register their own manager (and fallback backend).
with _manager_lock:
try:
return CacheManagerProxy.get()
except BackendNotFoundError:
except ProxyNotSetError:
manager = CacheManager(backend=get_backend_or_fallback())
CacheManagerProxy.set(manager)
return manager
Expand Down
9 changes: 9 additions & 0 deletions fastapi_cachex/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
12 changes: 11 additions & 1 deletion fastapi_cachex/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import threading
import warnings
from logging import getLogger
from typing import ClassVar
from typing import Generic
from typing import NoReturn
from typing import TypeVar

from .backends import BaseCacheBackend
from .backends import MemoryBackend
from .exceptions import BackendNotFoundError
from .exceptions import ProxyNotSetError

ProxyInstance = TypeVar("ProxyInstance")

Expand All @@ -35,17 +37,23 @@ 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:
"""Get the current instance of the proxy.

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
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions fastapi_cachex/state/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,7 +19,7 @@ def get_state_manager() -> StateManager:
"""
try:
return StateManagerProxy.get()
except BackendNotFoundError:
except ProxyNotSetError:
manager = StateManager()
StateManagerProxy.set(manager)
return manager
Expand Down
3 changes: 2 additions & 1 deletion tests/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down
30 changes: 30 additions & 0 deletions tests/test_proxybackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Loading