Using FS mtime to reload non recursive cache.
[pyTivo.git] / Cheetah / SettingsManager.py
blob096715536f8f8e0b59eabf3acc38fb76e5d2f7d9
1 #!/usr/bin/env python
3 """Provides a mixin/base class for collecting and managing application settings
5 Meta-Data
6 ==========
7 Author: Tavis Rudd <tavis@damnsimple.com>
8 Version: $Revision: 1.28 $
9 Start Date: 2001/05/30
10 Last Revision Date: $Date: 2006/01/29 07:19:12 $
11 """
13 # $Id: SettingsManager.py,v 1.28 2006/01/29 07:19:12 tavis_rudd Exp $
14 __author__ = "Tavis Rudd <tavis@damnsimple.com>"
15 __revision__ = "$Revision: 1.28 $"[11:-2]
18 ##################################################
19 ## DEPENDENCIES ##
21 import sys
22 import os.path
23 import copy as copyModule
24 from ConfigParser import ConfigParser
25 import re
26 from tokenize import Intnumber, Floatnumber, Number
27 from types import *
28 import types
29 import new
30 import tempfile
31 import imp
32 import time
34 from StringIO import StringIO # not cStringIO because of unicode support
36 import imp # used by SettingsManager.updateSettingsFromPySrcFile()
38 try:
39 import threading
40 from threading import Lock # used for thread lock on sys.path manipulations
41 except:
42 ## provide a dummy for non-threading Python systems
43 class Lock:
44 def acquire(self):
45 pass
46 def release(self):
47 pass
49 class BaseErrorClass: pass
51 ##################################################
52 ## CONSTANTS & GLOBALS ##
54 try:
55 True,False
56 except NameError:
57 True, False = (1==1),(1==0)
59 numberRE = re.compile(Number)
60 complexNumberRE = re.compile('[\(]*' +Number + r'[ \t]*\+[ \t]*' + Number + '[\)]*')
62 convertableToStrTypes = (StringType, IntType, FloatType,
63 LongType, ComplexType, NoneType,
64 UnicodeType)
66 ##################################################
67 ## FUNCTIONS ##
69 def mergeNestedDictionaries(dict1, dict2, copy=False, deepcopy=False):
71 """Recursively merge the values of dict2 into dict1.
73 This little function is very handy for selectively overriding settings in a
74 settings dictionary that has a nested structure.
75 """
77 if copy:
78 dict1 = copyModule.copy(dict1)
79 elif deepcopy:
80 dict1 = copyModule.deepcopy(dict1)
82 for key,val in dict2.items():
83 if dict1.has_key(key) and type(val) == types.DictType and \
84 type(dict1[key]) == types.DictType:
86 dict1[key] = mergeNestedDictionaries(dict1[key], val)
87 else:
88 dict1[key] = val
89 return dict1
91 def stringIsNumber(S):
93 """Return True if theString represents a Python number, False otherwise.
94 This also works for complex numbers and numbers with +/- in front."""
96 S = S.strip()
98 if S[0] in '-+' and len(S) > 1:
99 S = S[1:].strip()
101 match = complexNumberRE.match(S)
102 if not match:
103 match = numberRE.match(S)
104 if not match or (match.end() != len(S)):
105 return False
106 else:
107 return True
109 def convStringToNum(theString):
111 """Convert a string representation of a Python number to the Python version"""
113 if not stringIsNumber(theString):
114 raise Error(theString + ' cannot be converted to a Python number')
115 return eval(theString, {}, {})
119 ######
121 ident = r'[_a-zA-Z][_a-zA-Z0-9]*'
122 firstChunk = r'^(?P<indent>\s*)(?P<class>[_a-zA-Z][_a-zA-Z0-9]*)'
123 customClassRe = re.compile(firstChunk + r'\s*:')
124 baseClasses = r'(?P<bases>\(\s*([_a-zA-Z][_a-zA-Z0-9]*\s*(,\s*[_a-zA-Z][_a-zA-Z0-9]*\s*)*)\))'
125 customClassWithBasesRe = re.compile(firstChunk + baseClasses + '\s*:')
127 def translateClassBasedConfigSyntax(src):
129 """Compiles a config file in the custom class-based SettingsContainer syntax
130 to Vanilla Python
132 # WebKit.config
133 Applications:
134 MyApp:
135 Dirs:
136 ROOT = '/home/www/Home'
137 Products = '/home/www/Products'
138 becomes:
139 # WebKit.config
140 from Cheetah.SettingsManager import SettingsContainer
141 class Applications(SettingsContainer):
142 class MyApp(SettingsContainer):
143 class Dirs(SettingsContainer):
144 ROOT = '/home/www/Home'
145 Products = '/home/www/Products'
148 outputLines = []
149 for line in src.splitlines():
150 if customClassRe.match(line) and \
151 line.strip().split(':')[0] not in ('else','try', 'except', 'finally'):
153 line = customClassRe.sub(
154 r'\g<indent>class \g<class>(SettingsContainer):', line)
156 elif customClassWithBasesRe.match(line) and not line.strip().startswith('except'):
157 line = customClassWithBasesRe.sub(
158 r'\g<indent>class \g<class>\g<bases>:', line)
160 outputLines.append(line)
162 ## prepend this to the first line to make sure that tracebacks report the right line nums
163 if outputLines[0].find('class ') == -1:
164 initLine = 'from Cheetah.SettingsManager import SettingsContainer; True, False = 1, 0; '
165 else:
166 initLine = 'from Cheetah.SettingsManager import SettingsContainer; True, False = 1, 0\n'
167 return initLine + '\n'.join(outputLines) + '\n'
170 ##################################################
171 ## CLASSES ##
173 class Error(BaseErrorClass):
174 pass
176 class NoDefault:
177 pass
179 class ConfigParserCaseSensitive(ConfigParser):
181 """A case sensitive version of the standard Python ConfigParser."""
183 def optionxform(self, optionstr):
185 """Don't change the case as is done in the default implemenation."""
187 return optionstr
189 class SettingsContainer:
190 """An abstract base class for 'classes' that are used to house settings."""
191 pass
194 class _SettingsCollector:
196 """An abstract base class that provides the methods SettingsManager uses to
197 collect settings from config files and SettingsContainers.
199 This class only collects settings it doesn't modify the _settings dictionary
200 of SettingsManager instances in any way.
202 SettingsCollector is designed to:
203 - be able to read settings from Python src files (or strings) so that
204 complex Python objects can be stored in the application's settings
205 dictionary. For example, you might want to store references to various
206 classes that are used by the application and plugins to the application
207 might want to substitute one class for another.
208 - be able to read/write .ini style config files (or strings)
209 - allow sections in .ini config files to be extended by settings in Python
210 src files
211 - allow python literals to be used values in .ini config files
212 - maintain the case of setting names, unlike the ConfigParser module
216 _sysPathLock = Lock() # used by the updateSettingsFromPySrcFile() method
217 _ConfigParserClass = ConfigParserCaseSensitive
220 def __init__(self):
221 pass
223 def normalizePath(self, path):
225 """A hook for any neccessary path manipulations.
227 For example, when this is used with WebKit servlets all relative paths
228 must be converted so they are relative to the servlet's directory rather
229 than relative to the program's current working dir.
231 The default implementation just normalizes the path for the current
232 operating system."""
234 return os.path.normpath(path.replace("\\",'/'))
237 def readSettingsFromContainer(self, container, ignoreUnderscored=True):
239 """Returns all settings from a SettingsContainer or Python
240 module.
242 This method is recursive.
245 S = {}
246 if type(container) == ModuleType:
247 attrs = vars(container)
248 else:
249 attrs = self._getAllAttrsFromContainer(container)
251 for k, v in attrs.items():
252 if (ignoreUnderscored and k.startswith('_')) or v is SettingsContainer:
253 continue
254 if self._isContainer(v):
255 S[k] = self.readSettingsFromContainer(v)
256 else:
257 S[k] = v
258 return S
260 # provide an alias
261 readSettingsFromModule = readSettingsFromContainer
263 def _isContainer(self, thing):
265 """Check if 'thing' is a Python module or a subclass of
266 SettingsContainer."""
268 return type(thing) == ModuleType or (
269 type(thing) == ClassType and issubclass(thing, SettingsContainer)
272 def _getAllAttrsFromContainer(self, container):
273 """Extract all the attributes of a SettingsContainer subclass.
275 The 'container' is a class, so extracting all attributes from it, an
276 instance of it, and all its base classes.
278 This method is not recursive.
281 attrs = container.__dict__.copy()
282 # init an instance of the container and get all attributes
283 attrs.update( container().__dict__ )
285 for base in container.__bases__:
286 for k, v in base.__dict__.items():
287 if not attrs.has_key(k):
288 attrs[k] = v
289 return attrs
291 def readSettingsFromPySrcFile(self, path):
293 """Return new settings dict from variables in a Python source file.
295 This method will temporarily add the directory of src file to sys.path so
296 that import statements relative to that dir will work properly."""
298 path = self.normalizePath(path)
299 dirName = os.path.dirname(path)
300 tmpPath = tempfile.mkstemp('webware_temp')
302 pySrc = translateClassBasedConfigSyntax(open(path).read())
303 modName = path.replace('.','_').replace('/','_').replace('\\','_')
304 open(tmpPath, 'w').write(pySrc)
305 try:
306 fp = open(tmpPath)
307 self._sysPathLock.acquire()
308 sys.path.insert(0, dirName)
309 module = imp.load_source(modName, path, fp)
310 newSettings = self.readSettingsFromModule(module)
311 del sys.path[0]
312 self._sysPathLock.release()
313 return newSettings
314 finally:
315 fp.close()
316 try:
317 os.remove(tmpPath)
318 except:
319 pass
320 if os.path.exists(tmpPath + 'c'):
321 try:
322 os.remove(tmpPath + 'c')
323 except:
324 pass
325 if os.path.exists(path + 'c'):
326 try:
327 os.remove(path + 'c')
328 except:
329 pass
332 def readSettingsFromPySrcStr(self, theString):
334 """Return a dictionary of the settings in a Python src string."""
336 globalsDict = {'True':1,
337 'False':0,
338 'SettingsContainer':SettingsContainer,
340 newSettings = {'self':self}
341 exec theString in globalsDict, newSettings
342 del newSettings['self'], newSettings['True'], newSettings['False']
343 module = new.module('temp_settings_module')
344 module.__dict__.update(newSettings)
345 return self.readSettingsFromModule(module)
347 def readSettingsFromConfigFile(self, path, convert=True):
348 path = self.normalizePath(path)
349 fp = open(path)
350 settings = self.readSettingsFromConfigFileObj(fp, convert=convert)
351 fp.close()
352 return settings
354 def readSettingsFromConfigFileObj(self, inFile, convert=True):
356 """Return the settings from a config file that uses the syntax accepted by
357 Python's standard ConfigParser module (like Windows .ini files).
359 NOTE:
360 this method maintains case unlike the ConfigParser module, unless this
361 class was initialized with the 'caseSensitive' keyword set to False.
363 All setting values are initially parsed as strings. However, If the
364 'convert' arg is True this method will do the following value
365 conversions:
367 * all Python numeric literals will be coverted from string to number
369 * The string 'None' will be converted to the Python value None
371 * The string 'True' will be converted to a Python truth value
373 * The string 'False' will be converted to a Python false value
375 * Any string starting with 'python:' will be treated as a Python literal
376 or expression that needs to be eval'd. This approach is useful for
377 declaring lists and dictionaries.
379 If a config section titled 'Globals' is present the options defined
380 under it will be treated as top-level settings.
383 p = self._ConfigParserClass()
384 p.readfp(inFile)
385 sects = p.sections()
386 newSettings = {}
388 sects = p.sections()
389 newSettings = {}
391 for s in sects:
392 newSettings[s] = {}
393 for o in p.options(s):
394 if o != '__name__':
395 newSettings[s][o] = p.get(s,o)
397 ## loop through new settings -> deal with global settings, numbers,
398 ## booleans and None ++ also deal with 'importSettings' commands
400 for sect, subDict in newSettings.items():
401 for key, val in subDict.items():
402 if convert:
403 if val.lower().startswith('python:'):
404 subDict[key] = eval(val[7:],{},{})
405 if val.lower() == 'none':
406 subDict[key] = None
407 if val.lower() == 'true':
408 subDict[key] = True
409 if val.lower() == 'false':
410 subDict[key] = False
411 if stringIsNumber(val):
412 subDict[key] = convStringToNum(val)
414 ## now deal with any 'importSettings' commands
415 if key.lower() == 'importsettings':
416 if val.find(';') < 0:
417 importedSettings = self.readSettingsFromPySrcFile(val)
418 else:
419 path = val.split(';')[0]
420 rest = ''.join(val.split(';')[1:]).strip()
421 parentDict = self.readSettingsFromPySrcFile(path)
422 importedSettings = eval('parentDict["' + rest + '"]')
424 subDict.update(mergeNestedDictionaries(subDict,
425 importedSettings))
427 if sect.lower() == 'globals':
428 newSettings.update(newSettings[sect])
429 del newSettings[sect]
431 return newSettings
434 class SettingsManager(_SettingsCollector):
436 """A mixin class that provides facilities for managing application settings.
438 SettingsManager is designed to work well with nested settings dictionaries
439 of any depth.
442 ## init methods
444 def __init__(self):
445 """MUST BE CALLED BY SUBCLASSES"""
446 _SettingsCollector.__init__(self)
447 self._settings = {}
448 self._initializeSettings()
450 def _defaultSettings(self):
451 return {}
453 def _initializeSettings(self):
455 """A hook that allows for complex setting initialization sequences that
456 involve references to 'self' or other settings. For example:
457 self._settings['myCalcVal'] = self._settings['someVal'] * 15
458 This method should be called by the class' __init__() method when needed.
459 The dummy implementation should be reimplemented by subclasses.
462 pass
464 ## core post startup methods
466 def setting(self, name, default=NoDefault):
468 """Get a setting from self._settings, with or without a default value."""
470 if default is NoDefault:
471 return self._settings[name]
472 else:
473 return self._settings.get(name, default)
476 def hasSetting(self, key):
477 """True/False"""
478 return self._settings.has_key(key)
480 def setSetting(self, name, value):
481 """Set a setting in self._settings."""
482 self._settings[name] = value
484 def settings(self):
485 """Return a reference to the settings dictionary"""
486 return self._settings
488 def copySettings(self):
489 """Returns a shallow copy of the settings dictionary"""
490 return copy(self._settings)
492 def deepcopySettings(self):
493 """Returns a deep copy of the settings dictionary"""
494 return deepcopy(self._settings)
496 def updateSettings(self, newSettings, merge=True):
498 """Update the settings with a selective merge or a complete overwrite."""
500 if merge:
501 mergeNestedDictionaries(self._settings, newSettings)
502 else:
503 self._settings.update(newSettings)
508 ## source specific update methods
510 def updateSettingsFromPySrcStr(self, theString, merge=True):
512 """Update the settings from a code in a Python src string."""
514 newSettings = self.readSettingsFromPySrcStr(theString)
515 self.updateSettings(newSettings,
516 merge=newSettings.get('mergeSettings',merge) )
518 def updateSettingsFromPySrcFile(self, path, merge=True):
520 """Update the settings from variables in a Python source file.
522 This method will temporarily add the directory of src file to sys.path so
523 that import statements relative to that dir will work properly."""
525 newSettings = self.readSettingsFromPySrcFile(path)
526 self.updateSettings(newSettings,
527 merge=newSettings.get('mergeSettings',merge) )
530 def updateSettingsFromConfigFile(self, path, **kw):
532 """Update the settings from a text file using the syntax accepted by
533 Python's standard ConfigParser module (like Windows .ini files).
536 path = self.normalizePath(path)
537 fp = open(path)
538 self.updateSettingsFromConfigFileObj(fp, **kw)
539 fp.close()
542 def updateSettingsFromConfigFileObj(self, inFile, convert=True, merge=True):
544 """See the docstring for .updateSettingsFromConfigFile()
546 The caller of this method is responsible for closing the inFile file
547 object."""
549 newSettings = self.readSettingsFromConfigFileObj(inFile, convert=convert)
550 self.updateSettings(newSettings,
551 merge=newSettings.get('mergeSettings',merge))
553 def updateSettingsFromConfigStr(self, configStr, convert=True, merge=True):
555 """See the docstring for .updateSettingsFromConfigFile()
558 configStr = '[globals]\n' + configStr
559 inFile = StringIO(configStr)
560 newSettings = self.readSettingsFromConfigFileObj(inFile, convert=convert)
561 self.updateSettings(newSettings,
562 merge=newSettings.get('mergeSettings',merge))
565 ## methods for output representations of the settings
567 def _createConfigFile(self, outFile=None):
570 Write all the settings that can be represented as strings to an .ini
571 style config string.
573 This method can only handle one level of nesting and will only work with
574 numbers, strings, and None.
577 if outFile is None:
578 outFile = StringIO()
579 iniSettings = {'Globals':{}}
580 globals = iniSettings['Globals']
582 for key, theSetting in self.settings().items():
583 if type(theSetting) in convertableToStrTypes:
584 globals[key] = theSetting
585 if type(theSetting) is DictType:
586 iniSettings[key] = {}
587 for subKey, subSetting in theSetting.items():
588 if type(subSetting) in convertableToStrTypes:
589 iniSettings[key][subKey] = subSetting
591 sections = iniSettings.keys()
592 sections.sort()
593 outFileWrite = outFile.write # short-cut namebinding for efficiency
594 for section in sections:
595 outFileWrite("[" + section + "]\n")
596 sectDict = iniSettings[section]
598 keys = sectDict.keys()
599 keys.sort()
600 for key in keys:
601 if key == "__name__":
602 continue
603 outFileWrite("%s = %s\n" % (key, sectDict[key]))
604 outFileWrite("\n")
606 return outFile
608 def writeConfigFile(self, path):
610 """Write all the settings that can be represented as strings to an .ini
611 style config file."""
613 path = self.normalizePath(path)
614 fp = open(path,'w')
615 self._createConfigFile(fp)
616 fp.close()
618 def getConfigString(self):
619 """Return a string with the settings in .ini file format."""
621 return self._createConfigFile().getvalue()
623 # vim: shiftwidth=4 tabstop=4 expandtab