06b9a96d44da7aef6c711eceb1d8a1c2d53cd387
[roguelike.git] / cacher.py
1 class FirstArg(type):
2     """Metaclass to cache object creation by a key, which must be the
3     first constructor parameter.
4     
5         >>> import cacher
6         >>> class MyClass (object):
7         ...     __metaclass__ = cacher.FirstArg
8         ...     def __init__(self, name, extra):
9         ...         self.name = name
10         ...         self.extra = extra
11         >>> obj = MyClass('foo', 42)
12
13     The constructor must be called with the correct number of arguments:
14         >>> bad = MyClass('bad')
15         Traceback (most recent call last):
16             ....
17         TypeError: __init__() takes exactly 3 arguments (2 given)
18
19     Subsequent calls to the constructor with the same first argument result
20     in the same object:
21         >>> MyClass('foo', 42) is obj
22         True
23
24     
25     This is true even if the other parameters differ:
26         >>> MyClass('foo', None) is obj
27         True
28         >>> MyClass('foo', 'twelve').extra
29         42
30
31     In fact, if an instance has already been constructed, the remaining
32     parameters are disregarded entirely, and may be omitted:
33         >>> MyClass('foo') is obj
34         True
35         >>> MyClass('foo', 1, 2, 3, 4, 5, 6, 7) is obj
36         True
37
38     If the first parameter differs, the constructed object will differ:
39         >>> MyClass('bar', 18) is obj
40         False
41         >>> MyClass('bar').extra
42         18
43
44     Any immutable key may be used:
45         >>> pairobj = MyClass((1,2), {})
46         >>> MyClass((1,2)).name
47         (1, 2)
48         >>> MyClass((1,2)).extra
49         {}
50         >>> badobj = MyClass([1,2], 26)
51         Traceback (most recent call last):
52             ....
53         TypeError: list objects are unhashable
54
55     However, the key None will not be cached:
56         >>> noneobj = ( MyClass(None, 'first'), MyClass(None, 'second') )
57         >>> noneobj[0].extra
58         'first'
59         >>> noneobj[1].extra
60         'second'
61
62     The objects themselves can be mutable:
63         >>> MyClass('foo').extra = 9
64         >>> obj.extra
65         9
66         >>> MyClass('bar').name = 'something else'
67         >>> MyClass('bar').name
68         'something else'
69
70     
71     The cache is not shared with subclasses:
72         >>> class SubClass (MyClass):
73         ...     def __init__(self, name, extra):
74         ...         super(SubClass,self).__init__(name, extra)
75         >>> sub = SubClass('foo', 76)
76         >>> sub is obj
77         False
78         >>> sub.extra
79         76
80     """
81     def __init__(cls, name, bases, cdict):
82         super(FirstArg, cls).__init__(name, bases, cdict)
83         cls.__instcache = {}
84     def __call__(cls, name, *args, **kw):
85         
86         if name is None or name not in cls.__instcache:
87             cls.__instcache[name] = super(FirstArg, cls).__call__(
88                     name, *args, **kw )
89         return cls.__instcache[name]
90
91 class Singleton (type):
92     """Metaclass for singleton objects.  After the first constructor call
93     initializes an object, subsequent calls will return the same object.
94         >>> import cacher
95         >>> class MyClass (object):
96         ...     __metaclass__ = cacher.Singleton
97         ...     def __init__(self, data):
98         ...         self.data = data
99
100     The constructor must be called with the correct number of arguments:
101         >>> bad = MyClass()
102         Traceback (most recent call last):
103             ....
104         TypeError: __init__() takes exactly 2 arguments (1 given)
105         >>> obj = MyClass('only')
106
107     After an object is successfully constructed, subsequent calls to the
108     constructor yield the same object:
109         >>> MyClass('only') is obj
110         True
111
112     
113     This is true even if the other parameters differ:
114         >>> MyClass('another') is obj
115         True
116         >>> MyClass('ignored').data
117         'only'
118
119     In fact, once an instance has been constructed, subsequent calls to
120     the constructor ignore the parameters entirely:
121         >>> MyClass() is obj
122         True
123         >>> MyClass(1, 2, 3, 4, 5, 6, 7) is obj
124         True
125     
126     The objects can be mutable:
127         >>> MyClass().data = 'changed'
128         >>> obj.data
129         'changed'
130     
131     The cache is not shared with subclasses:
132         >>> class SubClass (MyClass):
133         ...     def __init__(self, data):
134         ...         super(SubClass,self).__init__(data)
135         >>> sub = SubClass('foo')
136         >>> sub is obj
137         False
138         >>> sub.data
139         'foo'
140         >>> MyClass('foo').data
141         'changed'
142     """
143     def __init__(cls, name, bases, cdict):
144         super(Singleton, cls).__init__(name, bases, cdict)
145         cls.__instance = None
146
147     def __call__(cls, *args, **kw):
148         if cls.__instance is None:
149             cls.__instance = super(Singleton, cls).__call__(*args, **kw)
150         return cls.__instance
151
152 if __name__ == "__main__":
153     import doctest
154     doctest.testmod()