Skip to content

Commit f271ae5

Browse files
authored
[#20] * Added configurable postprocessing, that allows to modify value retrieved from the cache (#30)
* [#20] * Added configurable postprocessing, that allows to modify value retrieved from the cache * Added built-in implementation, that applies deep-copy * Fix MANIFEST.in * [#20] * updated API docs --------- Co-authored-by: Michał Żmuda <[email protected]>
1 parent 451ac38 commit f271ae5

File tree

10 files changed

+215
-69
lines changed

10 files changed

+215
-69
lines changed

CHANGELOG.rst

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
2.1.0
2+
-----
3+
4+
* Added configurable postprocessing, that allows to modify value retrieved from the cache
5+
* Added built-in implementation, that applies deep-copy
6+
* Fixed missing invalidation module in api docs
7+
* Fixed MANIFEST.in
8+
19
2.0.0
210
-----
311

MANIFEST.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
include requirements.txt
22
include README.rst
3-
include CHANGELOG.md
3+
include CHANGELOG.rst

README.rst

+18-11
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ With *memoize* you have under control:
192192
least-recently-updated strategy is already provided;
193193
* entry builder (see :class:`memoize.entrybuilder.CacheEntryBuilder`)
194194
which has control over ``update_after`` & ``expires_after`` described in `Tunable eviction & async refreshing`_
195+
* value post-processing (see :class:`memoize.postprocessing.Postprocessing`);
196+
noop is the default one;
197+
deep-copy post-processing is also provided (be wary of deep-copy cost & limitations,
198+
but deep-copying allows callers to safely modify values retrieved from an in-memory cache).
195199

196200
All of these elements are open for extension (you can implement and plug-in your own).
197201
Please contribute!
@@ -209,19 +213,21 @@ Example how to customize default config (everything gets overridden):
209213
from memoize.entrybuilder import ProvidedLifeSpanCacheEntryBuilder
210214
from memoize.eviction import LeastRecentlyUpdatedEvictionStrategy
211215
from memoize.key import EncodedMethodNameAndArgsKeyExtractor
216+
from memoize.postprocessing import DeepcopyPostprocessing
212217
from memoize.storage import LocalInMemoryCacheStorage
213218
from memoize.wrapper import memoize
214219
215-
216-
@memoize(configuration=MutableCacheConfiguration
217-
.initialized_with(DefaultInMemoryCacheConfiguration())
218-
.set_method_timeout(value=timedelta(minutes=2))
219-
.set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(minutes=2),
220-
expire_after=timedelta(minutes=5)))
221-
.set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2048))
222-
.set_key_extractor(EncodedMethodNameAndArgsKeyExtractor(skip_first_arg_as_self=False))
223-
.set_storage(LocalInMemoryCacheStorage())
224-
)
220+
@memoize(
221+
configuration=MutableCacheConfiguration
222+
.initialized_with(DefaultInMemoryCacheConfiguration())
223+
.set_method_timeout(value=timedelta(minutes=2))
224+
.set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(minutes=2),
225+
expire_after=timedelta(minutes=5)))
226+
.set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2048))
227+
.set_key_extractor(EncodedMethodNameAndArgsKeyExtractor(skip_first_arg_as_self=False))
228+
.set_storage(LocalInMemoryCacheStorage())
229+
.set_postprocessing(DeepcopyPostprocessing())
230+
)
225231
async def cached():
226232
return 'dummy'
227233
@@ -232,7 +238,8 @@ Still, you can use default configuration which:
232238
* uses in-memory storage;
233239
* uses method instance & arguments to infer cache key;
234240
* stores up to 4096 elements in cache and evicts entries according to least recently updated policy;
235-
* refreshes elements after 10 minutes & ignores unrefreshed elements after 30 minutes.
241+
* refreshes elements after 10 minutes & ignores unrefreshed elements after 30 minutes;
242+
* does not post-process cached values.
236243

237244
If that satisfies you, just use default config:
238245

docs/source/memoize.rst

+55-40
Original file line numberDiff line numberDiff line change
@@ -8,103 +8,118 @@ memoize.coerced module
88
----------------------
99

1010
.. automodule:: memoize.coerced
11-
:members:
12-
:undoc-members:
13-
:show-inheritance:
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
1414

1515
memoize.configuration module
1616
----------------------------
1717

1818
.. automodule:: memoize.configuration
19-
:members:
20-
:undoc-members:
21-
:show-inheritance:
19+
:members:
20+
:undoc-members:
21+
:show-inheritance:
2222

2323
memoize.entry module
2424
--------------------
2525

2626
.. automodule:: memoize.entry
27-
:members:
28-
:undoc-members:
29-
:show-inheritance:
27+
:members:
28+
:undoc-members:
29+
:show-inheritance:
3030

3131
memoize.entrybuilder module
3232
---------------------------
3333

3434
.. automodule:: memoize.entrybuilder
35-
:members:
36-
:undoc-members:
37-
:show-inheritance:
35+
:members:
36+
:undoc-members:
37+
:show-inheritance:
3838

3939
memoize.eviction module
4040
-----------------------
4141

4242
.. automodule:: memoize.eviction
43-
:members:
44-
:undoc-members:
45-
:show-inheritance:
43+
:members:
44+
:undoc-members:
45+
:show-inheritance:
4646

4747
memoize.exceptions module
4848
-------------------------
4949

5050
.. automodule:: memoize.exceptions
51-
:members:
52-
:undoc-members:
53-
:show-inheritance:
51+
:members:
52+
:undoc-members:
53+
:show-inheritance:
54+
55+
memoize.invalidation module
56+
---------------------------
57+
58+
.. automodule:: memoize.invalidation
59+
:members:
60+
:undoc-members:
61+
:show-inheritance:
5462

5563
memoize.key module
5664
------------------
5765

5866
.. automodule:: memoize.key
59-
:members:
60-
:undoc-members:
61-
:show-inheritance:
67+
:members:
68+
:undoc-members:
69+
:show-inheritance:
6270

6371
memoize.memoize\_configuration module
6472
-------------------------------------
6573

6674
.. automodule:: memoize.memoize_configuration
67-
:members:
68-
:undoc-members:
69-
:show-inheritance:
75+
:members:
76+
:undoc-members:
77+
:show-inheritance:
78+
79+
memoize.postprocessing module
80+
-----------------------------
81+
82+
.. automodule:: memoize.postprocessing
83+
:members:
84+
:undoc-members:
85+
:show-inheritance:
7086

7187
memoize.serde module
7288
--------------------
7389

7490
.. automodule:: memoize.serde
75-
:members:
76-
:undoc-members:
77-
:show-inheritance:
91+
:members:
92+
:undoc-members:
93+
:show-inheritance:
7894

7995
memoize.statuses module
8096
-----------------------
8197

8298
.. automodule:: memoize.statuses
83-
:members:
84-
:undoc-members:
85-
:show-inheritance:
99+
:members:
100+
:undoc-members:
101+
:show-inheritance:
86102

87103
memoize.storage module
88104
----------------------
89105

90106
.. automodule:: memoize.storage
91-
:members:
92-
:undoc-members:
93-
:show-inheritance:
107+
:members:
108+
:undoc-members:
109+
:show-inheritance:
94110

95111
memoize.wrapper module
96112
----------------------
97113

98114
.. automodule:: memoize.wrapper
99-
:members:
100-
:undoc-members:
101-
:show-inheritance:
102-
115+
:members:
116+
:undoc-members:
117+
:show-inheritance:
103118

104119
Module contents
105120
---------------
106121

107122
.. automodule:: memoize
108-
:members:
109-
:undoc-members:
110-
:show-inheritance:
123+
:members:
124+
:undoc-members:
125+
:show-inheritance:

examples/configuration/custom_configuration.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@
44
from memoize.entrybuilder import ProvidedLifeSpanCacheEntryBuilder
55
from memoize.eviction import LeastRecentlyUpdatedEvictionStrategy
66
from memoize.key import EncodedMethodNameAndArgsKeyExtractor
7+
from memoize.postprocessing import DeepcopyPostprocessing
78
from memoize.storage import LocalInMemoryCacheStorage
89
from memoize.wrapper import memoize
910

1011

11-
@memoize(configuration=MutableCacheConfiguration
12-
.initialized_with(DefaultInMemoryCacheConfiguration())
13-
.set_method_timeout(value=timedelta(minutes=2))
14-
.set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(minutes=2),
15-
expire_after=timedelta(minutes=5)))
16-
.set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2048))
17-
.set_key_extractor(EncodedMethodNameAndArgsKeyExtractor(skip_first_arg_as_self=False))
18-
.set_storage(LocalInMemoryCacheStorage())
19-
)
12+
@memoize(
13+
configuration=MutableCacheConfiguration
14+
.initialized_with(DefaultInMemoryCacheConfiguration())
15+
.set_method_timeout(value=timedelta(minutes=2))
16+
.set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(minutes=2),
17+
expire_after=timedelta(minutes=5)))
18+
.set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2048))
19+
.set_key_extractor(EncodedMethodNameAndArgsKeyExtractor(skip_first_arg_as_self=False))
20+
.set_storage(LocalInMemoryCacheStorage())
21+
.set_postprocessing(DeepcopyPostprocessing())
22+
)
2023
async def cached():
2124
return 'dummy'

memoize/configuration.py

+29-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from memoize.entrybuilder import CacheEntryBuilder, ProvidedLifeSpanCacheEntryBuilder
1010
from memoize.eviction import EvictionStrategy, LeastRecentlyUpdatedEvictionStrategy
1111
from memoize.key import KeyExtractor, EncodedMethodReferenceAndArgsKeyExtractor
12+
from memoize.postprocessing import Postprocessing, NoPostprocessing
1213
from memoize.storage import CacheStorage
1314
from memoize.storage import LocalInMemoryCacheStorage
1415

@@ -51,30 +52,40 @@ def eviction_strategy(self) -> EvictionStrategy:
5152
""" Determines which EvictionStrategy is to be used by cache. """
5253
raise NotImplementedError()
5354

55+
@abstractmethod
56+
def postprocessing(self) -> Postprocessing:
57+
""" Determines which/if Postprocessing is to be used by cache. """
58+
raise NotImplementedError()
59+
5460
def __str__(self) -> str:
5561
return self.__repr__()
5662

5763
def __repr__(self) -> str:
58-
return "{name}[configured={configured}, method_timeout={method_timeout}, entry_builder={entry_builder}," \
59-
" key_extractor={key_extractor}, storage={storage}, eviction_strategy={eviction_strategy}]" \
60-
.format(name=self.__class__, configured=self.configured(), method_timeout=self.method_timeout(),
61-
entry_builder=self.entry_builder(), key_extractor=self.key_extractor(), storage=self.storage(),
62-
eviction_strategy=self.eviction_strategy())
64+
return (f"{self.__class__}["
65+
f"configured={self.configured()}, "
66+
f"method_timeout={self.method_timeout()}, "
67+
f"entry_builder={self.entry_builder()}, "
68+
f"key_extractor={self.key_extractor()}, "
69+
f"storage={self.storage()}, "
70+
f"eviction_strategy={self.eviction_strategy()}, "
71+
f"postprocessing={self.postprocessing()}"
72+
f"]")
6373

6474

6575
class MutableCacheConfiguration(CacheConfiguration):
6676
""" Mutable configuration which can be change at runtime.
6777
May be also used to customize existing configuration (for example a default one, which is immutable)."""
6878

6979
def __init__(self, configured: bool, storage: CacheStorage, key_extractor: KeyExtractor,
70-
eviction_strategy: EvictionStrategy, entry_builder: CacheEntryBuilder,
80+
eviction_strategy: EvictionStrategy, entry_builder: CacheEntryBuilder, postprocessing: Postprocessing,
7181
method_timeout: timedelta) -> None:
7282
self.__storage = storage
7383
self.__configured = configured
7484
self.__key_extractor = key_extractor
7585
self.__entry_builder = entry_builder
7686
self.__method_timeout = method_timeout
7787
self.__eviction_strategy = eviction_strategy
88+
self.__postprocessing = postprocessing
7889

7990
@staticmethod
8091
def initialized_with(configuration: CacheConfiguration) -> 'MutableCacheConfiguration':
@@ -85,6 +96,7 @@ def initialized_with(configuration: CacheConfiguration) -> 'MutableCacheConfigur
8596
entry_builder=configuration.entry_builder(),
8697
method_timeout=configuration.method_timeout(),
8798
eviction_strategy=configuration.eviction_strategy(),
99+
postprocessing=configuration.postprocessing(),
88100
)
89101

90102
def method_timeout(self) -> timedelta:
@@ -105,6 +117,9 @@ def entry_builder(self) -> CacheEntryBuilder:
105117
def eviction_strategy(self) -> EvictionStrategy:
106118
return self.__eviction_strategy
107119

120+
def postprocessing(self) -> Postprocessing:
121+
return self.__postprocessing
122+
108123
def set_method_timeout(self, value: timedelta) -> 'MutableCacheConfiguration':
109124
self.__method_timeout = value
110125
return self
@@ -129,6 +144,10 @@ def set_eviction_strategy(self, value: EvictionStrategy) -> 'MutableCacheConfigu
129144
self.__eviction_strategy = value
130145
return self
131146

147+
def set_postprocessing(self, value: Postprocessing) -> 'MutableCacheConfiguration':
148+
self.__postprocessing = value
149+
return self
150+
132151

133152
class DefaultInMemoryCacheConfiguration(CacheConfiguration):
134153
""" Default parameters that describe in-memory cache. Be ware that parameters used do not suit every case. """
@@ -142,6 +161,7 @@ def __init__(self, capacity: int = 4096, method_timeout: timedelta = timedelta(m
142161
self.__key_extractor = EncodedMethodReferenceAndArgsKeyExtractor()
143162
self.__eviction_strategy = LeastRecentlyUpdatedEvictionStrategy(capacity=capacity)
144163
self.__entry_builder = ProvidedLifeSpanCacheEntryBuilder(update_after=update_after, expire_after=expire_after)
164+
self.__postprocessing = NoPostprocessing()
145165

146166
def configured(self) -> bool:
147167
return self.__configured
@@ -160,3 +180,6 @@ def eviction_strategy(self) -> LeastRecentlyUpdatedEvictionStrategy:
160180

161181
def key_extractor(self) -> EncodedMethodReferenceAndArgsKeyExtractor:
162182
return self.__key_extractor
183+
184+
def postprocessing(self) -> Postprocessing:
185+
return self.__postprocessing

memoize/postprocessing.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import copy
2+
from abc import ABCMeta, abstractmethod
3+
from typing import Any
4+
5+
ValueType = Any
6+
7+
8+
class Postprocessing(metaclass=ABCMeta):
9+
@abstractmethod
10+
def apply(self, original: ValueType) -> ValueType:
11+
"""Transforms value just before returning from the cache."""
12+
raise NotImplementedError()
13+
14+
15+
class NoPostprocessing(Postprocessing):
16+
def apply(self, original: ValueType) -> ValueType:
17+
"""Applies no postprocessing (returns original value)."""
18+
return original
19+
20+
21+
class DeepcopyPostprocessing(Postprocessing):
22+
def apply(self, original: ValueType) -> ValueType:
23+
"""
24+
Performs deep copy of the value. Useful when you want to prevent modifying the value cached in memory
25+
(so callers could modify their copies safely).
26+
27+
Have in mind that this operation may be expensive,
28+
and may not be suitable for all types of values (see docs on copy.deepcopy).
29+
"""
30+
return copy.deepcopy(original)

memoize/wrapper.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,6 @@ def value_future_provider():
145145
else:
146146
result = current_entry
147147

148-
return result.value
148+
return configuration_snapshot.postprocessing().apply(result.value)
149149

150150
return wrapper

0 commit comments

Comments
 (0)