c2a302725b6549f32f15d60af2607dcedb6e4122
[scons.git] / src / engine / SCons / Memoize.py
1 """Memoizer
2
3 Memoizer -- base class to provide automatic, optimized caching of
4 method return values for subclassed objects.  Caching is activated by
5 the presence of "__cacheable__" in the doc of a method (acts like a
6 decorator).  The presence of "__cache_reset__" or "__reset_cache__"
7 in the doc string instead indicates a method that should reset the
8 cache, discarding any currently cached values.
9
10 Note: current implementation is optimized for speed, not space.  The
11 cache reset operation does not actually discard older results, and in
12 fact, all cached results (and keys) are held indefinitely.
13
14 Most of the work for this is done by copying and modifying the class
15 definition itself, rather than the object instances.  This will
16 therefore allow all instances of a class to get caching activated
17 without requiring lengthy initialization or other management of the
18 instance.
19
20 [This could also be done using metaclassing (which would require
21 Python 2.2) and decorators (which would require Python 2.4).  Current
22 implementation is used due to Python 1.5.2 compatability requirement
23 contraint.]
24
25 A few notes:
26
27     * All local methods/attributes use a prefix of "_MeMoIZeR" to avoid
28       namespace collisions with the attributes of the objects
29       being cached.
30
31     * Based on performance evaluations of dictionaries, caching is
32       done by providing each object with a unique key attribute and
33       using the value of that attribute as an index for dictionary
34       lookup.  If an object doesn't have one of these attributes,
35       fallbacks are utilized (although they will be somewhat slower).
36
37       * To support this unique-value attribute correctly, it must be
38         removed whenever a __cmp__ operation is performed, and it must
39         be updated whenever a copy.copy or copy.deepcopy is performed,
40         so appropriate manipulation is provided by the Caching code
41         below.
42
43     * Cached values are stored in the class (indexed by the caching
44       key attribute, then by the name of the method called and the
45       constructed key of the arguments passed).  By storing them here
46       rather than on the instance, the instance can be compared,
47       copied, and pickled much easier.
48
49 Some advantages:
50
51     * The method by which caching is implemented can be changed in a
52       single location and it will apply globally.
53
54     * Greatly simplified client code: remove lots of try...except or
55       similar handling of cached lookup.  Also usually more correct in
56       that it based caching on all input arguments whereas many
57       hand-implemented caching operations often miss arguments that
58       might affect results.
59
60     * Caching can be globally disabled very easily (for testing, etc.)
61
62 """
63
64 #
65 # __COPYRIGHT__
66 #
67 # Permission is hereby granted, free of charge, to any person obtaining
68 # a copy of this software and associated documentation files (the
69 # "Software"), to deal in the Software without restriction, including
70 # without limitation the rights to use, copy, modify, merge, publish,
71 # distribute, sublicense, and/or sell copies of the Software, and to
72 # permit persons to whom the Software is furnished to do so, subject to
73 # the following conditions:
74 #
75 # The above copyright notice and this permission notice shall be included
76 # in all copies or substantial portions of the Software.
77 #
78 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
79 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
80 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
81 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
82 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
83 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
84 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
85 #
86
87 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
88
89 #TBD: for pickling, should probably revert object to unclassed state...
90
91 import copy
92 import os
93 import string
94 import sys
95
96 # A flag controlling whether or not we actually use memoization.
97 use_memoizer = 1
98
99 #
100 # Generate a key for an object that is to be used as the caching key
101 # for that object.
102 #
103 # Current implementation: singleton generating a monotonically
104 # increasing integer
105
106 class MemoizerKey:
107     def __init__(self):
108         self._next_keyval = 0
109     def __call__(self):
110         r = self._next_keyval
111         self._next_keyval = self._next_keyval + 1
112         return str(r)
113 Next_Memoize_Key = MemoizerKey()
114
115
116 #
117 # Memoized Class management.
118 #
119 # Classes can be manipulated just like object instances; we are going
120 # to do some of that here, without the benefit of metaclassing
121 # introduced in Python 2.2 (it would be nice to use that, but this
122 # attempts to maintain backward compatibility to Python 1.5.2).
123 #
124 # The basic implementation therefore is to update the class definition
125 # for any objects that we want to enable caching for.  The updated
126 # definition performs caching activities for those methods
127 # appropriately marked in the original class.
128 #
129 # When an object is created, its class is switched to this updated,
130 # cache-enabled class definition, thereby enabling caching operations.
131 #
132 # To get an instance to used the updated, caching class, the instance
133 # must declare the Memoizer as a base class and make sure to call the
134 # Memoizer's __init__ during the instance's __init__.  The Memoizer's
135 # __init__ will perform the class updating.
136
137 # For Python 2.2 and later, where metaclassing is supported, it is
138 # sufficient to provide a "__metaclass__ = Memoized_Metaclass" as part
139 # of the class definition; the metaclassing will automatically invoke
140 # the code herein properly.
141
142 ##import cPickle
143 ##def ALT0_MeMoIZeR_gen_key(argtuple, kwdict):
144 ##    return cPickle.dumps( (argtuple,kwdict) )
145
146 def ALT1_MeMoIZeR_gen_key(argtuple, kwdict):
147     return repr(argtuple) + '|' + repr(kwdict)
148
149 def ALT2_MeMoIZeR_gen_key(argtuple, kwdict):
150     return string.join(map(lambda A:
151                            getattr(A, '_MeMoIZeR_Key', str(A)),
152                            argtuple) + \
153                        map(lambda D:
154                            str(D[0])+
155                            getattr(D[1], '_MeMoIZeR_Key', str(D[1])),
156                            kwdict.items()),
157                        '|')
158
159 def ALT3_MeMoIZeR_gen_key(argtuple, kwdict):
160     ret = []
161     for A in argtuple:
162         X = getattr(A, '_MeMoIZeR_Key', None)
163         if X:
164             ret.append(X)
165         else:
166             ret.append(str(A))
167     for K,V in kwdict.items():
168         ret.append(str(K))
169         X = getattr(V, '_MeMoIZeR_Key', None)
170         if X:
171             ret.append(X)
172         else:
173             ret.append(str(V))
174     return string.join(ret, '|')
175
176 def ALT4_MeMoIZeR_gen_key(argtuple, kwdict):
177     if kwdict:
178         return string.join(map(lambda A:
179                                getattr(A, '_MeMoIZeR_Key', None) or str(A),
180                                argtuple) + \
181                            map(lambda D:
182                                str(D[0])+
183                                (getattr(D[1], '_MeMoIZeR_Key', None) or str(D[1])),
184                                kwdict.items()),
185                            '|')
186     return string.join(map(lambda A:
187                         getattr(A, '_MeMoIZeR_Key', None) or str(A),
188                         argtuple),
189                        '!')
190
191 def ALT5_MeMoIZeR_gen_key(argtuple, kwdict):
192     A = string.join(map(str, argtuple), '|')
193     K = ''
194     if kwdict:
195         I = map(lambda K,D=kwdict: str(K)+'='+str(D[K]), kwdict.keys())
196         K = string.join(I, '|')
197     return string.join([A,K], '!')
198
199 def ALT6_MeMoIZeR_gen_key(argtuple, kwdict):
200     A = string.join(map(str, map(id, argtuple)), '|')
201     K = ''
202     if kwdict:
203         I = map(lambda K,D=kwdict: str(K)+'='+str(id(D[K])), kwdict.keys())
204         K = string.join(I, '|')
205     return string.join([A,K], '!')
206
207 def ALT7_MeMoIZeR_gen_key(argtuple, kwdict):
208     A = string.join(map(repr, argtuple), '|')
209     K = ''
210     if kwdict:
211         I = map(lambda K,D=kwdict: repr(K)+'='+repr(D[K]), kwdict.keys())
212         K = string.join(I, '|')
213     return string.join([A,K], '!')
214
215 def ALT8_MeMoIZeR_gen_key(argtuple, kwdict):
216     ret = []
217     for A in argtuple:
218         X = getattr(A, '_MeMoIZeR_Key', None)
219         if X:
220             ret.append(X)
221         else:
222             ret.append(repr(A))
223     for K,V in kwdict.items():
224         ret.append(str(K))
225         X = getattr(V, '_MeMoIZeR_Key', None)
226         if X:
227             ret.append(X)
228         else:
229             ret.append(repr(V))
230     return string.join(ret, '|')
231
232 def ALT9_MeMoIZeR_gen_key(argtuple, kwdict):
233     ret = []
234     for A in argtuple:
235         try:
236             X = A.__dict__.get('_MeMoIZeR_Key', None) or repr(A)
237         except (AttributeError, KeyError):
238             X = repr(A)
239         ret.append(X)
240     for K,V in kwdict.items():
241         ret.append(str(K))
242         ret.append('=')
243         try:
244             X = V.__dict__.get('_MeMoIZeR_Key', None) or repr(V)
245         except (AttributeError, KeyError):
246             X = repr(V)
247         ret.append(X)
248     return string.join(ret, '|')
249
250 #_MeMoIZeR_gen_key = ALT9_MeMoIZeR_gen_key    # 8.8, 0.20
251 _MeMoIZeR_gen_key = ALT8_MeMoIZeR_gen_key    # 8.5, 0.18
252 #_MeMoIZeR_gen_key = ALT7_MeMoIZeR_gen_key    # 8.7, 0.17
253 #_MeMoIZeR_gen_key = ALT6_MeMoIZeR_gen_key    #
254 #_MeMoIZeR_gen_key = ALT5_MeMoIZeR_gen_key    # 9.7, 0.20
255 #_MeMoIZeR_gen_key = ALT4_MeMoIZeR_gen_key    # 8.6, 0.19
256 #_MeMoIZeR_gen_key = ALT3_MeMoIZeR_gen_key    # 8.5, 0.20
257 #_MeMoIZeR_gen_key = ALT2_MeMoIZeR_gen_key    # 10.1, 0.22
258 #_MeMoIZeR_gen_key = ALT1_MeMoIZeR_gen_key    # 8.6 0.18
259
260
261
262 ## This is really the core worker of the Memoize module.  Any
263 ## __cacheable__ method ends up calling this function which tries to
264 ## return a previously cached value if it exists, and which calls the
265 ## actual function and caches the return value if it doesn't already
266 ## exist.
267 ##
268 ## This function should be VERY efficient: it will get called a lot
269 ## and its job is to be faster than what would be called.
270
271 def Memoizer_cache_get(func, cdict, args, kw):
272     """Called instead of name to see if this method call's return
273     value has been cached.  If it has, just return the cached
274     value; if not, call the actual method and cache the return."""
275
276     obj = args[0]
277
278     ckey = obj._MeMoIZeR_Key + ':' + _MeMoIZeR_gen_key(args, kw)
279
280 ##    try:
281 ##        rval = cdict[ckey]
282 ##    except KeyError:
283 ##        rval = cdict[ckey] = apply(func, args, kw)
284
285     rval = cdict.get(ckey, "_MeMoIZeR")
286     if rval is "_MeMoIZeR":
287         rval = cdict[ckey] = apply(func, args, kw)
288
289 ##    rval = cdict.setdefault(ckey, apply(func, args, kw))
290
291 ##    if cdict.has_key(ckey):
292 ##        rval = cdict[ckey]
293 ##    else:
294 ##        rval = cdict[ckey] = apply(func, args, kw)
295
296     return rval
297
298 def Memoizer_cache_get_self(func, cdict, self):
299     """Called instead of func(self) to see if this method call's
300     return value has been cached.  If it has, just return the cached
301     value; if not, call the actual method and cache the return.
302     Optimized version of Memoizer_cache_get for methods that take the
303     object instance as the only argument."""
304
305     ckey = self._MeMoIZeR_Key
306
307 ##    try:
308 ##        rval = cdict[ckey]
309 ##    except KeyError:
310 ##        rval = cdict[ckey] = func(self)
311
312     rval = cdict.get(ckey, "_MeMoIZeR")
313     if rval is "_MeMoIZeR":
314         rval = cdict[ckey] = func(self)
315
316 ##    rval = cdict.setdefault(ckey, func(self)))
317
318 ##    if cdict.has_key(ckey):
319 ##        rval = cdict[ckey]
320 ##    else:
321 ##        rval = cdict[ckey] = func(self)
322
323     return rval
324
325 def Memoizer_cache_get_one(func, cdict, self, arg):
326     """Called instead of func(self, arg) to see if this method call's
327     return value has been cached.  If it has, just return the cached
328     value; if not, call the actual method and cache the return.
329     Optimized version of Memoizer_cache_get for methods that take the
330     object instance and one other argument only."""
331
332 ##    X = getattr(arg, "_MeMoIZeR_Key", None)
333 ##    if X:
334 ##        ckey = self._MeMoIZeR_Key +':'+ X
335 ##    else:
336 ##        ckey = self._MeMoIZeR_Key +':'+ str(arg)
337     ckey = self._MeMoIZeR_Key + ':' + \
338            (getattr(arg, "_MeMoIZeR_Key", None) or repr(arg))
339
340 ##    try:
341 ##        rval = cdict[ckey]
342 ##    except KeyError:
343 ##        rval = cdict[ckey] = func(self, arg)
344
345     rval = cdict.get(ckey, "_MeMoIZeR")
346     if rval is "_MeMoIZeR":
347         rval = cdict[ckey] = func(self, arg)
348
349 ##    rval = cdict.setdefault(ckey, func(self, arg)))
350
351 ##    if cdict.has_key(ckey):
352 ##        rval = cdict[ckey]
353 ##    else:
354 ##        rval = cdict[ckey] = func(self, arg)
355
356     return rval
357
358 #
359 # Caching stuff is tricky, because the tradeoffs involved are often so
360 # non-obvious, so we're going to support an alternate set of functions
361 # that also count the hits and misses, to try to get a concrete idea of
362 # which Memoizations seem to pay off.
363 #
364 # Because different configurations can have such radically different
365 # performance tradeoffs, interpreting the hit/miss results will likely be
366 # more of an art than a science.  In other words, don't assume that just
367 # because you see no hits in one configuration that it's not worthwhile
368 # Memoizing that method.
369 #
370 # Note that these are essentially cut-and-paste copies of the above
371 # Memozer_cache_get*() implementations, with the addition of the
372 # counting logic.  If the above implementations change, the
373 # corresponding change should probably be made down below as well,
374 # just to try to keep things in sync.
375 #
376
377 class CounterEntry:
378     def __init__(self):
379         self.hit = 0
380         self.miss = 0
381
382 import UserDict
383 class Counter(UserDict.UserDict):
384     def __call__(self, obj, methname):
385         k = obj.__class__.__name__ + '.' + methname
386         try:
387             return self[k]
388         except KeyError:
389             c = self[k] = CounterEntry()
390             return c
391
392 CacheCount = Counter()
393 CacheCountSelf = Counter()
394 CacheCountOne = Counter()
395
396 def Dump():
397     items = CacheCount.items() + CacheCountSelf.items() + CacheCountOne.items()
398     items.sort()
399     for k, v in items:
400         print "    %7d hits %7d misses   %s()" % (v.hit, v.miss, k)
401
402 def Count_cache_get(name, func, cdict, args, kw):
403     """Called instead of name to see if this method call's return
404     value has been cached.  If it has, just return the cached
405     value; if not, call the actual method and cache the return."""
406
407     obj = args[0]
408
409     ckey = obj._MeMoIZeR_Key + ':' + _MeMoIZeR_gen_key(args, kw)
410
411     c = CacheCount(obj, name)
412     rval = cdict.get(ckey, "_MeMoIZeR")
413     if rval is "_MeMoIZeR":
414         rval = cdict[ckey] = apply(func, args, kw)
415         c.miss = c.miss + 1
416     else:
417         c.hit = c.hit + 1
418
419     return rval
420
421 def Count_cache_get_self(name, func, cdict, self):
422     """Called instead of func(self) to see if this method call's
423     return value has been cached.  If it has, just return the cached
424     value; if not, call the actual method and cache the return.
425     Optimized version of Memoizer_cache_get for methods that take the
426     object instance as the only argument."""
427
428     ckey = self._MeMoIZeR_Key
429
430     c = CacheCountSelf(self, name)
431     rval = cdict.get(ckey, "_MeMoIZeR")
432     if rval is "_MeMoIZeR":
433         rval = cdict[ckey] = func(self)
434         c.miss = c.miss + 1
435     else:
436         c.hit = c.hit + 1
437
438     return rval
439
440 def Count_cache_get_one(name, func, cdict, self, arg):
441     """Called instead of func(self, arg) to see if this method call's
442     return value has been cached.  If it has, just return the cached
443     value; if not, call the actual method and cache the return.
444     Optimized version of Memoizer_cache_get for methods that take the
445     object instance and one other argument only."""
446
447     ckey = self._MeMoIZeR_Key + ':' + \
448            (getattr(arg, "_MeMoIZeR_Key", None) or repr(arg))
449
450     c = CacheCountOne(self, name)
451     rval = cdict.get(ckey, "_MeMoIZeR")
452     if rval is "_MeMoIZeR":
453         rval = cdict[ckey] = func(self, arg)
454         c.miss = c.miss + 1
455     else:
456         c.hit = c.hit + 1
457
458     return rval
459
460 MCG_dict = {
461     'MCG'  : Memoizer_cache_get,
462     'MCGS' : Memoizer_cache_get_self,
463     'MCGO' : Memoizer_cache_get_one,
464 }
465
466 MCG_lambda = "lambda *args, **kw: MCG(methcode, methcached, args, kw)"
467 MCGS_lambda = "lambda self: MCGS(methcode, methcached, self)"
468 MCGO_lambda = "lambda self, arg: MCGO(methcode, methcached, self, arg)"
469
470 def EnableCounting():
471     """Enable counting of Memoizer hits and misses by overriding the
472     globals that hold the non-counting versions of the functions and
473     lambdas we call with the counting versions.
474     """
475     global MCG_dict
476     global MCG_lambda
477     global MCGS_lambda
478     global MCGO_lambda
479
480     MCG_dict = {
481         'MCG'  : Count_cache_get,
482         'MCGS' : Count_cache_get_self,
483         'MCGO' : Count_cache_get_one,
484     }
485
486     MCG_lambda = "lambda *args, **kw: MCG(methname, methcode, methcached, args, kw)"
487     MCGS_lambda = "lambda self: MCGS(methname, methcode, methcached, self)"
488     MCGO_lambda = "lambda self, arg: MCGO(methname, methcode, methcached, self, arg)"
489
490
491
492 class _Memoizer_Simple:
493
494     def __setstate__(self, state):
495         self.__dict__.update(state)
496         self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
497         #kwq: need to call original's setstate if it had one...
498
499     def _MeMoIZeR_reset(self):
500         self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
501         return 1
502
503
504 class _Memoizer_Comparable:
505
506     def __setstate__(self, state):
507         self.__dict__.update(state)
508         self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
509         #kwq: need to call original's setstate if it had one...
510
511     def _MeMoIZeR_reset(self):
512         self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
513         return 1
514
515     def __cmp__(self, other):
516         """A comparison might use the object dictionaries to
517         compare, so the dictionaries should contain caching
518         entries.  Make new dictionaries without those entries
519         to use with the underlying comparison."""
520
521         if self is other:
522             return 0
523
524         # We are here as a cached object, but cmp will flip its
525         # arguments back and forth and recurse attempting to get base
526         # arguments for the comparison, so we might have already been
527         # stripped.
528
529         try:
530             saved_d1 = self.__dict__
531             d1 = copy.copy(saved_d1)
532             del d1['_MeMoIZeR_Key']
533         except KeyError:
534             return self._MeMoIZeR_cmp(other)
535         self.__dict__ = d1
536
537         # Same thing for the other, but we should try to convert it
538         # here in case the _MeMoIZeR_cmp compares __dict__ objects
539         # directly.
540
541         saved_other = None
542         try:
543             if other.__dict__.has_key('_MeMoIZeR_Key'):
544                 saved_other = other.__dict__
545                 d2 = copy.copy(saved_other)
546                 del d2['_MeMoIZeR_Key']
547                 other.__dict__ = d2
548         except (AttributeError, KeyError):
549             pass
550
551         # Both self and other have been prepared: perform the test,
552         # then restore the original dictionaries and exit
553
554         rval = self._MeMoIZeR_cmp(other)
555
556         self.__dict__ = saved_d1
557         if saved_other:
558             other.__dict__ = saved_other
559
560         return rval
561
562
563 def Analyze_Class(klass):
564     if klass.__dict__.has_key('_MeMoIZeR_converted'): return klass
565
566     original_name = str(klass)
567
568     D,R,C = _analyze_classmethods(klass.__dict__, klass.__bases__)
569
570     if C:
571         modelklass = _Memoizer_Comparable
572         lcldict = {'_MeMoIZeR_cmp':C}
573     else:
574         modelklass = _Memoizer_Simple
575         lcldict = {}
576
577     klass.__dict__.update(memoize_classdict(klass, modelklass, lcldict, D, R))
578
579     return klass
580
581
582 # Note that each eval("lambda...") has a few \n's prepended to the
583 # lambda, and furthermore that each of these evals has a different
584 # number of \n's prepended.  This is to provide a little bit of info
585 # for traceback or profile output, which generate things like 'File
586 # "<string>", line X'.  X will be the number of \n's plus 1.
587
588 # Also use the following routine to specify the "filename" portion so
589 # that it provides useful information.  In addition, make sure it
590 # contains 'os.sep + "SCons" + os.sep' for the
591 # SCons.Script.find_deepest_user_frame operation.
592
593 def whoami(memoizer_funcname, real_funcname):
594     return '...'+os.sep+'SCons'+os.sep+'Memoizer-'+ \
595            memoizer_funcname+'-lambda<'+real_funcname+'>'
596
597 def memoize_classdict(klass, modelklass, new_klassdict, cacheable, resetting):
598     new_klassdict.update(modelklass.__dict__)
599     new_klassdict['_MeMoIZeR_converted'] = 1
600
601     for name,code in cacheable.items():
602         eval_dict = {
603             'methname' : name,
604             'methcode' : code,
605             'methcached' : {},
606         }
607         eval_dict.update(MCG_dict)
608         fc = code.func_code
609         if fc.co_argcount == 1 and not fc.co_flags & 0xC:
610             compiled = compile("\n"*1 + MCGS_lambda,
611                                whoami('cache_get_self', name),
612                                "eval")
613         elif fc.co_argcount == 2 and not fc.co_flags & 0xC:
614             compiled = compile("\n"*2 + MCGO_lambda,
615                                whoami('cache_get_one', name),
616                                "eval")
617         else:
618             compiled = compile("\n"*3 + MCG_lambda,
619                                whoami('cache_get', name),
620                                "eval")
621         newmethod = eval(compiled, eval_dict, {})
622         new_klassdict[name] = newmethod
623
624     for name,code in resetting.items():
625         newmethod = eval(
626             compile(
627             "lambda obj_self, *args, **kw: (obj_self._MeMoIZeR_reset(), apply(rmethcode, (obj_self,)+args, kw))[1]",
628             whoami('cache_reset', name),
629             'eval'),
630             {'rmethcode':code}, {})
631         new_klassdict[name] = newmethod
632
633     return new_klassdict
634
635 def _analyze_classmethods(klassdict, klassbases):
636     """Given a class, performs a scan of methods for that class and
637     all its base classes (recursively). Returns aggregated results of
638     _scan_classdict calls where subclass methods are superimposed over
639     base class methods of the same name (emulating instance->class
640     method lookup)."""
641
642     D = {}
643     R = {}
644     C = None
645
646     # Get cache/reset/cmp methods from subclasses
647
648     for K in klassbases:
649         if K.__dict__.has_key('_MeMoIZeR_converted'): continue
650         d,r,c = _analyze_classmethods(K.__dict__, K.__bases__)
651         D.update(d)
652         R.update(r)
653         C = c or C
654
655     # Delete base method info if current class has an override
656
657     for M in D.keys():
658         if M == '__cmp__': continue
659         if klassdict.has_key(M):
660             del D[M]
661     for M in R.keys():
662         if M == '__cmp__': continue
663         if klassdict.has_key(M):
664             del R[M]
665
666     # Get cache/reset/cmp from current class
667
668     d,r,c = _scan_classdict(klassdict)
669
670     # Update accumulated cache/reset/cmp methods
671
672     D.update(d)
673     R.update(r)
674     C = c or C
675
676     return D,R,C
677
678
679 def _scan_classdict(klassdict):
680     """Scans the method dictionary of a class to find all methods
681     interesting to caching operations.  Returns a tuple of these
682     interesting methods:
683
684       ( dict-of-cachable-methods,
685         dict-of-cache-resetting-methods,
686         cmp_method_val or None)
687
688     Each dict has the name of the method as a key and the corresponding
689     value is the method body."""
690
691     cache_setters = {}
692     cache_resetters = {}
693     cmp_if_exists = None
694     already_cache_modified = 0
695
696     for attr,val in klassdict.items():
697         if not callable(val): continue
698         if attr == '__cmp__':
699             cmp_if_exists = val
700             continue  # cmp can't be cached and can't reset cache
701         if attr == '_MeMoIZeR_cmp':
702             already_cache_modified = 1
703             continue
704         if not val.__doc__: continue
705         if string.find(val.__doc__, '__cache_reset__') > -1:
706             cache_resetters[attr] = val
707             continue
708         if string.find(val.__doc__, '__reset_cache__') > -1:
709             cache_resetters[attr] = val
710             continue
711         if string.find(val.__doc__, '__cacheable__') > -1:
712             cache_setters[attr] = val
713             continue
714     if already_cache_modified: cmp_if_exists = 'already_cache_modified'
715     return cache_setters, cache_resetters, cmp_if_exists
716
717 #
718 # Primary Memoizer class.  This should be a base-class for any class
719 # that wants method call results to be cached.  The sub-class should
720 # call this parent class's __init__ method, but no other requirements
721 # are made on the subclass (other than appropriate decoration).
722
723 class Memoizer:
724     """Object which performs caching of method calls for its 'primary'
725     instance."""
726
727     def __init__(self):
728         self.__class__ = Analyze_Class(self.__class__)
729         self._MeMoIZeR_Key =  Next_Memoize_Key()
730
731 # Find out if we are pre-2.2
732
733 try:
734     vinfo = sys.version_info
735 except AttributeError:
736     """Split an old-style version string into major and minor parts.  This
737     is complicated by the fact that a version string can be something
738     like 3.2b1."""
739     import re
740     version = string.split(string.split(sys.version, ' ')[0], '.')
741     vinfo = (int(version[0]), int(re.match('\d+', version[1]).group()))
742     del re
743
744 need_version = (2, 2) # actual
745 #need_version = (33, 0)  # always
746 #need_version = (0, 0)  # never
747
748 has_metaclass =  (vinfo[0] > need_version[0] or \
749                   (vinfo[0] == need_version[0] and
750                    vinfo[1] >= need_version[1]))
751
752 if not has_metaclass:
753
754     class Memoized_Metaclass:
755         # Just a place-holder so pre-metaclass Python versions don't
756         # have to have special code for the Memoized classes.
757         pass
758
759 else:
760
761     # Initialization is a wee bit of a hassle.  We want to do some of
762     # our own work for initialization, then pass on to the actual
763     # initialization function.  However, we have to be careful we
764     # don't interfere with (a) the super()'s initialization call of
765     # it's superclass's __init__, and (b) classes we are Memoizing
766     # that don't have their own __init__ but which have a super that
767     # has an __init__.  To do (a), we eval a lambda below where the
768     # actual init code is locally bound and the __init__ entry in the
769     # class's dictionary is replaced with the _MeMoIZeR_init call.  To
770     # do (b), we use _MeMoIZeR_superinit as a fallback if the class
771     # doesn't have it's own __init__.  Note that we don't use getattr
772     # to obtain the __init__ because we don't want to re-instrument
773     # parent-class __init__ operations (and we want to avoid the
774     # Object object's slot init if the class has no __init__).
775
776     def _MeMoIZeR_init(actual_init, self, args, kw):
777         self.__dict__['_MeMoIZeR_Key'] =  Next_Memoize_Key()
778         apply(actual_init, (self,)+args, kw)
779
780     def _MeMoIZeR_superinit(self, cls, args, kw):
781         apply(super(cls, self).__init__, args, kw)
782
783     class Memoized_Metaclass(type):
784         def __init__(cls, name, bases, cls_dict):
785             # Note that cls_dict apparently contains a *copy* of the
786             # attribute dictionary of the class; modifying cls_dict
787             # has no effect on the actual class itself.
788             D,R,C = _analyze_classmethods(cls_dict, bases)
789             if C:
790                 modelklass = _Memoizer_Comparable
791                 cls_dict['_MeMoIZeR_cmp'] = C
792             else:
793                 modelklass = _Memoizer_Simple
794             klassdict = memoize_classdict(cls, modelklass, cls_dict, D, R)
795
796             init = klassdict.get('__init__', None)
797             if not init:
798                 # Make sure filename has os.sep+'SCons'+os.sep so that
799                 # SCons.Script.find_deepest_user_frame doesn't stop here
800                 import inspect # It's OK, can't get here for Python < 2.1
801                 filename = inspect.getsourcefile(_MeMoIZeR_superinit)
802                 if not filename:
803                     # This file was compiled at a path name different from
804                     # how it's invoked now, so just make up something.
805                     filename = whoami('superinit', '???')
806                 superinitcode = compile(
807                     "lambda self, *args, **kw: MPI(self, cls, args, kw)",
808                     filename,
809                     "eval")
810                 superinit = eval(superinitcode,
811                                  {'cls':cls,
812                                   'MPI':_MeMoIZeR_superinit})
813                 init = superinit
814
815             newinitcode = compile(
816                 "\n"*(init.func_code.co_firstlineno-1) +
817                 "lambda self, args, kw: _MeMoIZeR_init(real_init, self, args, kw)",
818                 whoami('init', init.func_code.co_filename),
819                 'eval')
820             newinit = eval(newinitcode,
821                            {'real_init':init,
822                             '_MeMoIZeR_init':_MeMoIZeR_init},
823                            {})
824             klassdict['__init__'] = lambda self, *args, **kw: newinit(self, args, kw)
825
826             super(Memoized_Metaclass, cls).__init__(name, bases, klassdict)
827             # Now, since klassdict doesn't seem to have affected the class
828             # definition itself, apply klassdict.
829             for attr in klassdict.keys():
830                 setattr(cls, attr, klassdict[attr])
831
832 def DisableMemoization():
833     global use_memoizer
834     use_memoizer = None
835
836 def use_old_memoization():
837     return use_memoizer and not has_metaclass