source: anuga_core/source/anuga/caching/caching.py @ 4341

Last change on this file since 4341 was 4333, checked in by ole, 18 years ago

Added test that revealed issue where identical functions with different memory addresses caused cache to recompute.
Fixed problem.

File size: 68.7 KB
Line 
1# =============================================================================
2# caching.py - Supervised caching of function results.
3# Copyright (C) 1999, 2000, 2001, 2002 Ole Moller Nielsen
4# Australian National University (1999-2003)
5# Geoscience Australia (2003-present)
6#
7#    This program is free software; you can redistribute it and/or modify
8#    it under the terms of the GNU General Public License as published by
9#    the Free Software Foundation; either version 2 of the License, or
10#    (at your option) any later version.
11#
12#    This program is distributed in the hope that it will be useful,
13#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#    GNU General Public License (http://www.gnu.org/copyleft/gpl.html)
16#    for more details.
17#
18#    You should have received a copy of the GNU General Public License
19#    along with this program; if not, write to the Free Software
20#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
21#
22#
23# Contact address: Ole.Nielsen@ga.gov.au
24#
25# Version 1.5.6 February 2002
26# =============================================================================
27 
28"""Module caching.py - Supervised caching of function results.
29
30Public functions:
31
32cache(func,args) -- Cache values returned from func given args.
33cachestat() --      Reports statistics about cache hits and time saved.
34test() --       Conducts a basic test of the caching functionality.
35
36See doc strings of individual functions for detailed documentation.
37"""
38
39# -----------------------------------------------------------------------------
40# Initialisation code
41
42# Determine platform
43#
44import os
45if os.name in ['nt', 'dos', 'win32', 'what else?']:
46  unix = 0
47else:
48  unix = 1
49
50# Make default caching directory name
51#
52if unix:
53  homedir = '~'
54  CR = '\n'
55else:
56  homedir = 'c:'
57  CR = '\r\n'  #FIXME: Not tested under windows
58 
59cachedir = homedir + os.sep + '.python_cache' + os.sep
60
61# -----------------------------------------------------------------------------
62# Options directory with default values - to be set by user
63#
64
65options = { 
66  'cachedir': cachedir,  # Default cache directory
67  'maxfiles': 1000000,   # Maximum number of cached files
68  'savestat': 1,         # Log caching info to stats file
69  'verbose': 1,          # Write messages to standard output
70  'bin': 1,              # Use binary format (more efficient)
71  'compression': 1,      # Use zlib compression
72  'bytecode': 0,         # Recompute if bytecode has changed
73  'expire': 0            # Automatically remove files that have been accessed
74                         # least recently
75}
76
77# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
78
79def set_option(key, value):
80  """Function to set values in the options directory.
81
82  USAGE:
83    set_option(key, value)
84
85  ARGUMENTS:
86    key --   Key in options dictionary. (Required)
87    value -- New value for key. (Required)
88
89  DESCRIPTION:
90    Function to set values in the options directory.
91    Raises an exception if key is not in options.
92  """
93
94  if options.has_key(key):
95    options[key] = value
96  else:
97    raise KeyError(key)  # Key not found, raise an exception
98
99# -----------------------------------------------------------------------------
100# Function cache - the main routine
101
102def cache(func, args=(), kwargs = {}, dependencies=None , cachedir=None,
103          verbose=None, compression=None, evaluate=0, test=0, clear=0,
104          return_filename=0):
105  """Supervised caching of function results.
106
107  USAGE:
108    result = cache(func, args, kwargs, dependencies, cachedir, verbose,
109                   compression, evaluate, test, return_filename)
110
111  ARGUMENTS:
112    func --            Function object (Required)
113    args --            Arguments to func (Default: ())
114    kwargs --          Keyword arguments to func (Default: {})   
115    dependencies --    Filenames that func depends on (Default: None)
116    cachedir --        Directory for cache files (Default: options['cachedir'])
117    verbose --         Flag verbose output to stdout
118                       (Default: options['verbose'])
119    compression --     Flag zlib compression (Default: options['compression'])
120    evaluate --        Flag forced evaluation of func (Default: 0)
121    test --            Flag test for cached results (Default: 0)
122    clear --           Flag delete cached results (Default: 0)   
123    return_filename -- Flag return of cache filename (Default: 0)   
124
125  DESCRIPTION:
126    A Python function call of the form
127
128      result = func(arg1,...,argn)
129
130    can be replaced by
131
132      from caching import cache
133      result = cache(func,(arg1,...,argn))
134
135  The latter form returns the same output as the former but reuses cached
136  results if the function has been computed previously in the same context.
137  'result' and the arguments can be simple types, tuples, list, dictionaries or
138  objects, but not unhashable types such as functions or open file objects.
139  The function 'func' may be a member function of an object or a module.
140
141  This type of caching is particularly useful for computationally intensive
142  functions with few frequently used combinations of input arguments. Note that
143  if the inputs or output are very large caching might not save time because
144  disc access may dominate the execution time.
145
146  If the function definition changes after a result has been cached it will be
147  detected by examining the functions bytecode (co_code, co_consts,
148  func_defualts, co_argcount) and it will be recomputed.
149
150  LIMITATIONS:
151    1 Caching uses the apply function and will work with anything that can be
152      pickled, so any limitation in apply or pickle extends to caching.
153    2 A function to be cached should not depend on global variables
154      as wrong results may occur if globals are changed after a result has
155      been cached.
156
157  -----------------------------------------------------------------------------
158  Additional functionality:
159
160  Keyword args
161    Keyword arguments (kwargs) can be added as a dictionary of keyword: value
162    pairs, following the syntax of the built-in function apply().
163    A Python function call of the form
164   
165      result = func(arg1,...,argn, kwarg1=val1,...,kwargm=valm)   
166
167    is then cached as follows
168
169      from caching import cache
170      result = cache(func,(arg1,...,argn), {kwarg1:val1,...,kwargm:valm})
171   
172    The default value of kwargs is {} 
173
174  Explicit dependencies:
175    The call
176      cache(func,(arg1,...,argn),dependencies = <list of filenames>)
177    Checks the size, creation time and modification time of each listed file.
178    If any file has changed the function is recomputed and the results stored
179    again.
180
181  Specify caching directory:
182    The call
183      cache(func,(arg1,...,argn), cachedir = <cachedir>)
184    designates <cachedir> where cached data are stored. Use ~ to indicate users
185    home directory - not $HOME. The default is ~/.python_cache on a UNIX
186    platform and c:/.python_cache on a Win platform.
187
188  Silent operation:
189    The call
190      cache(func,(arg1,...,argn),verbose=0)
191    suppresses messages to standard output.
192
193  Compression:
194    The call
195      cache(func,(arg1,...,argn),compression=0)
196    disables compression. (Default: compression=1). If the requested compressed
197    or uncompressed file is not there, it'll try the other version.
198
199  Forced evaluation:
200    The call
201      cache(func,(arg1,...,argn),evaluate=1)
202    forces the function to evaluate even though cached data may exist.
203
204  Testing for presence of cached result:
205    The call
206      cache(func,(arg1,...,argn),test=1)
207    retrieves cached result if it exists, otherwise None. The function will not
208    be evaluated. If both evaluate and test are switched on, evaluate takes
209    precedence.
210   
211  Obtain cache filenames:
212    The call   
213      cache(func,(arg1,...,argn),return_filename=1)
214    returns the hashed base filename under which this function and its
215    arguments would be cached
216
217  Clearing cached results:
218    The call
219      cache(func,'clear')
220    clears all cached data for 'func' and
221      cache('clear')
222    clears all cached data.
223 
224    NOTE: The string 'clear' can be passed an *argument* to func using
225      cache(func,('clear',)) or cache(func,tuple(['clear'])).
226
227    New form of clear:
228      cache(func,(arg1,...,argn),clear=1)
229    clears cached data for particular combination func and args
230     
231  """
232
233  # Imports and input checks
234  #
235  import types, time, string
236
237  if not cachedir:
238    cachedir = options['cachedir']
239
240  if verbose == None:  # Do NOT write 'if not verbose:', it could be zero.
241    verbose = options['verbose']
242
243  if compression == None:  # Do NOT write 'if not compression:',
244                           # it could be zero.
245    compression = options['compression']
246
247  # Create cache directory if needed
248  #
249  CD = checkdir(cachedir,verbose)
250
251  # Handle the case cache('clear')
252  #
253  if type(func) == types.StringType:
254    if string.lower(func) == 'clear':
255      clear_cache(CD,verbose=verbose)
256      return
257
258  # Handle the case cache(func, 'clear')
259  #
260  if type(args) == types.StringType:
261    if string.lower(args) == 'clear':
262      clear_cache(CD,func,verbose=verbose)
263      return
264
265  # Force singleton arg into a tuple
266  #
267  if type(args) != types.TupleType:
268    args = tuple([args])
269 
270  # Check that kwargs is a dictionary
271  #
272  if type(kwargs) != types.DictType:
273    raise TypeError   
274   
275  #print 'hashing' #FIXME: make faster hashing function
276   
277  # Hash arguments (and keyword args) to integer
278  #
279  arghash = myhash((args,kwargs))
280
281  # Get sizes and timestamps for files listed in dependencies.
282  # Force singletons into a tuple.
283  #
284  if dependencies and type(dependencies) != types.TupleType \
285                  and type(dependencies) != types.ListType:
286    dependencies = tuple([dependencies])
287  deps = get_depstats(dependencies)
288
289  # Extract function name from func object
290  #
291  funcname = get_funcname(func)
292
293  # Create cache filename
294  #
295  FN = funcname+'['+`arghash`+']'  # The symbol '(' does not work under unix
296
297  if return_filename:
298    return(FN)
299
300  if clear:
301    for file_type in file_types:
302      file_name = CD+FN+'_'+file_type
303      for fn in [file_name, file_name + '.z']:
304        if os.access(fn, os.F_OK):             
305          if unix:
306            os.remove(fn)
307          else:
308            # FIXME: os.remove doesn't work under windows       
309            os.system('del '+fn)
310          if verbose is True:
311            print 'MESSAGE (caching): File %s deleted' %fn
312        ##else:
313        ##  print '%s was not accessed' %fn
314    return None
315
316
317  #-------------------------------------------------------------------       
318 
319  # Check if previous computation has been cached
320  #
321  if evaluate:
322    Retrieved = None  # Force evaluation of func regardless of caching status.
323    reason = 4
324  else:
325    (T, FN, Retrieved, reason, comptime, loadtime, compressed) = \
326      CacheLookup(CD, FN, func, args, kwargs, deps, verbose, compression, \
327                  dependencies)
328
329  if not Retrieved:
330    if test:  # Do not attempt to evaluate function
331      T = None
332    else:  # Evaluate function and save to cache
333      if verbose:
334       
335        msg1(funcname, args, kwargs,reason)
336
337      # Remove expired files automatically
338      #
339      if options['expire']:
340        DeleteOldFiles(CD,verbose)
341       
342      # Save args before function is evaluated in case
343      # they are modified by function
344      #
345      save_args_to_cache(CD,FN,args,kwargs,compression)
346
347      # Execute and time function with supplied arguments
348      #
349      t0 = time.time()
350      T = apply(func,args,kwargs)
351      #comptime = round(time.time()-t0)
352      comptime = time.time()-t0
353
354      if verbose:
355        msg2(funcname,args,kwargs,comptime,reason)
356
357      # Save results and estimated loading time to cache
358      #
359      loadtime = save_results_to_cache(T, CD, FN, func, deps, comptime, \
360                                       funcname, dependencies, compression)
361      if verbose:
362        msg3(loadtime, CD, FN, deps, compression)
363      compressed = compression
364
365  if options['savestat'] and (not test or Retrieved):
366  ##if options['savestat']:
367    addstatsline(CD,funcname,FN,Retrieved,reason,comptime,loadtime,compressed)
368
369  return(T)  # Return results in all cases
370
371# -----------------------------------------------------------------------------
372
373def cachestat(sortidx=4, period=-1, showuser=None, cachedir=None):
374  """Generate statistics of caching efficiency.
375
376  USAGE:
377    cachestat(sortidx, period, showuser, cachedir)
378
379  ARGUMENTS:
380    sortidx --  Index of field by which lists are (default: 4)
381                Legal values are
382                 0: 'Name'
383                 1: 'Hits'
384                 2: 'CPU'
385                 3: 'Time Saved'
386                 4: 'Gain(%)'
387                 5: 'Size'
388    period --   If set to -1 all available caching history is used.
389                If set 0 only the current month is used (default -1).
390    showuser -- Flag for additional table showing user statistics
391                (default: None).
392    cachedir -- Directory for cache files (default: options['cachedir']).
393
394  DESCRIPTION:
395    Logged caching statistics is converted into summaries of the form
396    --------------------------------------------------------------------------
397    Function Name   Hits   Exec(s)  Cache(s)  Saved(s)   Gain(%)      Size
398    --------------------------------------------------------------------------
399  """
400
401  __cachestat(sortidx, period, showuser, cachedir)
402  return
403
404# -----------------------------------------------------------------------------
405
406#Has mostly been moved to proper unit test
407def test(cachedir=None,verbose=0,compression=None):
408  """Test the functionality of caching.
409
410  USAGE:
411    test(verbose)
412
413  ARGUMENTS:
414    verbose --     Flag whether caching will output its statistics (default=0)
415    cachedir --    Directory for cache files (Default: options['cachedir'])
416    compression -- Flag zlib compression (Default: options['compression'])
417  """
418   
419  import string, time
420
421  # Initialise
422  #
423  import caching
424  reload(caching)
425
426  if not cachedir:
427    cachedir = options['cachedir']
428
429  if verbose is None:  # Do NOT write 'if not verbose:', it could be zero.
430    verbose = options['verbose']
431 
432  if compression == None:  # Do NOT write 'if not compression:',
433                           # it could be zero.
434    compression = options['compression']
435  else:
436    try:
437      set_option('compression', compression)
438    except:
439      test_error('Set option failed')     
440
441  try:
442    import zlib
443  except:
444    print
445    print '*** Could not find zlib, default to no-compression      ***'
446    print '*** Installing zlib will improve performance of caching ***'
447    print
448    compression = 0       
449    set_option('compression', compression)   
450 
451  print 
452  print_header_box('Testing caching module - please stand by')
453  print   
454
455  # Define a test function to be cached
456  #
457  def f(a,b,c,N,x=0,y='abcdefg'):
458    """f(a,b,c,N)
459       Do something time consuming and produce a complex result.
460    """
461
462    import string
463
464    B = []
465    for n in range(N):
466      s = str(n+2.0/(n + 4.0))+'.a'*10
467      B.append((a,b,c,s,n,x,y))
468    return(B)
469   
470  # Check that default cachedir is OK
471  #     
472  CD = checkdir(cachedir,verbose)   
473   
474   
475  # Make a dependency file
476  #   
477  try:
478    DepFN = CD + 'testfile.tmp'
479    DepFN_wildcard = CD + 'test*.tmp'
480    Depfile = open(DepFN,'w')
481    Depfile.write('We are the knights who say NI!')
482    Depfile.close()
483    test_OK('Wrote file %s' %DepFN)
484  except:
485    test_error('Could not open file %s for writing - check your environment' \
486               % DepFN)
487
488  # Check set_option (and switch stats off
489  #   
490  try:
491    set_option('savestat',0)
492    assert(options['savestat'] == 0)
493    test_OK('Set option')
494  except:
495    test_error('Set option failed')   
496   
497  # Make some test input arguments
498  #
499  N = 5000  #Make N fairly small here
500
501  a = [1,2]
502  b = ('Thou shalt count the number three',4)
503  c = {'Five is right out': 6, (7,8): 9}
504  x = 3
505  y = 'holy hand granate'
506
507  # Test caching
508  #
509  if compression:
510    comprange = 2
511  else:
512    comprange = 1
513
514  for comp in range(comprange):
515 
516    # Evaluate and store
517    #
518    try:
519      T1 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, evaluate=1, \
520                         verbose=verbose, compression=comp)
521      if comp:                   
522        test_OK('Caching evaluation with compression')
523      else:     
524        test_OK('Caching evaluation without compression')     
525    except:
526      if comp:
527        test_error('Caching evaluation with compression failed - try caching.test(compression=0)')
528      else:
529        test_error('Caching evaluation failed - try caching.test(verbose=1)')
530
531    # Retrieve
532    #                           
533    try:                         
534      T2 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, \
535                         compression=comp) 
536
537      if comp:                   
538        test_OK('Caching retrieval with compression')
539      else:     
540        test_OK('Caching retrieval without compression')     
541    except:
542      if comp:
543        test_error('Caching retrieval with compression failed - try caching.test(compression=0)')
544      else:                                     
545        test_error('Caching retrieval failed - try caching.test(verbose=1)')
546
547    # Reference result
548    #   
549    T3 = f(a,b,c,N,x=x,y=y)  # Compute without caching
550   
551    if T1 == T2 and T2 == T3:
552      if comp:
553        test_OK('Basic caching functionality (with compression)')
554      else:
555        test_OK('Basic caching functionality (without compression)')
556    else:
557      test_error('Cached result does not match computed result')
558
559
560  # Test return_filename
561  #   
562  try:
563    FN = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, \
564                       return_filename=1)   
565    assert(FN[:2] == 'f[')
566    test_OK('Return of cache filename')
567  except:
568    test_error('Return of cache filename failed')
569
570  # Test existence of cachefiles
571 
572  try:
573    (datafile,compressed0) = myopen(CD+FN+'_'+file_types[0],"rb",compression)
574    (argsfile,compressed1) = myopen(CD+FN+'_'+file_types[1],"rb",compression)
575    (admfile,compressed2) =  myopen(CD+FN+'_'+file_types[2],"rb",compression)
576    test_OK('Presence of cache files')
577    datafile.close()
578    argsfile.close()
579    admfile.close()
580  except:
581    test_error('Expected cache files did not exist') 
582             
583  # Test 'test' function when cache is present
584  #     
585  try:
586    #T1 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, \
587    #                   evaluate=1) 
588    T4 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, test=1)
589    assert(T1 == T4)
590
591    test_OK("Option 'test' when cache file present")
592  except:
593    test_error("Option 'test' when cache file present failed")     
594
595  # Test that 'clear' works
596  #
597  #try:
598  #  caching.cache(f,'clear',verbose=verbose)
599  #  test_OK('Clearing of cache files')
600  #except:
601  #  test_error('Clear does not work')
602  try:
603    caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, clear=1)   
604    test_OK('Clearing of cache files')
605  except:
606    test_error('Clear does not work') 
607
608 
609
610  # Test 'test' function when cache is absent
611  #     
612  try:
613    T4 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, test=1)
614    assert(T4 is None)
615    test_OK("Option 'test' when cache absent")
616  except:
617    test_error("Option 'test' when cache absent failed")     
618         
619  # Test dependencies
620  #
621  T1 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, \
622                       dependencies=DepFN) 
623  T2 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, \
624                       dependencies=DepFN)                     
625                       
626  if T1 == T2:
627    test_OK('Basic dependencies functionality')
628  else:
629    test_error('Dependencies do not work')
630
631  # Test basic wildcard dependency
632  #
633  T3 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, \
634                       dependencies=DepFN_wildcard)                     
635   
636  if T1 == T3:
637    test_OK('Basic dependencies with wildcard functionality')
638  else:
639    test_error('Dependencies with wildcards do not work')
640
641
642  # Test that changed timestamp in dependencies triggers recomputation
643 
644  # Modify dependency file
645  Depfile = open(DepFN,'a')
646  Depfile.write('You must cut down the mightiest tree in the forest with a Herring')
647  Depfile.close()
648 
649  T3 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, \
650                       dependencies=DepFN, test = 1)                     
651 
652  if T3 is None:
653    test_OK('Changed dependencies recognised')
654  else:
655    test_error('Changed dependencies not recognised')   
656 
657  # Test recomputation when dependencies have changed
658  #
659  T3 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose, \
660                       dependencies=DepFN)                       
661  if T1 == T3:
662    test_OK('Recomputed value with changed dependencies')
663  else:
664    test_error('Recomputed value with changed dependencies failed')
665
666  # Performance test (with statistics)
667  # Don't really rely on this as it will depend on specific computer.
668  #
669
670  set_option('savestat',1)
671
672  N = 20*N   #Should be large on fast computers...
673  tt = time.time()
674  T1 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose)
675  t1 = time.time() - tt
676 
677  tt = time.time()
678  T2 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=verbose)
679  t2 = time.time() - tt
680 
681  if T1 == T2:
682    if t1 > t2:
683      test_OK('Performance test: relative time saved = %s pct' \
684              %str(round((t1-t2)*100/t1,2)))
685    #else:
686    #  print 'WARNING: Performance a bit low - this could be specific to current platform'
687  else:       
688    test_error('Basic caching failed for new problem')
689           
690  # Test presence of statistics file
691  #
692  try: 
693    DIRLIST = os.listdir(CD)
694    SF = []
695    for FN in DIRLIST:
696      if string.find(FN,statsfile) >= 0:
697        fid = open(CD+FN,'r')
698        fid.close()
699    test_OK('Statistics files present') 
700  except:
701    test_OK('Statistics files cannot be opened')         
702     
703  print_header_box('Show sample output of the caching function:')
704 
705  T2 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=0)
706  T2 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=0)
707  T2 = caching.cache(f,(a,b,c,N), {'x':x, 'y':y}, verbose=1)
708 
709  print_header_box('Show sample output of cachestat():')
710  if unix:
711    cachestat()   
712  else:
713    try:
714      import time
715      t = time.strptime('2030','%Y')
716      cachestat()
717    except: 
718      print 'caching.cachestat() does not work here, because it'
719      print 'relies on time.strptime() which is unavailable in Windows'
720     
721  print
722  test_OK('Caching self test completed')   
723     
724           
725  # Test setoption (not yet implemented)
726  #
727
728 
729#==============================================================================
730# Auxiliary functions
731#==============================================================================
732
733# Import pickler
734# cPickle is used by functions mysave, myload, and compare
735#
736import cPickle  # 10 to 100 times faster than pickle
737pickler = cPickle
738
739# Local immutable constants
740#
741comp_level = 1              # Compression level for zlib.
742                            # comp_level = 1 works well.
743textwidth1 = 16             # Text width of key fields in report forms.
744#textwidth2 = 132            # Maximal width of textual representation of
745textwidth2 = 300            # Maximal width of textual representation of
746                            # arguments.
747textwidth3 = 16             # Initial width of separation lines. Is modified.
748textwidth4 = 50             # Text width in test_OK()
749statsfile  = '.cache_stat'  # Basefilename for cached statistics.
750                            # It will reside in the chosen cache directory.
751
752file_types = ['Result',     # File name extension for cached function results.
753              'Args',       # File name extension for stored function args.
754              'Admin']      # File name extension for administrative info.
755
756Reason_msg = ['OK',         # Verbose reasons for recomputation
757              'No cached result', 
758              'Dependencies have changed', 
759              'Byte code or arguments have changed',
760              'Recomputation was requested by caller',
761              'Cached file was unreadable']             
762             
763# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
764
765def CacheLookup(CD, FN, func, args, kwargs, deps, verbose, compression, 
766                dependencies):
767  """Determine whether cached result exists and return info.
768
769  USAGE:
770    (T, FN, Retrieved, reason, comptime, loadtime, compressed) = \ 
771    CacheLookup(CD, FN, func, args, kwargs, deps, verbose, compression, \
772                dependencies)
773
774  INPUT ARGUMENTS:
775    CD --            Cache Directory
776    FN --            Suggested cache file name
777    func --          Function object
778    args --          Tuple of arguments
779    kwargs --        Dictionary of keyword arguments   
780    deps --          Dependencies time stamps
781    verbose --       Flag text output
782    compression --   Flag zlib compression
783    dependencies --  Given list of dependencies
784   
785  OUTPUT ARGUMENTS:
786    T --             Cached result if present otherwise None
787    FN --            File name under which new results must be saved
788    Retrieved --     True if a valid cached result was found
789    reason --        0: OK (if Retrieved),
790                     1: No cached result,
791                     2: Dependencies have changed,
792                     3: Arguments or Bytecode have changed
793                     4: Recomputation was forced
794    comptime --      Number of seconds it took to computed cachged result
795    loadtime --      Number of seconds it took to load cached result
796    compressed --    Flag (0,1) if cached results were compressed or not
797
798  DESCRIPTION:
799    Determine if cached result exists as follows:
800    Load in saved arguments and bytecode stored under hashed filename.
801    If they are identical to current arguments and bytecode and if dependencies
802    have not changed their time stamp, then return cached result.
803
804    Otherwise return filename under which new results should be cached.
805    Hash collisions are handled recursively by calling CacheLookup again with a
806    modified filename.
807  """
808
809  import time, string, types
810
811  # Assess whether cached result exists - compressed or not.
812  #
813  if verbose:
814    print 'Caching: looking for cached files %s_{%s,%s,%s}.z'\
815           %(CD+FN, file_types[0], file_types[1], file_types[2])
816  (datafile,compressed0) = myopen(CD+FN+'_'+file_types[0],"rb",compression)
817  (argsfile,compressed1) = myopen(CD+FN+'_'+file_types[1],"rb",compression)
818  (admfile,compressed2) =  myopen(CD+FN+'_'+file_types[2],"rb",compression)
819
820  if not (argsfile and datafile and admfile) or \
821     not (compressed0 == compressed1 and compressed0 == compressed2):
822    # Cached result does not exist or files were compressed differently
823    #
824    # This will ensure that evaluation will take place unless all files are
825    # present.
826
827    reason = 1
828    return(None,FN,None,reason,None,None,None) #Recompute using same filename
829
830  compressed = compressed0  # Remember if compressed files were actually used
831  datafile.close()
832
833  # Retrieve arguments and adm. info
834  #
835  R, reason = myload(argsfile,compressed)  # The original arguments
836  argsfile.close()
837   
838  ##if R == None and reason > 0:
839  if reason > 0:
840    return(None,FN,None,reason,None,None,None) #Recompute using same filename
841  else:   
842    (argsref, kwargsref) = R
843
844  R, reason = myload(admfile,compressed)
845  admfile.close() 
846  ##if R == None and reason > 0:
847  if reason > 0:
848    return(None,FN,None,reason,None,None,None) #Recompute using same filename
849
850 
851  depsref  = R[0]  # Dependency statistics
852  comptime = R[1]  # The computation time
853  coderef  = R[2]  # The byte code
854  funcname = R[3]  # The function name
855
856  # Check if dependencies have changed
857  #
858  if dependencies and not compare(depsref,deps):
859    if verbose:
860      print 'MESSAGE (caching.py): Dependencies', dependencies, \
861            'have changed - recomputing'
862    # Don't use cached file - recompute
863    reason = 2
864    return(None,FN,None,reason,None,None,None)
865
866  # Get bytecode from func
867  #
868  bytecode = get_bytecode(func)
869
870  #print compare(argsref,args),
871  #print compare(kwargsref,kwargs),
872  #print compare(bytecode,coderef)
873
874  # Check if arguments or bytecode have changed
875  #
876  if compare(argsref,args) and compare(kwargsref,kwargs) and \
877     (not options['bytecode'] or compare(bytecode,coderef)):
878
879    # Arguments and dependencies match. Get cached results
880    #
881    T, loadtime, compressed, reason = load_from_cache(CD,FN,compressed)
882    ###if T == None and reason > 0:  #This doesn't work if T is a numeric array
883    if reason > 0:
884      return(None,FN,None,reason,None,None,None) #Recompute using same FN
885
886    Retrieved = 1
887    reason = 0
888
889    if verbose:
890      msg4(funcname,args,kwargs,deps,comptime,loadtime,CD,FN,compressed)
891
892      if loadtime >= comptime:
893        print 'WARNING (caching.py): Caching did not yield any gain.'
894        print '                      Consider executing function ',
895        print '('+funcname+') without caching.'
896  else:
897
898    # Non matching arguments or bytecodes signify a hash-collision.
899    # This is resolved by recursive search of cache filenames
900    # until either a matching or an unused filename is found.
901    #
902    (T,FN,Retrieved,reason,comptime,loadtime,compressed) = \
903       CacheLookup(CD,FN+'x',func,args,kwargs,deps,verbose,compression, \
904                   dependencies)
905
906    # DEBUGGING
907    # if not Retrieved:
908    #   print 'Arguments did not match'
909    # else:
910    #   print 'Match found !'
911    if not Retrieved:
912      reason = 3     #The real reason is that args or bytecodes have changed.
913                     #Not that the recursive seach has found an unused filename
914   
915  return((T, FN, Retrieved, reason, comptime, loadtime, compressed))
916
917# -----------------------------------------------------------------------------
918
919def clear_cache(CD,func=None, verbose=None):
920  """Clear cache for func.
921
922  USAGE:
923     clear(CD, func, verbose)
924
925  ARGUMENTS:
926     CD --       Caching directory (required)
927     func --     Function object (default: None)
928     verbose --  Flag verbose output (default: None)
929
930  DESCRIPTION:
931
932    If func == None, clear everything,
933    otherwise clear only files pertaining to func.
934  """
935
936  import os, re
937   
938  if CD[-1] != os.sep:
939    CD = CD+os.sep
940 
941  if verbose == None:
942    verbose = options['verbose']
943
944  # FIXME: Windows version needs to be tested
945
946  if func:
947    funcname = get_funcname(func)
948    if verbose:
949      print 'MESSAGE (caching.py): Clearing', CD+funcname+'*'
950
951    file_names = os.listdir(CD)
952    for file_name in file_names:
953      #RE = re.search('^' + funcname,file_name)  #Inefficient
954      #if RE:
955      if file_name[:len(funcname)] == funcname:
956        if unix:
957          os.remove(CD+file_name)
958        else:
959          os.system('del '+CD+file_name)
960          # FIXME: os.remove doesn't work under windows
961  else:
962    file_names = os.listdir(CD)
963    if len(file_names) > 0:
964      if verbose:
965        print 'MESSAGE (caching.py): Remove the following files:'
966        for file_name in file_names:
967            print file_name
968
969        A = raw_input('Delete (Y/N)[N] ?')
970      else:
971        A = 'Y' 
972       
973      if A == 'Y' or A == 'y':
974        for file_name in file_names:
975          if unix:
976            os.remove(CD+file_name)
977          else:
978            os.system('del '+CD+file_name)
979            # FIXME: os.remove doesn't work under windows
980          #exitcode=os.system('/bin/rm '+CD+'* 2> /dev/null')
981
982# -----------------------------------------------------------------------------
983
984def DeleteOldFiles(CD,verbose=None):
985  """Remove expired files
986
987  USAGE:
988    DeleteOldFiles(CD,verbose=None)
989  """
990
991  if verbose == None:
992    verbose = options['verbose']
993
994  maxfiles = options['maxfiles']
995
996  # FIXME: Windows version
997
998  import os
999  block = 1000  # How many files to delete per invokation
1000  Files = os.listdir(CD)
1001  numfiles = len(Files)
1002  if not unix: return  # FIXME: Windows case ?
1003
1004  if numfiles > maxfiles:
1005    delfiles = numfiles-maxfiles+block
1006    if verbose:
1007      print 'Deleting '+`delfiles`+' expired files:'
1008      os.system('ls -lur '+CD+'* | head -' + `delfiles`)            # List them
1009    os.system('ls -ur '+CD+'* | head -' + `delfiles` + ' | xargs /bin/rm')
1010                                                                  # Delete them
1011    # FIXME: Replace this with os.listdir and os.remove
1012
1013# -----------------------------------------------------------------------------
1014
1015def save_args_to_cache(CD,FN,args,kwargs,compression):
1016  """Save arguments to cache
1017
1018  USAGE:
1019    save_args_to_cache(CD,FN,args,kwargs,compression)
1020  """
1021
1022  import time, os, sys, types
1023
1024  (argsfile, compressed) = myopen(CD+FN+'_'+file_types[1], 'wb', compression)
1025
1026  if not argsfile:
1027    if verbose:
1028      print 'ERROR (caching): Could not open %s' %argsfile.name
1029    raise IOError
1030
1031  mysave((args,kwargs),argsfile,compression)  # Save args and kwargs to cache
1032  argsfile.close()
1033
1034  # Change access rights if possible
1035  #
1036  #if unix:
1037  #  try:
1038  #    exitcode=os.system('chmod 666 '+argsfile.name)
1039  #  except:
1040  #    pass
1041  #else:
1042  #  pass  # FIXME: Take care of access rights under Windows
1043
1044  return
1045
1046# -----------------------------------------------------------------------------
1047
1048def save_results_to_cache(T, CD, FN, func, deps, comptime, funcname,
1049                          dependencies, compression):
1050  """Save computed results T and admin info to cache
1051
1052  USAGE:
1053    save_results_to_cache(T, CD, FN, func, deps, comptime, funcname,
1054                          dependencies, compression)
1055  """
1056
1057  import time, os, sys, types
1058
1059  (datafile, compressed1) = myopen(CD+FN+'_'+file_types[0],'wb',compression)
1060  (admfile, compressed2) = myopen(CD+FN+'_'+file_types[2],'wb',compression)
1061
1062  if not datafile:
1063    if verbose:
1064      print 'ERROR (caching): Could not open %s' %datafile.name
1065    raise IOError
1066
1067  if not admfile:
1068    if verbose:
1069      print 'ERROR (caching): Could not open %s' %admfile.name
1070    raise IOError
1071
1072  t0 = time.time()
1073
1074  mysave(T,datafile,compression)  # Save data to cache
1075  datafile.close()
1076  #savetime = round(time.time()-t0,2)
1077  savetime = time.time()-t0 
1078
1079  bytecode = get_bytecode(func)  # Get bytecode from function object
1080  admtup = (deps, comptime, bytecode, funcname)  # Gather admin info
1081
1082  mysave(admtup,admfile,compression)  # Save admin info to cache
1083  admfile.close()
1084
1085  # Change access rights if possible
1086  #
1087  #if unix:
1088  #  try:
1089  #    exitcode=os.system('chmod 666 '+datafile.name)
1090  #    exitcode=os.system('chmod 666 '+admfile.name)
1091  #  except:
1092  #    pass
1093  #else:
1094  #  pass  # FIXME: Take care of access rights under Windows
1095
1096  return(savetime)
1097
1098# -----------------------------------------------------------------------------
1099
1100def load_from_cache(CD,FN,compression):
1101  """Load previously cached data from file FN
1102
1103  USAGE:
1104    load_from_cache(CD,FN,compression)
1105  """
1106
1107  import time
1108
1109  (datafile, compressed) = myopen(CD+FN+'_'+file_types[0],"rb",compression)
1110  t0 = time.time()
1111  T, reason = myload(datafile,compressed)
1112  #loadtime = round(time.time()-t0,2)
1113  loadtime = time.time()-t0
1114  datafile.close() 
1115
1116  return T, loadtime, compressed, reason
1117
1118# -----------------------------------------------------------------------------
1119
1120def myopen(FN,mode,compression=1):
1121  """Open file FN using given mode
1122
1123  USAGE:
1124    myopen(FN,mode,compression=1)
1125
1126  ARGUMENTS:
1127    FN --           File name to be opened
1128    mode --         Open mode (as in open)
1129    compression --  Flag zlib compression
1130
1131  DESCRIPTION:
1132     if compression
1133       Attempt first to open FN + '.z'
1134       If this fails try to open FN
1135     else do the opposite
1136     Return file handle plus info about whether it was compressed or not.
1137  """
1138
1139  import string
1140
1141  # Determine if file exists already (if writing was requested)
1142  # This info is only used to determine if access modes should be set
1143  #
1144  if 'w' in mode or 'a' in mode:
1145    try:
1146      file = open(FN+'.z','r')
1147      file.close()
1148      new_file = 0
1149    except:
1150      try:
1151        file = open(FN,'r') 
1152        file.close()
1153        new_file = 0
1154      except:
1155        new_file = 1
1156  else:
1157    new_file = 0 #Assume it exists if mode was not 'w'
1158 
1159
1160  compressed = 0
1161  if compression:
1162    try:
1163      file = open(FN+'.z',mode)
1164      compressed = 1
1165    except:
1166      try:
1167        file = open(FN,mode)
1168      except:
1169        file = None
1170  else:
1171    try:
1172      file = open(FN,mode)
1173    except:
1174      try:
1175        file = open(FN+'.z',mode)
1176        compressed = 1
1177      except:
1178        file = None
1179
1180  # Now set access rights if it is a new file
1181  #
1182  if file and new_file:
1183    if unix:
1184      exitcode=os.system('chmod 666 '+file.name)
1185    else:
1186      pass  # FIXME: Take care of access rights under Windows
1187
1188  return(file,compressed)
1189
1190# -----------------------------------------------------------------------------
1191
1192def myload(file, compressed):
1193  """Load data from file
1194
1195  USAGE:
1196    myload(file, compressed)
1197  """
1198
1199  reason = 0
1200  try:
1201    if compressed:
1202      import zlib
1203
1204      RsC = file.read()
1205      try:
1206        Rs  = zlib.decompress(RsC)
1207      except:
1208        #  File "./caching.py", line 1032, in load_from_cache
1209        #  T = myload(datafile,compressed)
1210        #  File "./caching.py", line 1124, in myload
1211        #  Rs  = zlib.decompress(RsC)
1212        #  zlib.error: Error -5 while decompressing data
1213        #print 'ERROR (caching): Could not decompress ', file.name
1214        #raise Exception
1215        reason = 5  #(Unreadable file)
1216        return None, reason 
1217     
1218     
1219      del RsC  # Free up some space
1220      R   = pickler.loads(Rs)
1221    else:
1222      try:
1223        R = pickler.load(file)
1224      #except EOFError, e:
1225      except:
1226        #Catch e.g., file with 0 length or corrupted
1227        reason = 5  #(Unreadable file)
1228        return None, reason
1229     
1230  except MemoryError:
1231    import sys
1232    if options['verbose']:
1233      print 'ERROR (caching): Out of memory while loading %s, aborting' \
1234            %(file.name)
1235
1236    # Raise the error again for now
1237    #
1238    raise MemoryError
1239
1240  return R, reason
1241
1242# -----------------------------------------------------------------------------
1243
1244def mysave(T,file,compression):
1245  """Save data T to file
1246
1247  USAGE:
1248    mysave(T,file,compression)
1249
1250  """
1251
1252  bin = options['bin']
1253
1254  if compression:
1255    try:
1256      import zlib
1257    except:
1258      print
1259      print '*** Could not find zlib ***'
1260      print '*** Try to run caching with compression off ***'
1261      print "*** caching.set_option('compression', 0) ***"
1262      raise Exception
1263     
1264
1265    try:
1266      Ts  = pickler.dumps(T, bin)
1267    except MemoryError:
1268      msg = '****WARNING (caching.py): Could not pickle data for compression.'
1269      msg += ' Try using compression = False'
1270      raise MemoryError, msg
1271    else: 
1272      #Compressed pickling     
1273      TsC = zlib.compress(Ts, comp_level)
1274      file.write(TsC)
1275  else:
1276      #Uncompressed pickling
1277      pickler.dump(T, file, bin)
1278
1279      # FIXME: This may not work on Windoze network drives.
1280      # The error msg is IOError: [Errno 22] Invalid argument
1281      # Testing with small files was OK, though.
1282      # I think this is an OS problem.
1283
1284      # Excerpt from http://www.ultraseek.com/support/faqs/4173.html
1285     
1286# The error is caused when there is a problem with server disk access (I/0). This happens at the OS level, and there is no controlling these errors through the Ultraseek application.
1287#
1288#Ultraseek contains an embedded Python interpreter. The exception "exceptions.IOError: [Errno 22] Invalid argument" is generated by the Python interpreter. The exception is thrown when a disk access operation fails due to an I/O-related reason.
1289#
1290#The following extract is taken from the site http://www.python.org:
1291#
1292#---------------------------------------------------------------------------------------------
1293#exception IOError
1294#Raised when an I/O operation (such as a print statement, the built-in open() function or a method of a file object) fails for an I/O-related reason, e.g., ``file not found'' or ``disk full''.
1295#This class is derived from EnvironmentError. See the discussion above for more information on exception instance attributes.
1296#---------------------------------------------------------------------------------------------
1297#
1298#The error code(s) that accompany exceptions are described at:
1299#http://www.python.org/dev/doc/devel//lib/module-errno.html
1300#
1301#You can view several postings on this error message by going to http://www.python.org, and typing the below into the search box:
1302#
1303#exceptions.IOError invalid argument Errno 22
1304       
1305      #try:
1306      #  pickler.dump(T,file,bin)
1307      #except IOError, e:
1308      #  print e
1309      #  msg = 'Could not store to %s, bin=%s' %(file, bin)
1310      #  raise msg
1311     
1312
1313# -----------------------------------------------------------------------------
1314
1315def myhash(T):
1316  """Compute hashed integer from hashable values of tuple T
1317
1318  USAGE:
1319    myhash(T)
1320
1321  ARGUMENTS:
1322    T -- Tuple
1323  """
1324
1325  import types
1326
1327  # On some architectures None, False and True gets different hash values
1328  if T is None:
1329    return(-1)
1330  if T is False:
1331    return(0)
1332  if T is True:
1333    return(1)
1334
1335  # Get hash vals for hashable entries
1336  #
1337  if type(T) == types.TupleType or type(T) == types.ListType:
1338    hvals = []
1339    for k in range(len(T)):
1340      h = myhash(T[k])
1341      hvals.append(h)
1342    val = hash(tuple(hvals))
1343  elif type(T) == types.DictType:
1344    val = dicthash(T)
1345  else:
1346    try:
1347      val = hash(T)
1348    except:
1349      val = 1
1350      try:
1351        import Numeric
1352        if type(T) == Numeric.ArrayType:
1353          hvals = []       
1354          for e in T:
1355            h = myhash(e)
1356            hvals.append(h)         
1357          val = hash(tuple(hvals))
1358        else:
1359          val = 1  #Could implement other Numeric types here
1360      except:   
1361        pass
1362
1363  return(val)
1364
1365# -----------------------------------------------------------------------------
1366
1367def dicthash(D):
1368  """Compute hashed integer from hashable values of dictionary D
1369
1370  USAGE:
1371    dicthash(D)
1372  """
1373
1374  keys = D.keys()
1375
1376  # Get hash values for hashable entries
1377  #
1378  hvals = []
1379  for k in range(len(keys)):
1380    try:
1381      h = myhash(D[keys[k]])
1382      hvals.append(h)
1383    except:
1384      pass
1385
1386  # Hash obtained values into one value
1387  #
1388  return(hash(tuple(hvals)))
1389
1390# -----------------------------------------------------------------------------
1391
1392def compare(A,B):
1393  """Safe comparison of general objects
1394
1395  USAGE:
1396    compare(A,B)
1397
1398  DESCRIPTION:
1399    Return 1 if A and B they are identical, 0 otherwise
1400  """
1401
1402  try:
1403    identical = (A == B)
1404  except:
1405    try:
1406      identical = (pickler.dumps(A) == pickler.dumps(B))
1407    except:
1408      identical = 0
1409
1410  return(identical)
1411
1412# -----------------------------------------------------------------------------
1413
1414def nospace(s):
1415  """Replace spaces in string s with underscores
1416
1417  USAGE:
1418    nospace(s)
1419
1420  ARGUMENTS:
1421    s -- string
1422  """
1423
1424  import string
1425
1426  newstr = ''
1427  for i in range(len(s)):
1428    if s[i] == ' ':
1429      newstr = newstr+'_'
1430    else:
1431      newstr = newstr+s[i]
1432
1433  return(newstr)
1434
1435# -----------------------------------------------------------------------------
1436
1437def get_funcname(func):
1438  """Retrieve name of function object func (depending on its type)
1439
1440  USAGE:
1441    get_funcname(func)
1442  """
1443
1444  import types, string
1445
1446  if type(func) == types.FunctionType:
1447    funcname = func.func_name
1448  elif type(func) == types.BuiltinFunctionType:
1449    funcname = func.__name__
1450  else:
1451    tab = string.maketrans("<>'","   ")
1452    tmp = string.translate(`func`,tab)
1453    tmp = string.split(tmp)
1454    funcname = string.join(tmp)
1455
1456    # Truncate memory address as in
1457    # class __main__.Dummy at 0x00A915D0
1458    index = funcname.find('at 0x')
1459    if index >= 0:
1460      funcname = funcname[:index+5] # Keep info that there is an address
1461
1462  funcname = nospace(funcname)
1463  return(funcname)
1464
1465# -----------------------------------------------------------------------------
1466
1467def get_bytecode(func):
1468  """ Get bytecode from function object.
1469
1470  USAGE:
1471    get_bytecode(func)
1472  """
1473
1474  import types
1475
1476  if type(func) == types.FunctionType:
1477    bytecode = func.func_code.co_code
1478    consts = func.func_code.co_consts
1479    argcount = func.func_code.co_argcount   
1480    defaults = func.func_defaults     
1481  elif type(func) == types.MethodType:
1482    bytecode = func.im_func.func_code.co_code
1483    consts =  func.im_func.func_code.co_consts
1484    argcount =  func.im_func.func_code.co_argcount   
1485    defaults = func.im_func.func_defaults         
1486  else:
1487    #raise Exception  #Test only
1488    bytecode = None   #Built-in functions are assumed not to change
1489    consts = 0
1490    argcount = 0
1491    defaults = 0
1492
1493  return (bytecode, consts, argcount, defaults)
1494
1495# -----------------------------------------------------------------------------
1496
1497def get_depstats(dependencies):
1498  """ Build dictionary of dependency files and their size, mod. time and ctime.
1499
1500  USAGE:
1501    get_depstats(dependencies):
1502  """
1503
1504  import types
1505
1506  d = {}
1507  if dependencies:
1508
1509    #Expand any wildcards
1510    import glob
1511    expanded_dependencies = []
1512    for FN in dependencies:
1513      expanded_FN = glob.glob(FN)
1514     
1515      expanded_dependencies += expanded_FN
1516
1517   
1518    for FN in expanded_dependencies:
1519      if not type(FN) == types.StringType:
1520        errmsg = 'ERROR (caching.py): Dependency must be a string.\n'
1521        errmsg += '                    Dependency given: %s' %FN
1522        raise Exception, errmsg     
1523      if not os.access(FN,os.F_OK):
1524        errmsg = 'ERROR (caching.py): Dependency '+FN+' does not exist.'
1525        raise Exception, errmsg
1526      (size,atime,mtime,ctime) = filestat(FN)
1527
1528      # We don't use atime because that would cause recomputation every time.
1529      # We don't use ctime because that is irrelevant and confusing for users.
1530      d.update({FN : (size,mtime)})
1531
1532  return(d)
1533
1534# -----------------------------------------------------------------------------
1535
1536def filestat(FN):
1537  """A safe wrapper using os.stat to get basic file statistics
1538     The built-in os.stat breaks down if file sizes are too large (> 2GB ?)
1539
1540  USAGE:
1541    filestat(FN)
1542
1543  DESCRIPTION:
1544     Must compile Python with
1545     CFLAGS="`getconf LFS_CFLAGS`" OPT="-g -O2 $CFLAGS" \
1546              configure
1547     as given in section 8.1.1 Large File Support in the Libray Reference
1548  """
1549
1550  import os, time
1551
1552  try:
1553    stats = os.stat(FN)
1554    size  = stats[6]
1555    atime = stats[7]
1556    mtime = stats[8]
1557    ctime = stats[9]
1558  except:
1559
1560    # Hack to get the results anyway (works only on Unix at the moment)
1561    #
1562    print 'Hack to get os.stat when files are too large'
1563
1564    if unix:
1565      tmp = '/tmp/cach.tmp.'+`time.time()`+`os.getpid()`
1566      # Unique filename, FIXME: Use random number
1567
1568      # Get size and access time (atime)
1569      #
1570      exitcode=os.system('ls -l --full-time --time=atime '+FN+' > '+tmp)
1571      (size,atime) = get_lsline(tmp)
1572
1573      # Get size and modification time (mtime)
1574      #
1575      exitcode=os.system('ls -l --full-time '+FN+' > '+tmp)
1576      (size,mtime) = get_lsline(tmp)
1577
1578      # Get size and ctime
1579      #
1580      exitcode=os.system('ls -l --full-time --time=ctime '+FN+' > '+tmp)
1581      (size,ctime) = get_lsline(tmp)
1582
1583      try:
1584        exitcode=os.system('rm '+tmp)
1585        # FIXME: Gives error if file doesn't exist
1586      except:
1587        pass
1588    else:
1589      pass
1590      raise Exception  # FIXME: Windows case
1591
1592  return(long(size),atime,mtime,ctime)
1593
1594# -----------------------------------------------------------------------------
1595
1596def get_lsline(FN):
1597  """get size and time for filename
1598
1599  USAGE:
1600    get_lsline(file_name)
1601
1602  DESCRIPTION:
1603    Read in one line 'ls -la' item from file (generated by filestat) and
1604    convert time to seconds since epoch. Return file size and time.
1605  """
1606
1607  import string, time
1608
1609  f = open(FN,'r')
1610  info = f.read()
1611  info = string.split(info)
1612
1613  size = info[4]
1614  week = info[5]
1615  mon  = info[6]
1616  day  = info[7]
1617  hour = info[8]
1618  year = info[9]
1619
1620  str = week+' '+mon+' '+day+' '+hour+' '+year
1621  timetup = time.strptime(str)
1622  t = time.mktime(timetup)
1623  return(size, t)
1624
1625# -----------------------------------------------------------------------------
1626
1627def checkdir(CD,verbose=None):
1628  """Check or create caching directory
1629
1630  USAGE:
1631    checkdir(CD,verbose):
1632
1633  ARGUMENTS:
1634    CD -- Directory
1635    verbose -- Flag verbose output (default: None)
1636
1637  DESCRIPTION:
1638    If CD does not exist it will be created if possible
1639  """
1640
1641  import os
1642  import os.path
1643
1644  if CD[-1] != os.sep: 
1645    CD = CD + os.sep  # Add separator for directories
1646
1647  CD = os.path.expanduser(CD) # Expand ~ or ~user in pathname
1648  if not (os.access(CD,os.R_OK and os.W_OK) or CD == ''):
1649    try:
1650      exitcode=os.mkdir(CD)
1651
1652      # Change access rights if possible
1653      #
1654      if unix:
1655        exitcode=os.system('chmod 777 '+CD)
1656      else:
1657        pass  # FIXME: What about acces rights under Windows?
1658      if verbose: print 'MESSAGE: Directory', CD, 'created.'
1659    except:
1660      print 'WARNING: Directory', CD, 'could not be created.'
1661      if unix:
1662        CD = '/tmp/'
1663      else:
1664        CD = 'C:' 
1665      print 'Using directory %s instead' %CD
1666
1667  return(CD)
1668
1669#==============================================================================
1670# Statistics
1671#==============================================================================
1672
1673def addstatsline(CD,funcname,FN,Retrieved,reason,comptime,loadtime,
1674                 compression):
1675  """Add stats entry
1676
1677  USAGE:
1678    addstatsline(CD,funcname,FN,Retrieved,reason,comptime,loadtime,compression)
1679
1680  DESCRIPTION:
1681    Make one entry in the stats file about one cache hit recording time saved
1682    and other statistics. The data are used by the function cachestat.
1683  """
1684
1685  import os, time
1686
1687  try:
1688    TimeTuple = time.localtime(time.time())
1689    extension = time.strftime('%b%Y',TimeTuple)
1690    SFN = CD+statsfile+'.'+extension
1691    #statfile = open(SFN,'a')
1692    (statfile, dummy) = myopen(SFN,'a',compression=0)
1693
1694    # Change access rights if possible
1695    #
1696    #if unix:
1697    #  try:
1698    #    exitcode=os.system('chmod 666 '+SFN)
1699    #  except:
1700    #    pass
1701  except:
1702    print 'Warning: Stat file could not be opened'
1703
1704  try:
1705    if os.environ.has_key('USER'):
1706      user = os.environ['USER']
1707    else:
1708      user = 'Nobody'
1709
1710    date = time.asctime(TimeTuple)
1711
1712    if Retrieved:
1713      hit = '1'
1714    else:
1715      hit = '0'
1716
1717    # Get size of result file
1718    #   
1719    if compression:
1720      stats = os.stat(CD+FN+'_'+file_types[0]+'.z')
1721    else:
1722      stats = os.stat(CD+FN+'_'+file_types[0])
1723 
1724    if stats: 
1725      size = stats[6]
1726    else:
1727      size = -1  # Error condition, but don't crash. This is just statistics 
1728
1729    # Build entry
1730   
1731    entry = date             + ',' +\
1732            user             + ',' +\
1733            FN               + ',' +\
1734            str(int(size))   + ',' +\
1735            str(compression) + ',' +\
1736            hit              + ',' +\
1737            str(reason)      + ',' +\
1738            str(round(comptime,4)) + ',' +\
1739            str(round(loadtime,4)) +\
1740            CR
1741           
1742    statfile.write(entry)
1743    statfile.close()
1744  except:
1745    print 'Warning: Writing of stat file failed'
1746
1747# -----------------------------------------------------------------------------
1748
1749# FIXME: should take cachedir as an optional arg
1750#
1751def __cachestat(sortidx=4,period=-1,showuser=None,cachedir=None):
1752  """  List caching statistics.
1753
1754  USAGE:
1755    __cachestat(sortidx=4,period=-1,showuser=None,cachedir=None):
1756
1757      Generate statistics of caching efficiency.
1758      The parameter sortidx determines by what field lists are sorted.
1759      If the optional keyword period is set to -1,
1760      all available caching history is used.
1761      If it is 0 only the current month is used.
1762      Future versions will include more than one month....
1763      OMN 20/8/2000
1764  """
1765
1766  import os
1767  import os.path
1768  from string import split, rstrip, find, atof, atoi
1769  from time import strptime, localtime, strftime, mktime, ctime
1770
1771  # sortidx = 4    # Index into Fields[1:]. What to sort by.
1772
1773  Fields = ['Name', 'Hits', 'Exec(s)', \
1774            'Cache(s)', 'Saved(s)', 'Gain(%)', 'Size']
1775  Widths = [25,7,9,9,9,9,13]
1776  #Types = ['s','d','d','d','d','.2f','d']
1777  Types = ['s','d','.2f','.2f','.2f','.2f','d'] 
1778
1779  Dictnames = ['Function', 'User']
1780
1781  if not cachedir:
1782    cachedir = checkdir(options['cachedir'])
1783
1784  SD = os.path.expanduser(cachedir)  # Expand ~ or ~user in pathname
1785
1786  if period == -1:  # Take all available stats
1787    SFILENAME = statsfile
1788  else:  # Only stats from current month 
1789       # MAKE THIS MORE GENERAL SO period > 0 counts several months backwards!
1790    TimeTuple = localtime(time())
1791    extension = strftime('%b%Y',TimeTuple)
1792    SFILENAME = statsfile+'.'+extension
1793
1794  DIRLIST = os.listdir(SD)
1795  SF = []
1796  for FN in DIRLIST:
1797    if find(FN,SFILENAME) >= 0:
1798      SF.append(FN)
1799
1800  blocksize = 15000000
1801  total_read = 0
1802  total_hits = 0
1803  total_discarded = 0
1804  firstday = mktime(strptime('2030','%Y'))
1805             # FIXME: strptime don't exist in WINDOWS ?
1806  lastday = 0
1807
1808  FuncDict = {}
1809  UserDict = {}
1810  for FN in SF:
1811    input = open(SD+FN,'r')
1812    print 'Reading file ', SD+FN
1813
1814    while 1:
1815      A = input.readlines(blocksize)
1816      if len(A) == 0: break
1817      total_read = total_read + len(A)
1818      for record in A:
1819        record = tuple(split(rstrip(record),','))
1820        #print record
1821
1822        if len(record) in [8,9]:
1823          n = 0
1824          timestamp = record[n]; n=n+1
1825       
1826          try:
1827            t = mktime(strptime(timestamp))
1828          except:
1829            total_discarded = total_discarded + 1         
1830            continue   
1831             
1832          if t > lastday:
1833            lastday = t
1834          if t < firstday:
1835            firstday = t
1836
1837          user     = record[n]; n=n+1
1838          func     = record[n]; n=n+1
1839
1840          # Strip hash-stamp off
1841          #
1842          i = find(func,'[')
1843          func = func[:i]
1844
1845          size        = atof(record[n]); n=n+1
1846          compression = atoi(record[n]); n=n+1
1847          hit         = atoi(record[n]); n=n+1
1848          reason      = atoi(record[n]); n=n+1   # Not used here   
1849          cputime     = atof(record[n]); n=n+1
1850          loadtime    = atof(record[n]); n=n+1
1851
1852          if hit:
1853            total_hits = total_hits + 1
1854            saving = cputime-loadtime
1855
1856            if cputime != 0:
1857              rel_saving = round(100.0*saving/cputime,2)
1858            else:
1859              #rel_saving = round(1.0*saving,2)
1860              rel_saving = 100.0 - round(1.0*saving,2)  # A bit of a hack
1861
1862            info = [1,cputime,loadtime,saving,rel_saving,size]
1863
1864            UpdateDict(UserDict,user,info)
1865            UpdateDict(FuncDict,func,info)
1866          else:
1867            pass #Stats on recomputations and their reasons could go in here
1868             
1869        else:
1870          #print 'Record discarded'
1871          #print record
1872          total_discarded = total_discarded + 1
1873
1874    input.close()
1875
1876  # Compute averages of all sums and write list
1877  #
1878
1879  if total_read == 0:
1880    printline(Widths,'=')
1881    print 'CACHING STATISTICS: No valid records read'
1882    printline(Widths,'=')
1883    return
1884
1885  print
1886  printline(Widths,'=')
1887  print 'CACHING STATISTICS: '+ctime(firstday)+' to '+ctime(lastday)
1888  printline(Widths,'=')
1889  #print '  Period:', ctime(firstday), 'to', ctime(lastday)
1890  print '  Total number of valid records', total_read
1891  print '  Total number of discarded records', total_discarded
1892  print '  Total number of hits', total_hits
1893  print
1894
1895  print '  Fields', Fields[2:], 'are averaged over number of hits'
1896  print '  Time is measured in seconds and size in bytes'
1897  print '  Tables are sorted by', Fields[1:][sortidx]
1898
1899  # printline(Widths,'-')
1900
1901  if showuser:
1902    Dictionaries = [FuncDict, UserDict]
1903  else:
1904    Dictionaries = [FuncDict]
1905
1906  i = 0
1907  for Dict in Dictionaries:
1908    for key in Dict.keys():
1909      rec = Dict[key]
1910      for n in range(len(rec)):
1911        if n > 0:
1912          rec[n] = round(1.0*rec[n]/rec[0],2)
1913      Dict[key] = rec
1914
1915    # Sort and output
1916    #
1917    keylist = SortDict(Dict,sortidx)
1918
1919    # Write Header
1920    #
1921    print
1922    #print Dictnames[i], 'statistics:'; i=i+1
1923    printline(Widths,'-')
1924    n = 0
1925    for s in Fields:
1926      if s == Fields[0]:  # Left justify
1927        s = Dictnames[i] + ' ' + s; i=i+1
1928        exec "print '%-" + str(Widths[n]) + "s'%s,"; n=n+1
1929      else:
1930        exec "print '%" + str(Widths[n]) + "s'%s,"; n=n+1
1931    print
1932    printline(Widths,'-')
1933
1934    # Output Values
1935    #
1936    for key in keylist:
1937      rec = Dict[key]
1938      n = 0
1939      if len(key) > Widths[n]: key = key[:Widths[n]-3] + '...'
1940      exec "print '%-" + str(Widths[n]) + Types[n]+"'%key,";n=n+1
1941      for val in rec:
1942        exec "print '%" + str(Widths[n]) + Types[n]+"'%val,"; n=n+1
1943      print
1944    print
1945
1946#==============================================================================
1947# Auxiliary stats functions
1948#==============================================================================
1949
1950def UpdateDict(Dict,key,info):
1951  """Update dictionary by adding new values to existing.
1952
1953  USAGE:
1954    UpdateDict(Dict,key,info)
1955  """
1956
1957  if Dict.has_key(key):
1958    dinfo = Dict[key]
1959    for n in range(len(dinfo)):
1960      dinfo[n] = info[n] + dinfo[n]
1961  else:
1962    dinfo = info[:]  # Make a copy of info list
1963
1964  Dict[key] = dinfo
1965  return Dict
1966
1967# -----------------------------------------------------------------------------
1968
1969def SortDict(Dict,sortidx=0):
1970  """Sort dictionary
1971
1972  USAGE:
1973    SortDict(Dict,sortidx):
1974
1975  DESCRIPTION:
1976    Sort dictionary of lists according field number 'sortidx'
1977  """
1978
1979  import types
1980
1981  sortlist  = []
1982  keylist = Dict.keys()
1983  for key in keylist:
1984    rec = Dict[key]
1985    if not type(rec) in [types.ListType, types.TupleType]:
1986      rec = [rec]
1987
1988    if sortidx > len(rec)-1:
1989      if options['verbose']:
1990        print 'ERROR: Sorting index to large, sortidx = ', sortidx
1991      raise IndexError
1992
1993    val = rec[sortidx]
1994    sortlist.append(val)
1995
1996  A = map(None,sortlist,keylist)
1997  A.sort()
1998  keylist = map(lambda x: x[1], A)  # keylist sorted by sortidx
1999
2000  return(keylist)
2001
2002# -----------------------------------------------------------------------------
2003
2004def printline(Widths,char):
2005  """Print textline in fixed field.
2006
2007  USAGE:
2008    printline(Widths,char)
2009  """
2010
2011  s = ''
2012  for n in range(len(Widths)):
2013    s = s+Widths[n]*char
2014    if n > 0:
2015      s = s+char
2016
2017  print s
2018
2019#==============================================================================
2020# Messages
2021#==============================================================================
2022
2023def msg1(funcname,args,kwargs,reason):
2024  """Message 1
2025
2026  USAGE:
2027    msg1(funcname,args,kwargs,reason):
2028  """
2029
2030  import string
2031  #print 'MESSAGE (caching.py): Evaluating function', funcname,
2032
2033  print_header_box('Evaluating function %s' %funcname)
2034 
2035  msg7(args,kwargs)
2036  msg8(reason) 
2037 
2038  print_footer()
2039 
2040  #
2041  # Old message
2042  #
2043  #args_present = 0
2044  #if args:
2045  #  if len(args) == 1:
2046  #    print 'with argument', mkargstr(args[0], textwidth2),
2047  #  else:
2048  #    print 'with arguments', mkargstr(args, textwidth2),
2049  #  args_present = 1     
2050  #   
2051  #if kwargs:
2052  #  if args_present:
2053  #    word = 'and'
2054  #  else:
2055  #    word = 'with'
2056  #     
2057  #  if len(kwargs) == 1:
2058  #    print word + ' keyword argument', mkargstr(kwargs, textwidth2)
2059  #  else:
2060  #    print word + ' keyword arguments', mkargstr(kwargs, textwidth2)
2061  #  args_present = 1           
2062  #else:
2063  #  print    # Newline when no keyword args present
2064  #       
2065  #if not args_present:   
2066  #  print '',  # Default if no args or kwargs present
2067   
2068   
2069
2070# -----------------------------------------------------------------------------
2071
2072def msg2(funcname,args,kwargs,comptime,reason):
2073  """Message 2
2074
2075  USAGE:
2076    msg2(funcname,args,kwargs,comptime,reason)
2077  """
2078
2079  import string
2080
2081  #try:
2082  #  R = Reason_msg[reason]
2083  #except:
2084  #  R = 'Unknown reason' 
2085 
2086  #print_header_box('Caching statistics (storing) - %s' %R)
2087  print_header_box('Caching statistics (storing)') 
2088 
2089  msg6(funcname,args,kwargs)
2090  msg8(reason)
2091
2092  print string.ljust('| CPU time:', textwidth1) + str(round(comptime,2)) + ' seconds'
2093
2094# -----------------------------------------------------------------------------
2095
2096def msg3(savetime, CD, FN, deps,compression):
2097  """Message 3
2098
2099  USAGE:
2100    msg3(savetime, CD, FN, deps,compression)
2101  """
2102
2103  import string
2104  print string.ljust('| Loading time:', textwidth1) + str(round(savetime,2)) + \
2105                     ' seconds (estimated)'
2106  msg5(CD,FN,deps,compression)
2107
2108# -----------------------------------------------------------------------------
2109
2110def msg4(funcname,args,kwargs,deps,comptime,loadtime,CD,FN,compression):
2111  """Message 4
2112
2113  USAGE:
2114    msg4(funcname,args,kwargs,deps,comptime,loadtime,CD,FN,compression)
2115  """
2116
2117  import string
2118
2119  print_header_box('Caching statistics (retrieving)')
2120 
2121  msg6(funcname,args,kwargs)
2122  print string.ljust('| CPU time:', textwidth1) + str(round(comptime,2)) + ' seconds'
2123  print string.ljust('| Loading time:', textwidth1) + str(round(loadtime,2)) + ' seconds'
2124  print string.ljust('| Time saved:', textwidth1) + str(round(comptime-loadtime,2)) + \
2125        ' seconds'
2126  msg5(CD,FN,deps,compression)
2127
2128# -----------------------------------------------------------------------------
2129
2130def msg5(CD,FN,deps,compression):
2131  """Message 5
2132
2133  USAGE:
2134    msg5(CD,FN,deps,compression)
2135
2136  DESCRIPTION:
2137   Print dependency stats. Used by msg3 and msg4
2138  """
2139
2140  import os, time, string
2141
2142  print '|'
2143  print string.ljust('| Caching dir: ', textwidth1) + CD
2144
2145  if compression:
2146    suffix = '.z'
2147    bytetext = 'bytes, compressed'
2148  else:
2149    suffix = ''
2150    bytetext = 'bytes'
2151
2152  for file_type in file_types:
2153    file_name = FN + '_' + file_type + suffix
2154    print string.ljust('| ' + file_type + ' file: ', textwidth1) + file_name,
2155    stats = os.stat(CD+file_name)
2156    print '('+ str(stats[6]) + ' ' + bytetext + ')'
2157
2158  print '|'
2159  if len(deps) > 0:
2160    print '| Dependencies:  '
2161    dependencies  = deps.keys()
2162    dlist = []; maxd = 0
2163    tlist = []; maxt = 0
2164    slist = []; maxs = 0
2165    for d in dependencies:
2166      stats = deps[d]
2167      t = time.ctime(stats[1])
2168      s = str(stats[0])
2169      #if s[-1] == 'L':
2170      #  s = s[:-1]  # Strip rightmost 'long integer' L off.
2171      #              # FIXME: Unnecessary in versions later than 1.5.2
2172
2173      if len(d) > maxd: maxd = len(d)
2174      if len(t) > maxt: maxt = len(t)
2175      if len(s) > maxs: maxs = len(s)
2176      dlist.append(d)
2177      tlist.append(t)
2178      slist.append(s)
2179
2180    for n in range(len(dlist)):
2181      d = string.ljust(dlist[n]+':', maxd+1)
2182      t = string.ljust(tlist[n], maxt)
2183      s = string.rjust(slist[n], maxs)
2184
2185      print '| ', d, t, ' ', s, 'bytes'
2186  else:
2187    print '| No dependencies'
2188  print_footer()
2189
2190# -----------------------------------------------------------------------------
2191
2192def msg6(funcname,args,kwargs):
2193  """Message 6
2194
2195  USAGE:
2196    msg6(funcname,args,kwargs)
2197  """
2198
2199  import string
2200  print string.ljust('| Function:', textwidth1) + funcname
2201
2202  msg7(args,kwargs)
2203 
2204# -----------------------------------------------------------------------------   
2205
2206def msg7(args,kwargs):
2207  """Message 7
2208 
2209  USAGE:
2210    msg7(args,kwargs):
2211  """
2212 
2213  import string
2214 
2215  args_present = 0 
2216  if args:
2217    if len(args) == 1:
2218      print string.ljust('| Argument:', textwidth1) + mkargstr(args[0], \
2219                         textwidth2)
2220    else:
2221      print string.ljust('| Arguments:', textwidth1) + \
2222            mkargstr(args, textwidth2)
2223    args_present = 1
2224           
2225  if kwargs:
2226    if len(kwargs) == 1:
2227      print string.ljust('| Keyword Arg:', textwidth1) + mkargstr(kwargs, \
2228                         textwidth2)
2229    else:
2230      print string.ljust('| Keyword Args:', textwidth1) + \
2231            mkargstr(kwargs, textwidth2)
2232    args_present = 1
2233
2234  if not args_present:               
2235    print '| No arguments' # Default if no args or kwargs present
2236
2237# -----------------------------------------------------------------------------
2238
2239def msg8(reason):
2240  """Message 8
2241 
2242  USAGE:
2243    msg8(reason):
2244  """
2245 
2246  import string
2247   
2248  try:
2249    R = Reason_msg[reason]
2250  except:
2251    R = 'Unknown' 
2252 
2253  print string.ljust('| Reason:', textwidth1) + R
2254   
2255# -----------------------------------------------------------------------------
2256
2257def print_header_box(line):
2258  """Print line in a nice box.
2259 
2260  USAGE:
2261    print_header_box(line)
2262
2263  """
2264  global textwidth3
2265
2266  import time
2267
2268  time_stamp = time.ctime(time.time())
2269  line = time_stamp + '. ' + line
2270   
2271  N = len(line) + 1
2272  s = '+' + '-'*N + CR
2273
2274  print s + '| ' + line + CR + s,
2275
2276  textwidth3 = N
2277
2278# -----------------------------------------------------------------------------
2279   
2280def print_footer():
2281  """Print line same width as that of print_header_box.
2282  """
2283 
2284  N = textwidth3
2285  s = '+' + '-'*N + CR   
2286     
2287  print s     
2288     
2289# -----------------------------------------------------------------------------
2290
2291def mkargstr(args, textwidth, argstr = ''):
2292  """ Generate a string containing first textwidth characters of arguments.
2293
2294  USAGE:
2295    mkargstr(args, textwidth, argstr = '')
2296
2297  DESCRIPTION:
2298    Exactly the same as str(args) possibly followed by truncation,
2299    but faster if args is huge.
2300  """
2301
2302  import types
2303
2304  WasTruncated = 0
2305
2306  if not type(args) in [types.TupleType, types.ListType, types.DictType]:
2307    if type(args) == types.StringType:
2308      argstr = argstr + "'"+str(args)+"'"
2309    else:
2310      #Truncate large Numeric arrays before using str()
2311      import Numeric
2312      if type(args) == Numeric.ArrayType:
2313#        if len(args.flat) > textwidth: 
2314#        Changed by Duncan and Nick 21/2/07 .flat has problems with
2315#        non-contigous arrays and ravel is equal to .flat except it
2316#        can work with non-contiguous  arrays
2317        if len(Numeric.ravel(args)) > textwidth:
2318          args = 'Array: ' + str(args.shape)
2319
2320      argstr = argstr + str(args)
2321  else:
2322    if type(args) == types.DictType:
2323      argstr = argstr + "{"
2324      for key in args.keys():
2325        argstr = argstr + mkargstr(key, textwidth) + ": " + \
2326                 mkargstr(args[key], textwidth) + ", "
2327        if len(argstr) > textwidth:
2328          WasTruncated = 1
2329          break
2330      argstr = argstr[:-2]  # Strip off trailing comma     
2331      argstr = argstr + "}"
2332
2333    else:
2334      if type(args) == types.TupleType:
2335        lc = '('
2336        rc = ')'
2337      else:
2338        lc = '['
2339        rc = ']'
2340      argstr = argstr + lc
2341      for arg in args:
2342        argstr = argstr + mkargstr(arg, textwidth) + ', '
2343        if len(argstr) > textwidth:
2344          WasTruncated = 1
2345          break
2346
2347      # Strip off trailing comma and space unless singleton tuple
2348      #
2349      if type(args) == types.TupleType and len(args) == 1:
2350        argstr = argstr[:-1]   
2351      else:
2352        argstr = argstr[:-2]
2353      argstr = argstr + rc
2354
2355  if len(argstr) > textwidth:
2356    WasTruncated = 1
2357
2358  if WasTruncated:
2359    argstr = argstr[:textwidth]+'...'
2360  return(argstr)
2361
2362# -----------------------------------------------------------------------------
2363
2364def test_OK(msg):
2365  """Print OK msg if test is OK.
2366 
2367  USAGE
2368    test_OK(message)
2369  """
2370
2371  import string
2372   
2373  print string.ljust(msg, textwidth4) + ' - OK' 
2374 
2375  #raise StandardError
2376 
2377# -----------------------------------------------------------------------------
2378
2379def test_error(msg):
2380  """Print error if test fails.
2381 
2382  USAGE
2383    test_error(message)
2384  """
2385 
2386  print 'ERROR (caching.test): %s' %msg
2387  print 'Please send this code example and output to '
2388  print 'Ole.Nielsen@anu.edu.au'
2389  print
2390  print
2391 
2392  #import sys
2393  #sys.exit()
2394  raise StandardError
Note: See TracBrowser for help on using the repository browser.