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

Last change on this file since 5287 was 4817, checked in by ole, 17 years ago

Fixed cachestat to recognise boolean values of the compression keyword.

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