Skip to content

Commit aa9eba6

Browse files
committed
Add support for deleting cached views when is set to query_params=True. Fixes #243
1 parent 09bd86f commit aa9eba6

7 files changed

Lines changed: 359 additions & 30 deletions

File tree

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ Unreleased
1919
- Send Signals for cache hits and misses. :pr:`#237` and :pr:`667`
2020
- Include ``key_prefix`` when building ``@cached(query_string=True)`` cache
2121
keys. :issue:`302`
22+
- Add ``Cache.delete_cached()`` and extend ``make_cache_key()`` with ``path`` and ``query_args``
23+
arguments to make deleting views decorated with ``cached(query_string=True)`` possible.
24+
:issue:`243`
2225
- Use ``hashlib.sha256`` instead of ``hashlib.md5`` for hashing the cache keys.
2326
This changes the generated keys, so entries cached by an earlier version become
2427
obsolete. If you wish to still use hashlib.md5 set the config

docs/api.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ Cache API
1515
.. module:: flask_caching
1616
.. autoclass:: Cache
1717
:members: cache, init_app, get, set, add, delete, get_many, set_many,
18-
delete_many, get_dict, unlink, has, clear, cached, memoize,
19-
delete_memoized, delete_memoized_verhash
18+
delete_many, get_dict, unlink, has, clear, cached, delete_cached,
19+
memoize, delete_memoized, delete_memoized_verhash
2020

2121

2222
.. autoclass:: CachedResponse

docs/usage.rst

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,52 @@ a subclass of `flask.Response`::
4343
``@route`` decorator, and not the result of your view function.
4444

4545

46+
Deleting Cached Views
47+
`````````````````````
48+
49+
When you want to remove the value of a cached view you can use :meth:`~Cache.delete_cached`
50+
instead of :meth:`~Cache.delete`. :meth:`~Cache.delete_cached`. builds the key of the
51+
decorated view and deletes the cache in one go::
52+
53+
@app.route("/user/<name>")
54+
@cache.cached(timeout=50)
55+
def user(name):
56+
return render_template("user.html", name=name)
57+
58+
cache.delete_cached(user, "/user/Fred")
59+
60+
Outside of a request context the view doesn't know which request was used to build
61+
the cache key. So to make it work outside of a request context you have to pass the
62+
``path``. You can also use the view function and the named arguments of the view
63+
in which case the path is built using ``url_for()``::
64+
65+
cache.delete_cached(user, name="bob")
66+
67+
When you cache views with ``query_string=True`` you also have to pass the query string/args
68+
because otherwise the cache key cannot be built::
69+
70+
@app.route("/works")
71+
@cache.cached(timeout=50, query_string=True)
72+
def works():
73+
return do_search(request.args)
74+
75+
cache.delete_cached(works, "/works", "limit=15&mock=true")
76+
77+
You can use either pass the query string as a string, a mapping or an iterable of
78+
``(key, value)`` pairs::
79+
80+
cache.delete_cached(works, "/works", {"limit": 15, "mock": "true"})
81+
82+
Additionally, ``path`` and ``query_args`` are also supported by the views ``make_cache_key()``::
83+
84+
key = works.make_cache_key(path="/works", query_args={"limit": 15})
85+
86+
.. note::
87+
88+
If you view has arguments named ``path`` or ``query_args`` you have to build the key
89+
from via the request context!
90+
91+
4692
Caching Pluggable View Classes
4793
------------------------------
4894

@@ -168,7 +214,7 @@ every time this information is needed you might do something like the following:
168214

169215

170216

171-
Deleting memoize cache
217+
Deleting Memoize Cache
172218
``````````````````````
173219

174220
.. versionadded:: 0.2
@@ -384,6 +430,9 @@ string instead of ``key_prefix``. The arguments are sorted before hashing, so
384430
def search():
385431
return do_search(request.args)
386432

433+
Deleting such an entry needs the query string as well, see
434+
`Deleting cached views`_.
435+
387436

388437
response_hit_indication
389438
```````````````````````
@@ -468,7 +517,8 @@ Considering we have ``render_form_field`` and ``render_submit`` macros::
468517
Clearing Cache
469518
--------------
470519

471-
See :meth:`~Cache.clear`.
520+
See :meth:`~Cache.clear`. To delete the entry of a single view see
521+
`Deleting cached views`_.
472522

473523
Here's an example script to empty your application's cache:
474524

src/flask_caching/__init__.py

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import warnings
1818
from collections import OrderedDict
1919
from collections.abc import Callable
20+
from collections.abc import Iterable
2021
from typing import Any
2122
from typing import cast
2223
from typing import Concatenate
@@ -26,7 +27,6 @@
2627
from typing import TypeAlias
2728
from typing import TypeVar
2829

29-
from blinker import Namespace
3030
from cachelib.serializers import BaseSerializer
3131
from flask import current_app
3232
from flask import Flask
@@ -36,19 +36,24 @@
3636
from flask import url_for
3737
from werkzeug.utils import import_string
3838

39-
from flask_caching.backends.base import BaseCache
40-
from flask_caching.backends.simplecache import SimpleCache
41-
from flask_caching.utils import function_namespace
42-
from flask_caching.utils import get_arg_default
43-
from flask_caching.utils import get_arg_names
44-
from flask_caching.utils import get_id
45-
from flask_caching.utils import join_generator
46-
from flask_caching.utils import make_template_fragment_key as make_template_fragment_key
47-
from flask_caching.utils import wants_args
39+
from .backends.base import BaseCache
40+
from .backends.simplecache import SimpleCache
41+
from .signals import cache_memoize_hit as cache_memoize_hit
42+
from .signals import cache_memoize_miss as cache_memoize_miss
43+
from .signals import cache_view_hit as cache_view_hit
44+
from .signals import cache_view_miss as cache_view_miss
45+
from .utils import _QueryArgs
46+
from .utils import function_namespace
47+
from .utils import get_arg_default
48+
from .utils import get_arg_names
49+
from .utils import get_id
50+
from .utils import join_generator
51+
from .utils import make_template_fragment_key as make_template_fragment_key
52+
from .utils import query_args_as_pairs
53+
from .utils import wants_args
4854

4955
logger = logging.getLogger(__name__)
5056

51-
_signals = Namespace()
5257

5358
# The initial version timeout of a memoize version key. Will be overwritten on the first
5459
# write with the memoize value timeout
@@ -63,6 +68,7 @@
6368
hashlib.md5,
6469
]
6570

71+
6672
P = ParamSpec("P")
6773
# The parameters left over after ``__get__`` binds the instance.
6874
P2 = ParamSpec("P2")
@@ -73,11 +79,6 @@
7379
T_co = TypeVar("T_co", covariant=True)
7480
T_contra = TypeVar("T_contra", contravariant=True)
7581

76-
cache_view_hit = _signals.signal("cache-view-hit")
77-
cache_view_miss = _signals.signal("cache-view-miss")
78-
cache_memoize_hit = _signals.signal("cache-memoize-hit")
79-
cache_memoize_miss = _signals.signal("cache-memoize-miss")
80-
8182

8283
class _BoundCachedFunction(Protocol[T_contra, P, T_co]):
8384
"""The type of a :meth:`Cache.cached` method accessed on an instance."""
@@ -160,6 +161,10 @@ def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...
160161
"_MemoizedFunction[..., Any] | _BoundMemoizedFunction[Any, ..., Any]"
161162
)
162163

164+
_AnyCachedFunction: TypeAlias = (
165+
"_CachedFunction[..., Any] | _BoundCachedFunction[Any, ..., Any]"
166+
)
167+
163168

164169
class CachedResponse(Response):
165170
"""
@@ -463,6 +468,20 @@ def get_list():
463468
464469
readable and writable
465470
471+
Outside of a request context, pass ``path`` to build the
472+
key for a given ``request.path`` and, for
473+
``query_string=True``, ``query_args`` to build the key for
474+
a given query string::
475+
476+
key = view.make_cache_key(
477+
path="/works", query_args="limit=15&mock=true"
478+
)
479+
cache.delete(key)
480+
481+
``query_args`` accepts a query string, a mapping or an
482+
iterable of ``(key, value)`` pairs. See
483+
:meth:`delete_cached` for the shorthand.
484+
466485
:param timeout: Default None. If set to an integer, will cache for that
467486
amount of time. Unit of time is in seconds.
468487
@@ -636,9 +655,20 @@ def default_make_cache_key(*args: Any, **kwargs: Any) -> str:
636655
kwargs[arg_name] = arg
637656

638657
use_request = kwargs.pop("use_request", False)
639-
return _make_cache_key(args, kwargs, use_request=use_request)
658+
path = kwargs.pop("path", None)
659+
query_args = kwargs.pop("query_args", None)
660+
return _make_cache_key(
661+
args,
662+
kwargs,
663+
use_request=use_request,
664+
path=path,
665+
query_args=query_args,
666+
)
640667

641-
def _make_cache_key_query_string() -> str:
668+
def _make_cache_key_query_string(
669+
path: str | None = None,
670+
query_args: _QueryArgs | None = None,
671+
) -> str:
642672
"""Create consistent keys for query string arguments.
643673
644674
Produces the same cache key regardless of argument order, e.g.,
@@ -657,9 +687,12 @@ def _make_cache_key_query_string() -> str:
657687
# are the same, regardless of the order in which they are
658688
# provided.
659689

660-
args_as_sorted_tuple = tuple(
661-
sorted(pair for pair in request.args.items(multi=True))
662-
)
690+
if query_args is None:
691+
pairs: Iterable[tuple[str, str]] = request.args.items(multi=True)
692+
else:
693+
pairs = query_args_as_pairs(query_args)
694+
695+
args_as_sorted_tuple = tuple(sorted(pair for pair in pairs))
663696
# ... now hash the sorted (key, value) tuple so it can be
664697
# used as a key for cache. Turn them into bytes so that the
665698
# hash function will accept them
@@ -678,26 +711,37 @@ def _make_cache_key_query_string() -> str:
678711
if callable(key_prefix):
679712
cache_key = key_prefix()
680713
elif "%s" in key_prefix:
681-
cache_key = key_prefix % request.path
714+
cache_key = key_prefix % (request.path if path is None else path)
682715
else:
683716
cache_key = key_prefix
684717

685718
return cache_key + cache_hash
686719

687720
def _make_cache_key(
688-
args: tuple[Any, ...], kwargs: dict[str, Any], use_request: bool
721+
args: tuple[Any, ...],
722+
kwargs: dict[str, Any],
723+
use_request: bool,
724+
path: str | None = None,
725+
query_args: _QueryArgs | None = None,
689726
) -> str:
690727
if query_string:
691-
return _make_cache_key_query_string()
728+
return _make_cache_key_query_string(path, query_args)
692729
else:
693730
cache_key: str
694731
if callable(key_prefix):
695732
cache_key = key_prefix()
696733
elif "%s" in key_prefix:
697-
if use_request:
734+
if path is not None:
735+
cache_key = key_prefix % path
736+
elif use_request:
698737
cache_key = key_prefix % request.path
699738
else:
700-
cache_key = key_prefix % url_for(f.__name__, **kwargs)
739+
# Outside of a request context ``url_for``
740+
# defaults to an external URL, which would not
741+
# match the key the request stored.
742+
cache_key = key_prefix % url_for(
743+
f.__name__, _external=False, **kwargs
744+
)
701745
else:
702746
cache_key = key_prefix
703747

@@ -720,6 +764,39 @@ def _make_cache_key(
720764

721765
return decorator
722766

767+
def delete_cached(
768+
self,
769+
f: _AnyCachedFunction,
770+
path: str | None = None,
771+
query_args: _QueryArgs | None = None,
772+
**kwargs: Any,
773+
) -> bool:
774+
"""Delete the cached value of a :meth:`cached` decorated function.
775+
776+
Example::
777+
778+
@app.route("/works")
779+
@cache.cached(query_string=True)
780+
def view_works():
781+
return do_search(request.args)
782+
783+
cache.delete_cached(view_works, "/works", {"limit": 15})
784+
785+
If you are calling this outside of a request context pass ``path`` and
786+
when the function was decorated with ``query_string=True`` you also
787+
have to pass ``query_args``.
788+
789+
:param f: The decorated function whose cached value to delete.
790+
:param path: The ``request.path`` the value was cached for.
791+
:param query_args: The query string the value was cached for, as a
792+
query string, a mapping or an iterable of
793+
``(key, value)`` pairs. Only used when the function
794+
was decorated with ``query_string=True``.
795+
:param kwargs: The view arguments, passed to ``url_for()`` to build
796+
the path when ``path`` is not given.
797+
"""
798+
return self.delete(f.make_cache_key(path=path, query_args=query_args, **kwargs))
799+
723800
def _memvname(self, funcname: str) -> str:
724801
return funcname + "_memver"
725802

src/flask_caching/signals.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
flask_caching.signals
3+
~~~~~~~~~~~~~~~~~~~~~
4+
5+
The signals for Flask-Caching.
6+
7+
:copyright: (c) 2026 by Peter Justin
8+
:license: BSD, see LICENSE for more details.
9+
"""
10+
11+
from blinker import Namespace
12+
13+
_signals = Namespace()
14+
15+
#: Sent when a view decorated with :meth:`~Cache.cached` is served from the
16+
#: cache. It is passed ``cache``, the :class:`Cache` instance, ``cache_key``,
17+
#: the key the response was found under, and ``args`` and ``kwargs``, the
18+
#: arguments the view was called with.
19+
cache_view_hit = _signals.signal("cache-view-hit")
20+
21+
#: Sent when a view decorated with :meth:`~Cache.cached` is not found in the
22+
#: cache and has to be called. It is passed the same arguments as
23+
#: :data:`cache_view_hit`.
24+
cache_view_miss = _signals.signal("cache-view-miss")
25+
26+
#: Sent when a function decorated with :meth:`~Cache.memoize` is served from
27+
#: the cache. In addition to the arguments passed to :data:`cache_view_hit`,
28+
#: it is passed ``f``, the undecorated function.
29+
cache_memoize_hit = _signals.signal("cache-memoize-hit")
30+
31+
#: Sent when a function decorated with :meth:`~Cache.memoize` is not found in
32+
#: the cache and has to be called. It is passed the same arguments as
33+
#: :data:`cache_memoize_hit`.
34+
cache_memoize_miss = _signals.signal("cache-memoize-miss")

0 commit comments

Comments
 (0)