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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ Note that 0.3.3 was never released; 0.3.4 follows 0.3.2.
entry that expires while the route runs.
([#178](https://github.com/allen0099/FastAPI-CacheX/issues/178))

- **`MemoryBackend.clear_pattern()` is case-sensitive on Windows.** It used
`fnmatch.fnmatch`, which folds case through `os.path.normcase` on Windows, so
`cache:user:*` also cleared `cache:User:1`. It now uses `fnmatch.fnmatchcase`,
like Redis on every platform. The backends docs list where the glob syntax
still differs from Redis.
([#179](https://github.com/allen0099/FastAPI-CacheX/issues/179))

## [0.3.7] - 2026-09-25

### Added
Expand Down
6 changes: 6 additions & 0 deletions docs/BACKENDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ BackendProxy.set(backend)
> The in-memory cache is not suitable for production with multiple processes.
> Each process maintains its own separate cache.

`clear_pattern()` matches whole keys case-sensitively on every platform, like Redis.
The glob syntax is Python's `fnmatch`, which differs from Redis in two places: negate
a character class with `[!...]` (Redis uses `[^...]`), and escape a special character
by putting it in brackets, as in `[*]` (Redis also accepts `\*`). `*`, `?` and
`[abc]` behave the same on both.

## Redis

Install the extra with `uv add "fastapi-cachex[redis]"`.
Expand Down
4 changes: 3 additions & 1 deletion fastapi_cachex/backends/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,16 @@ async def clear_pattern(self, pattern: str) -> int:
Matches ``fnmatch`` against the whole key, which is what the Redis
backend's SCAN does. Matching only the path component, as this used to,
made the same call clear different things on different backends.
Matching is case-sensitive on every platform, as on Redis; plain
``fnmatch.fnmatch`` folds case on Windows.

Args:
pattern: A glob pattern to match whole cache keys against

Returns:
Number of cache entries cleared
"""
cleared_count = await self._evict(lambda key: fnmatch.fnmatch(key, pattern))
cleared_count = await self._evict(lambda key: fnmatch.fnmatchcase(key, pattern))
warn_if_path_shaped(pattern, cleared_count)
logger.debug(
"Memory cache CLEAR_PATTERN; pattern=%s removed=%s", pattern, cleared_count
Expand Down
2 changes: 2 additions & 0 deletions i18n/zh-TW/docs/BACKENDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ BackendProxy.set(backend)
> [!NOTE]
> 記憶體快取不適合用於多行程的正式環境。每個行程都各自維護獨立的快取。

`clear_pattern()` 在所有平台上都以區分大小寫的方式比對完整的鍵,與 Redis 相同。萬用字元語法採用 Python 的 `fnmatch`,與 Redis 有兩處不同:否定字元類別要寫 `[!...]`(Redis 為 `[^...]`);跳脫特殊字元要放進中括號,例如 `[*]`(Redis 另外也接受 `\*`)。`*`、`?` 與 `[abc]` 在兩者上的行為相同。

## Redis {#redis}

以 `uv add "fastapi-cachex[redis]"` 安裝此 extra。
Expand Down
16 changes: 16 additions & 0 deletions tests/backends/test_memory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import asyncio
import ntpath
import os
import time
from collections.abc import Awaitable
from collections.abc import Callable
Expand Down Expand Up @@ -285,6 +287,20 @@ async def test_memory_backend_clear_pattern_separator_less_keys(
assert await memory_backend.get("cache:post:789") == value3


@pytest.mark.asyncio
async def test_memory_backend_clear_pattern_is_case_sensitive_on_windows(
memory_backend: MemoryBackend, monkeypatch: pytest.MonkeyPatch
):
"""fnmatch.fnmatch folds case through os.path.normcase on Windows (#179)."""
monkeypatch.setattr(os.path, "normcase", ntpath.normcase)
value = CacheEntry(fingerprint="e1", content=b"v1")
await memory_backend.set("cache:User:1", value)

assert await memory_backend.clear_pattern("cache:user:*") == 0
assert await memory_backend.get("cache:User:1") == value
assert await memory_backend.clear_pattern("cache:User:*") == 1


@pytest.mark.asyncio
async def test_memory_backend_clear_path_with_colon_in_path(
memory_backend: MemoryBackend,
Expand Down
Loading