Skip to content

Commit db55bc6

Browse files
committed
Restore cache-clear behavior on a per-path basis.
1 parent 6e2740e commit db55bc6

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

importlib_metadata/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
Protocol,
2323
)
2424

25+
from ._functools import method_cache
2526
from ._itertools import unique_everseen
2627

2728
from configparser import ConfigParser
@@ -632,8 +633,9 @@ def search(self, name):
632633
def mtime(self):
633634
with contextlib.suppress(OSError):
634635
return os.stat(self.root).st_mtime
636+
self.lookup.cache_clear()
635637

636-
@functools.lru_cache()
638+
@method_cache
637639
def lookup(self, mtime):
638640
return Lookup(self)
639641

importlib_metadata/_functools.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import types
2+
import functools
3+
4+
5+
# from jaraco.functools 3.3
6+
def method_cache(method, cache_wrapper=None):
7+
"""
8+
Wrap lru_cache to support storing the cache data in the object instances.
9+
10+
Abstracts the common paradigm where the method explicitly saves an
11+
underscore-prefixed protected property on first call and returns that
12+
subsequently.
13+
14+
>>> class MyClass:
15+
... calls = 0
16+
...
17+
... @method_cache
18+
... def method(self, value):
19+
... self.calls += 1
20+
... return value
21+
22+
>>> a = MyClass()
23+
>>> a.method(3)
24+
3
25+
>>> for x in range(75):
26+
... res = a.method(x)
27+
>>> a.calls
28+
75
29+
30+
Note that the apparent behavior will be exactly like that of lru_cache
31+
except that the cache is stored on each instance, so values in one
32+
instance will not flush values from another, and when an instance is
33+
deleted, so are the cached values for that instance.
34+
35+
>>> b = MyClass()
36+
>>> for x in range(35):
37+
... res = b.method(x)
38+
>>> b.calls
39+
35
40+
>>> a.method(0)
41+
0
42+
>>> a.calls
43+
75
44+
45+
Note that if method had been decorated with ``functools.lru_cache()``,
46+
a.calls would have been 76 (due to the cached value of 0 having been
47+
flushed by the 'b' instance).
48+
49+
Clear the cache with ``.cache_clear()``
50+
51+
>>> a.method.cache_clear()
52+
53+
Same for a method that hasn't yet been called.
54+
55+
>>> c = MyClass()
56+
>>> c.method.cache_clear()
57+
58+
Another cache wrapper may be supplied:
59+
60+
>>> cache = functools.lru_cache(maxsize=2)
61+
>>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
62+
>>> a = MyClass()
63+
>>> a.method2()
64+
3
65+
66+
Caution - do not subsequently wrap the method with another decorator, such
67+
as ``@property``, which changes the semantics of the function.
68+
69+
See also
70+
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
71+
for another implementation and additional justification.
72+
"""
73+
cache_wrapper = cache_wrapper or functools.lru_cache()
74+
75+
def wrapper(self, *args, **kwargs):
76+
# it's the first call, replace the method with a cached, bound method
77+
bound_method = types.MethodType(method, self)
78+
cached_method = cache_wrapper(bound_method)
79+
setattr(self, method.__name__, cached_method)
80+
return cached_method(*args, **kwargs)
81+
82+
# Support cache clear even before cache has been created.
83+
wrapper.cache_clear = lambda: None
84+
85+
return wrapper

0 commit comments

Comments
 (0)