Add copyright notices (GPL v2 or later).
[roguelike.git] / cacher.py
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 # Copyright © 2008 Neil Moore <neil@s-z.org>.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #  
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 class FirstArg(type):
21     """Metaclass to cache object creation by a key, which must be the
22     first constructor parameter.
23     
24         >>> import cacher
25         >>> class MyClass (object):
26         ...     __metaclass__ = cacher.FirstArg
27         ...     def __init__(self, name, extra):
28         ...         self.name = name
29         ...         self.extra = extra
30         >>> obj = MyClass('foo', 42)
31
32     The constructor must be called with the correct number of arguments:
33         >>> bad = MyClass('bad')
34         Traceback (most recent call last):
35             ....
36         TypeError: __init__() takes exactly 3 arguments (2 given)
37
38     Subsequent calls to the constructor with the same first argument result
39     in the same object:
40         >>> MyClass('foo', 42) is obj
41         True
42
43     
44     This is true even if the other parameters differ:
45         >>> MyClass('foo', None) is obj
46         True
47         >>> MyClass('foo', 'twelve').extra
48         42
49
50     In fact, if an instance has already been constructed, the remaining
51     parameters are disregarded entirely, and may be omitted:
52         >>> MyClass('foo') is obj
53         True
54         >>> MyClass('foo', 1, 2, 3, 4, 5, 6, 7) is obj
55         True
56
57     If the first parameter differs, the constructed object will differ:
58         >>> MyClass('bar', 18) is obj
59         False
60         >>> MyClass('bar').extra
61         18
62
63     Any immutable key may be used:
64         >>> pairobj = MyClass((1,2), {})
65         >>> MyClass((1,2)).name
66         (1, 2)
67         >>> MyClass((1,2)).extra
68         {}
69         >>> badobj = MyClass([1,2], 26)
70         Traceback (most recent call last):
71             ....
72         TypeError: list objects are unhashable
73
74     However, the key None will not be cached:
75         >>> noneobj = ( MyClass(None, 'first'), MyClass(None, 'second') )
76         >>> noneobj[0].extra
77         'first'
78         >>> noneobj[1].extra
79         'second'
80
81     The objects themselves can be mutable:
82         >>> MyClass('foo').extra = 9
83         >>> obj.extra
84         9
85         >>> MyClass('bar').name = 'something else'
86         >>> MyClass('bar').name
87         'something else'
88
89     
90     The cache is not shared with subclasses:
91         >>> class SubClass (MyClass):
92         ...     def __init__(self, name, extra):
93         ...         super(SubClass,self).__init__(name, extra)
94         >>> sub = SubClass('foo', 76)
95         >>> sub is obj
96         False
97         >>> sub.extra
98         76
99     """
100     def __init__(cls, name, bases, cdict):
101         super(FirstArg, cls).__init__(name, bases, cdict)
102         cls.__instcache = {}
103     def __call__(cls, name, *args, **kw):
104         
105         if name is None or name not in cls.__instcache:
106             cls.__instcache[name] = super(FirstArg, cls).__call__(
107                     name, *args, **kw )
108         return cls.__instcache[name]
109
110 class Singleton (type):
111     """Metaclass for singleton objects.  After the first constructor call
112     initializes an object, subsequent calls will return the same object.
113         >>> import cacher
114         >>> class MyClass (object):
115         ...     __metaclass__ = cacher.Singleton
116         ...     def __init__(self, data):
117         ...         self.data = data
118
119     The constructor must be called with the correct number of arguments:
120         >>> bad = MyClass()
121         Traceback (most recent call last):
122             ....
123         TypeError: __init__() takes exactly 2 arguments (1 given)
124         >>> obj = MyClass('only')
125
126     After an object is successfully constructed, subsequent calls to the
127     constructor yield the same object:
128         >>> MyClass('only') is obj
129         True
130
131     
132     This is true even if the other parameters differ:
133         >>> MyClass('another') is obj
134         True
135         >>> MyClass('ignored').data
136         'only'
137
138     In fact, once an instance has been constructed, subsequent calls to
139     the constructor ignore the parameters entirely:
140         >>> MyClass() is obj
141         True
142         >>> MyClass(1, 2, 3, 4, 5, 6, 7) is obj
143         True
144     
145     The objects can be mutable:
146         >>> MyClass().data = 'changed'
147         >>> obj.data
148         'changed'
149     
150     The cache is not shared with subclasses:
151         >>> class SubClass (MyClass):
152         ...     def __init__(self, data):
153         ...         super(SubClass,self).__init__(data)
154         >>> sub = SubClass('foo')
155         >>> sub is obj
156         False
157         >>> sub.data
158         'foo'
159         >>> MyClass('foo').data
160         'changed'
161     """
162     def __init__(cls, name, bases, cdict):
163         super(Singleton, cls).__init__(name, bases, cdict)
164         cls.__instance = None
165
166     def __call__(cls, *args, **kw):
167         if cls.__instance is None:
168             cls.__instance = super(Singleton, cls).__call__(*args, **kw)
169         return cls.__instance
170
171 if __name__ == "__main__":
172     import doctest
173     doctest.testmod()