Skip to content

Commit

Permalink
Fix cache key/0 (#50)
Browse files Browse the repository at this point in the history
* Update gitignore

* Add module name to the default prefix

* Sort keyword arguments by name

* Fix kwargs quoting
  • Loading branch information
kri-k authored May 3, 2021
1 parent fd91bbe commit 1e1d0ce
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 31 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ dist
django_cache_memoize.egg-info
.venv
.vscode
.idea
13 changes: 10 additions & 3 deletions src/cache_memoize/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from functools import wraps
import itertools

import hashlib
from urllib.parse import quote
Expand Down Expand Up @@ -99,11 +100,17 @@ def noop(*args):
def decorator(func):
def _default_make_cache_key(*args, **kwargs):
cache_key = ":".join(
[quote(str(x)) for x in args_rewrite(*args)]
+ [quote("{}={}".format(k, v)) for k, v in kwargs.items()]
itertools.chain(
(quote(str(x)) for x in args_rewrite(*args)),
(
"{}={}".format(quote(k), quote(str(v)))
for k, v in sorted(kwargs.items())
),
)
)
prefix_ = prefix or ".".join((func.__module__ or "", func.__qualname__))
return hashlib.md5(
force_bytes("cache_memoize" + (prefix or func.__qualname__) + cache_key)
force_bytes("cache_memoize" + prefix_ + cache_key)
).hexdigest()

_make_cache_key = key_generator_callable or _default_make_cache_key
Expand Down
12 changes: 12 additions & 0 deletions tests/dummy_package/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import functools


def dummy_decorator():
def _decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)

return wrapper

return _decorator
43 changes: 43 additions & 0 deletions tests/dummy_package/a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from cache_memoize import cache_memoize

from . import dummy_decorator


@cache_memoize(None)
def func():
pass


@cache_memoize(None)
@dummy_decorator()
def decorated_func():
pass


@cache_memoize(None)
def another_decorated_func():
pass


def func_factory():
@cache_memoize(None)
def func():
pass

return func


class DummyClass:
@cache_memoize(None)
def func(self):
pass

@cache_memoize(None)
@dummy_decorator()
def decorated_func(self):
pass

@cache_memoize(None)
@dummy_decorator()
def another_decorated_func(self):
pass
25 changes: 25 additions & 0 deletions tests/dummy_package/b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from cache_memoize import cache_memoize

from . import dummy_decorator


@cache_memoize(None)
def func():
pass


@cache_memoize(None)
@dummy_decorator()
def decorated_func():
pass


class DummyClass:
@cache_memoize(None)
def func(self):
pass

@cache_memoize(None)
@dummy_decorator()
def decorated_func(self):
pass
127 changes: 99 additions & 28 deletions tests/test_cache_memoize.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

from cache_memoize import cache_memoize

from .dummy_package import a as dummy_a
from .dummy_package import b as dummy_b


def test_the_setup():
"""If this doesn't work, the settings' CACHES isn't working."""
Expand All @@ -19,26 +22,55 @@ def test_cache_memoize():
calls_made = []

@cache_memoize(10)
def runmeonce(a, b, k="bla"):
calls_made.append((a, b, k))
return "{} {} {}".format(a, b, k) # sample implementation
def runmeonce(a, b, k1="bla", k2=None):
calls_made.append((a, b, k1, k2))
return "{} {} {} {}".format(a, b, k1, k2) # sample implementation

runmeonce(1, 2)
runmeonce(1, 2)
assert len(calls_made) == 1
runmeonce(1, 3)
assert len(calls_made) == 2
# should work with most basic types
# Should work with most basic types
runmeonce(1.1, "foo")
runmeonce(1.1, "foo")
assert len(calls_made) == 3
# even more "advanced" types
runmeonce(1.1, "foo", k=list("åäö"))
runmeonce(1.1, "foo", k=list("åäö"))
# Even more "advanced" types
runmeonce(1.1, "foo", k1=list("åäö"))
runmeonce(1.1, "foo", k1=list("åäö"))
assert len(calls_made) == 4
# And shouldn't be a problem even if the arguments are really long
runmeonce("A" * 200, "B" * 200, {"C" * 100: "D" * 100})
assert len(calls_made) == 5
# The order of the keyword arguments doesn't matter
runmeonce(1, 2, k1=3, k2=4)
runmeonce(1, 2, k2=4, k1=3)
assert len(calls_made) == 6


@pytest.mark.parametrize(
("obj_1", "obj_2"),
[
# Check identically named entities from different modules
(dummy_a.func, dummy_b.func),
(dummy_a.decorated_func, dummy_b.decorated_func),
(dummy_a.DummyClass().func, dummy_b.DummyClass().func),
(dummy_a.DummyClass().decorated_func, dummy_b.DummyClass().decorated_func),
#
# Check identically named entities from different scopes
(dummy_a.func, dummy_a.DummyClass().func),
(dummy_a.func, dummy_a.func_factory()),
#
# Check decorated entities
(dummy_a.decorated_func, dummy_a.another_decorated_func),
(
dummy_a.DummyClass().decorated_func,
dummy_a.DummyClass().another_decorated_func,
),
],
)
def test_default_prefix_uniqueness(obj_1, obj_2):
assert obj_1.get_cache_key() != obj_2.get_cache_key()


def test_prefixes():
Expand Down Expand Up @@ -75,25 +107,62 @@ def returnnothing(a, b, k="bla"):
assert len(calls_made) == 1


@pytest.mark.parametrize(
"bits", [("a", "b", "c"), ("ä", "á", "ö"), ("ë".encode(), b"\02", b"i")]
)
def test_colons(bits):
calls_made = []
class TestDefaultCacheKeyQuoting:
@pytest.mark.parametrize(
"bits", [("a", "b", "c"), ("ä", "á", "ö"), ("ë".encode(), b"\02", b"i")]
)
def test_colons_quoting(self, bits):
calls_made = []

@cache_memoize(10)
def fun(a, b, k="bla"):
calls_made.append((a, b, k))
return (a, b, k)

sep = ":"
if isinstance(bits[0], bytes):
sep = sep.encode()
a1, a2 = (sep.join(bits[:2]), bits[2])
b1, b2 = (bits[0], sep.join(bits[1:]))
fun(a1, a2)
fun(b1, b2)
assert len(calls_made) == 2

@pytest.mark.parametrize(
("arguments_1", "arguments_2"),
[
(
(("a", "b", "c"), {}),
(("a:b:c",), {}),
),
(
(("a", "b"), {"c": "d"}),
(("a",), {"b:c": "d"}),
),
(
(("a",), {"b": "c"}),
(("a", "b=c"), {}),
),
(
((), {"a": "b=c"}),
((), {"a=b": "c"}),
),
],
)
def test_general_quoting(self, arguments_1, arguments_2):
calls_made = []

@cache_memoize(10)
def fun(a, b, k="bla"):
calls_made.append((a, b, k))
return (a, b, k)

sep = ":"
if isinstance(bits[0], bytes):
sep = sep.encode()
a1, a2 = (sep.join(bits[:2]), bits[2])
b1, b2 = (bits[0], sep.join(bits[1:]))
fun(a1, a2)
fun(b1, b2)
assert len(calls_made) == 2
@cache_memoize(10)
def fun(*args, **kwargs):
calls_made.append((args, kwargs))

args, kwargs = arguments_1
fun(*args, **kwargs)

args, kwargs = arguments_2
fun(*args, **kwargs)

assert calls_made == [arguments_1, arguments_2]


def test_cache_memoize_hit_miss_callables():
Expand Down Expand Up @@ -192,7 +261,9 @@ def function_2(a):
# If you set the prefix, you can cross wire functions.
# Note sure why you'd ever want to do this though

@cache_memoize(10, prefix=function_2.__qualname__)
@cache_memoize(
10, prefix=".".join((function_2.__module__, function_2.__qualname__))
)
def function_3(a):
raise Exception

Expand Down Expand Up @@ -248,8 +319,8 @@ def test_get_cache_key():
def funky(argument):
pass

assert funky.get_cache_key(100) == "f0b86356861e088e2058855e95ee8981"
assert funky.get_cache_key(100, _refresh=True) == "f0b86356861e088e2058855e95ee8981"
assert funky.get_cache_key(100) == "d33e7fad5d1d04da8e588a9ee348644a"
assert funky.get_cache_key(100, _refresh=True) == "d33e7fad5d1d04da8e588a9ee348644a"


def test_cache_memoize_custom_alias():
Expand Down

0 comments on commit 1e1d0ce

Please sign in to comment.