1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
"""
Transparent opportunistic weakref instance caching
:py:class:`WeakInstMeta` is a metaclass designed such that you just add it into
the target class, and instance caching will be done based on the args/keywords
passed to __init__. Essentially, embedding an opportunistic instance caching
factory into the target class.
There are some caveats to be aware of in using this metaclass:
* If you're doing instance sharing, it's strongly advised you do this only for
immutable instances. Generally speaking, you don't want two different codepaths
modifying the same object (a ORM implementation is a notable exemption to this).
* The implementation doesn't guarantee that it'll always reuse an instance- if the
args/keywords aren't hashable, this machinery cannot cache the instance. If the
invocation of the class differs in positional vs optional arg invocation, it's
possible to get back a new instance.
* In short, if you're generating a lot of immutable instances and want to
automatically share instances to lower your memory footprint, WeakInstMeta is
a good metaclass to use.
* This is weakref caching of instances- it will not force an instance to stay in
memory, it will only reuse instances that are already in memory.
Simple usage example:
>>> from snakeoil.caching import WeakInstMeta
>>> class myfoo(metaclass=WeakInstMeta):
... __inst_caching__ = True # safety measure turning caching on
... counter = 0
...
... def __init__(self, arg1, arg2, option=None):
... self.arg1, self.arg2, self.option = arg1, arg2, option
... self.__class__.counter += 1
>>>
>>> assert myfoo(1, 2, 3) is myfoo(1, 2, 3)
>>> assert myfoo(1, 2, option=3) is myfoo(1, 2, option=3)
>>> assert myfoo(1, 2) is not myfoo(1, 2, 3)
>>> # per caveats, please note that because this invocation differs
>>> # in positional/keywords, instance sharing does not occur-
>>> # despite the fact they're effectively the same to __init__
>>> assert myfoo(2, 3, 4) is not myfoo(1, 2, option=4)
>>>
>>> # finally note that it is weakref'ing the instances.
>>> # we use the counter attribute here since the python allocator
>>> # will sometimes reuse the address if there are no allocations
>>> # between the deletion and creation
>>> o = myfoo(1, 2)
>>> my_count = o.counter
>>> del o
>>> assert my_count != myfoo(1, 2).counter # a new instance is created
"""
__all__ = ("WeakInstMeta",)
import warnings
from weakref import WeakValueDictionary
class WeakInstMeta(type):
"""Metaclass for instance caching, resulting in reuse of unique instances.
few notes-
- instances must be immutable (or effectively so).
Since creating a new instance may return a preexisting instance,
this requirement B{must} be honored.
- due to the potential for mishap, each subclass of a caching class must
assign __inst_caching__ = True to enable caching for the derivative.
- conversely, __inst_caching__ = False does nothing
(although it's useful as a sign of
I{do not enable caching for this class}
- instance caching can be disabled per instantiation via passing
disabling_inst_caching=True into the class constructor.
Being a metaclass, the voodoo used doesn't require modification of
the class itself.
Examples of usage is the restrictions subsystem for
U{pkgcore project<http://pkgcore.org>}
"""
def __new__(cls, name, bases, d):
if d.get("__inst_caching__", False):
d["__inst_caching__"] = True
d["__inst_dict__"] = WeakValueDictionary()
else:
d["__inst_caching__"] = False
slots = d.get("__slots__")
# get ourselves a singleton to be safe...
o = object()
if slots is not None:
for base in bases:
if getattr(base, "__weakref__", o) is not o:
break
else:
d["__slots__"] = tuple(slots) + ("__weakref__",)
return type.__new__(cls, name, bases, d)
def __call__(cls, *a, **kw):
"""disable caching via disable_inst_caching=True"""
if cls.__inst_caching__ and not kw.pop("disable_inst_caching", False):
kwlist = list(kw.items())
kwlist.sort()
key = (a, tuple(kwlist))
try:
instance = cls.__inst_dict__.get(key)
except (NotImplementedError, TypeError) as t:
warnings.warn(f"caching keys for {cls}, got {t} for a={a}, kw={kw}")
del t
key = instance = None
if instance is None:
instance = super(WeakInstMeta, cls).__call__(*a, **kw)
if key is not None:
cls.__inst_dict__[key] = instance
else:
instance = super(WeakInstMeta, cls).__call__(*a, **kw)
return instance
|