Skip to content
Open
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
28 changes: 24 additions & 4 deletions diskcache/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import contextlib as cl
import errno
import functools as ft
import inspect
import io
import json
import os
Expand Down Expand Up @@ -384,18 +385,26 @@ class EmptyDirWarning(UserWarning):
"""Warning used by Cache.check for empty directories."""


def args_to_key(base, args, kwargs, typed, ignore):
def args_to_key(base, args, kwargs, typed, ignore, arg_names=()):
"""Create cache key out of function arguments.

:param tuple base: base of key
:param tuple args: function arguments
:param dict kwargs: function keyword arguments
:param bool typed: include types in cache key
:param set ignore: positional or keyword args to ignore
:param tuple arg_names: names of the wrapped function's positional
parameters, in order, so a name in `ignore` matches a positional
argument the same way it matches a keyword argument (default ())
:return: cache key tuple

"""
args = tuple(arg for index, arg in enumerate(args) if index not in ignore)
args = tuple(
arg
for index, arg in enumerate(args)
if index not in ignore
and not (index < len(arg_names) and arg_names[index] in ignore)
)
key = base + args + (None,)

if kwargs:
Expand Down Expand Up @@ -1853,7 +1862,10 @@ def memoize(
:param float expire: seconds until arguments expire
(default None, no expiry)
:param str tag: text to associate with arguments (default None)
:param set ignore: positional or keyword args to ignore (default ())
:param set ignore: positional or keyword args to ignore (default ()).
A parameter's name in this set is matched whether it was passed
positionally or by keyword; a positional index is also still
matched by position, same as before.
:return: callable decorator

"""
Expand All @@ -1865,6 +1877,14 @@ def decorator(func):
"""Decorator created by memoize() for callable `func`."""
base = (full_name(func),) if name is None else (name,)

try:
arg_names = tuple(inspect.signature(func).parameters)
except (TypeError, ValueError):
# Some callables (e.g. certain builtins) don't expose a
# signature; fall back to matching `ignore` by position only,
# same as before this parameter-name lookup was added.
arg_names = ()

@ft.wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper for callable to cache arguments and return values."""
Expand All @@ -1880,7 +1900,7 @@ def wrapper(*args, **kwargs):

def __cache_key__(*args, **kwargs):
"""Make key for cache given function arguments."""
return args_to_key(base, args, kwargs, typed, ignore)
return args_to_key(base, args, kwargs, typed, ignore, arg_names)

wrapper.__cache_key__ = __cache_key__
return wrapper
Expand Down
14 changes: 14 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,20 @@ def test(*args, **kwargs):
assert cache.stats() == (2, 1)


def test_memoize_ignore_by_name_regardless_of_call_style(cache):
# Regression test for GH #240: a name in `ignore` should be honored
# whether the caller passes that argument positionally or by keyword.
# `session` is deliberately something that can't be pickled, so if it
# ever leaks into the cache key this raises instead of quietly caching
# under two different keys.
@cache.memoize(ignore={'session'})
def get(entity_id, session):
return entity_id

assert get('a', session=threading.Lock())
assert get('b', threading.Lock())


def test_memoize_iter(cache):
@cache.memoize()
def test(*args, **kwargs):
Expand Down