Ratings from .nfo files should no longer be converted to strings.
[pyTivo/wmcbrine.git] / Cheetah / SettingsManager.py
blob8c33e0db379d52d295cdc5c9f8e58c45685757cf
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.29 $
9 Start Date: 2001/05/30
10 Last Revision Date: $Date: 2007/04/03 02:03:26 $
11 """
13 # $Id: SettingsManager.py,v 1.29 2007/04/03 02:03:26 tavis_rudd Exp $
14 __author__ = "Tavis Rudd <tavis@damnsimple.com>"
15 __revision__ = "$Revision: 1.29 $"[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 time
32 from StringIO import StringIO # not cStringIO because of unicode support
33 import imp # used by SettingsManager.updateSettingsFromPySrcFile()
35 try:
36 import threading
37 from threading import Lock # used for thread lock on sys.path manipulations
38 except:
39 ## provide a dummy for non-threading Python systems
40 class Lock:
41 def acquire(self):
42 pass
43 def release(self):
44 pass
46 class BaseErrorClass: pass
48 ##################################################
49 ## CONSTANTS & GLOBALS ##
51 try:
52 True,False
53 except NameError:
54 True, False = (1==1),(1==0)
56 numberRE = re.compile(Number)
57 complexNumberRE = re.compile('[\(]*' +Number + r'[ \t]*\+[ \t]*' + Number + '[\)]*')
59 convertableToStrTypes = (StringType, IntType, FloatType,
60 LongType, ComplexType, NoneType,
61 UnicodeType)
63 ##################################################
64 ## FUNCTIONS ##
66 def mergeNestedDictionaries(dict1, dict2, copy=False, deepcopy=False):
68 """Recursively merge the values of dict2 into dict1.
70 This little function is very handy for selectively overriding settings in a
71 settings dictionary that has a nested structure.
72 """
74 if copy:
75 dict1 = copyModule.copy(dict1)
76 elif deepcopy:
77 dict1 = copyModule.deepcopy(dict1)
79 for key,val in dict2.items():
80 if dict1.has_key(key) and type(val) == types.DictType and \
81 type(dict1[key]) == types.DictType:
83 dict1[key] = mergeNestedDictionaries(dict1[key], val)
84 else:
85 dict1[key] = val
86 return dict1
88 def stringIsNumber(S):
90 """Return True if theString represents a Python number, False otherwise.
91 This also works for complex numbers and numbers with +/- in front."""
93 S = S.strip()
95 if S[0] in '-+' and len(S) > 1:
96 S = S[1:].strip()
98 match = complexNumberRE.match(S)
99 if not match:
100 match = numberRE.match(S)
101 if not match or (match.end() != len(S)):
102 return False
103 else:
104 return True
106 def convStringToNum(theString):
108 """Convert a string representation of a Python number to the Python version"""
110 if not stringIsNumber(theString):
111 raise Error(theString + ' cannot be converted to a Python number')
112 return eval(theString, {}, {})
116 ######
118 ident = r'[_a-zA-Z][_a-zA-Z0-9]*'
119 firstChunk = r'^(?P<indent>\s*)(?P<class>[_a-zA-Z][_a-zA-Z0-9]*)'
120 customClassRe = re.compile(firstChunk + r'\s*:')
121 baseClasses = r'(?P<bases>\(\s*([_a-zA-Z][_a-zA-Z0-9]*\s*(,\s*[_a-zA-Z][_a-zA-Z0-9]*\s*)*)\))'
122 customClassWithBasesRe = re.compile(firstChunk + baseClasses + '\s*:')
124 def translateClassBasedConfigSyntax(src):
126 """Compiles a config file in the custom class-based SettingsContainer syntax
127 to Vanilla Python
129 # WebKit.config
130 Applications:
131 MyApp:
132 Dirs:
133 ROOT = '/home/www/Home'
134 Products = '/home/www/Products'
135 becomes:
136 # WebKit.config
137 from Cheetah.SettingsManager import SettingsContainer
138 class Applications(SettingsContainer):
139 class MyApp(SettingsContainer):
140 class Dirs(SettingsContainer):
141 ROOT = '/home/www/Home'
142 Products = '/home/www/Products'
145 outputLines = []
146 for line in src.splitlines():
147 if customClassRe.match(line) and \
148 line.strip().split(':')[0] not in ('else','try', 'except', 'finally'):
150 line = customClassRe.sub(
151 r'\g<indent>class \g<class>(SettingsContainer):', line)
153 elif customClassWithBasesRe.match(line) and not line.strip().startswith('except'):
154 line = customClassWithBasesRe.sub(
155 r'\g<indent>class \g<class>\g<bases>:', line)
157 outputLines.append(line)
159 ## prepend this to the first line to make sure that tracebacks report the right line nums
160 if outputLines[0].find('class ') == -1:
161 initLine = 'from Cheetah.SettingsManager import SettingsContainer; True, False = 1, 0; '
162 else:
163 initLine = 'from Cheetah.SettingsManager import SettingsContainer; True, False = 1, 0\n'
164 return initLine + '\n'.join(outputLines) + '\n'
167 ##################################################
168 ## CLASSES ##
170 class Error(BaseErrorClass):
171 pass
173 class NoDefault:
174 pass
176 class ConfigParserCaseSensitive(ConfigParser):
178 """A case sensitive version of the standard Python ConfigParser."""
180 def optionxform(self, optionstr):
182 """Don't change the case as is done in the default implemenation."""
184 return optionstr
186 class SettingsContainer:
187 """An abstract base class for 'classes' that are used to house settings."""
188 pass
191 class _SettingsCollector:
193 """An abstract base class that provides the methods SettingsManager uses to
194 collect settings from config files and SettingsContainers.
196 This class only collects settings it doesn't modify the _settings dictionary
197 of SettingsManager instances in any way.
199 SettingsCollector is designed to:
200 - be able to read settings from Python src files (or strings) so that
201 complex Python objects can be stored in the application's settings
202 dictionary. For example, you might want to store references to various
203 classes that are used by the application and plugins to the application
204 might want to substitute one class for another.
205 - be able to read/write .ini style config files (or strings)
206 - allow sections in .ini config files to be extended by settings in Python
207 src files
208 - allow python literals to be used values in .ini config files
209 - maintain the case of setting names, unlike the ConfigParser module
213 _sysPathLock = Lock() # used by the updateSettingsFromPySrcFile() method
214 _ConfigParserClass = ConfigParserCaseSensitive
217 def __init__(self):
218 pass
220 def normalizePath(self, path):
222 """A hook for any neccessary path manipulations.
224 For example, when this is used with WebKit servlets all relative paths
225 must be converted so they are relative to the servlet's directory rather
226 than relative to the program's current working dir.
228 The default implementation just normalizes the path for the current
229 operating system."""
231 return os.path.normpath(path.replace("\\",'/'))
234 def readSettingsFromContainer(self, container, ignoreUnderscored=True):
236 """Returns all settings from a SettingsContainer or Python
237 module.
239 This method is recursive.
242 S = {}
243 if type(container) == ModuleType:
244 attrs = vars(container)
245 else:
246 attrs = self._getAllAttrsFromContainer(container)
248 for k, v in attrs.items():
249 if (ignoreUnderscored and k.startswith('_')) or v is SettingsContainer:
250 continue
251 if self._isContainer(v):
252 S[k] = self.readSettingsFromContainer(v)
253 else:
254 S[k] = v
255 return S
257 # provide an alias
258 readSettingsFromModule = readSettingsFromContainer
260 def _isContainer(self, thing):
262 """Check if 'thing' is a Python module or a subclass of
263 SettingsContainer."""
265 return type(thing) == ModuleType or (
266 type(thing) == ClassType and issubclass(thing, SettingsContainer)
269 def _getAllAttrsFromContainer(self, container):
270 """Extract all the attributes of a SettingsContainer subclass.
272 The 'container' is a class, so extracting all attributes from it, an
273 instance of it, and all its base classes.
275 This method is not recursive.
278 attrs = container.__dict__.copy()
279 # init an instance of the container and get all attributes
280 attrs.update( container().__dict__ )
282 for base in container.__bases__:
283 for k, v in base.__dict__.items():
284 if not attrs.has_key(k):
285 attrs[k] = v
286 return attrs
288 def readSettingsFromPySrcFile(self, path):
290 """Return new settings dict from variables in a Python source file.
292 This method will temporarily add the directory of src file to sys.path so
293 that import statements relative to that dir will work properly."""
295 path = self.normalizePath(path)
296 dirName = os.path.dirname(path)
297 tmpPath = tempfile.mkstemp('webware_temp')
299 pySrc = translateClassBasedConfigSyntax(open(path).read())
300 modName = path.replace('.','_').replace('/','_').replace('\\','_')
301 open(tmpPath, 'w').write(pySrc)
302 try:
303 fp = open(tmpPath)
304 self._sysPathLock.acquire()
305 sys.path.insert(0, dirName)
306 module = imp.load_source(modName, path, fp)
307 newSettings = self.readSettingsFromModule(module)
308 del sys.path[0]
309 self._sysPathLock.release()
310 return newSettings
311 finally:
312 fp.close()
313 try:
314 os.remove(tmpPath)
315 except:
316 pass
317 if os.path.exists(tmpPath + 'c'):
318 try:
319 os.remove(tmpPath + 'c')
320 except:
321 pass
322 if os.path.exists(path + 'c'):
323 try:
324 os.remove(path + 'c')
325 except:
326 pass
329 def readSettingsFromPySrcStr(self, theString):
331 """Return a dictionary of the settings in a Python src string."""
333 globalsDict = {'True':1,
334 'False':0,
335 'SettingsContainer':SettingsContainer,
337 newSettings = {'self':self}
338 exec theString in globalsDict, newSettings
339 del newSettings['self'], newSettings['True'], newSettings['False']
340 module = new.module('temp_settings_module')
341 module.__dict__.update(newSettings)
342 return self.readSettingsFromModule(module)
344 def readSettingsFromConfigFile(self, path, convert=True):
345 path = self.normalizePath(path)
346 fp = open(path)
347 settings = self.readSettingsFromConfigFileObj(fp, convert=convert)
348 fp.close()
349 return settings
351 def readSettingsFromConfigFileObj(self, inFile, convert=True):
353 """Return the settings from a config file that uses the syntax accepted by
354 Python's standard ConfigParser module (like Windows .ini files).
356 NOTE:
357 this method maintains case unlike the ConfigParser module, unless this
358 class was initialized with the 'caseSensitive' keyword set to False.
360 All setting values are initially parsed as strings. However, If the
361 'convert' arg is True this method will do the following value
362 conversions:
364 * all Python numeric literals will be coverted from string to number
366 * The string 'None' will be converted to the Python value None
368 * The string 'True' will be converted to a Python truth value
370 * The string 'False' will be converted to a Python false value
372 * Any string starting with 'python:' will be treated as a Python literal
373 or expression that needs to be eval'd. This approach is useful for
374 declaring lists and dictionaries.
376 If a config section titled 'Globals' is present the options defined
377 under it will be treated as top-level settings.
380 p = self._ConfigParserClass()
381 p.readfp(inFile)
382 sects = p.sections()
383 newSettings = {}
385 sects = p.sections()
386 newSettings = {}
388 for s in sects:
389 newSettings[s] = {}
390 for o in p.options(s):
391 if o != '__name__':
392 newSettings[s][o] = p.get(s,o)
394 ## loop through new settings -> deal with global settings, numbers,
395 ## booleans and None ++ also deal with 'importSettings' commands
397 for sect, subDict in newSettings.items():
398 for key, val in subDict.items():
399 if convert:
400 if val.lower().startswith('python:'):
401 subDict[key] = eval(val[7:],{},{})
402 if val.lower() == 'none':
403 subDict[key] = None
404 if val.lower() == 'true':
405 subDict[key] = True
406 if val.lower() == 'false':
407 subDict[key] = False
408 if stringIsNumber(val):
409 subDict[key] = convStringToNum(val)
411 ## now deal with any 'importSettings' commands
412 if key.lower() == 'importsettings':
413 if val.find(';') < 0:
414 importedSettings = self.readSettingsFromPySrcFile(val)
415 else:
416 path = val.split(';')[0]
417 rest = ''.join(val.split(';')[1:]).strip()
418 parentDict = self.readSettingsFromPySrcFile(path)
419 importedSettings = eval('parentDict["' + rest + '"]')
421 subDict.update(mergeNestedDictionaries(subDict,
422 importedSettings))
424 if sect.lower() == 'globals':
425 newSettings.update(newSettings[sect])
426 del newSettings[sect]
428 return newSettings
431 class SettingsManager(_SettingsCollector):
433 """A mixin class that provides facilities for managing application settings.
435 SettingsManager is designed to work well with nested settings dictionaries
436 of any depth.
439 ## init methods
441 def __init__(self):
442 """MUST BE CALLED BY SUBCLASSES"""
443 _SettingsCollector.__init__(self)
444 self._settings = {}
445 self._initializeSettings()
447 def _defaultSettings(self):
448 return {}
450 def _initializeSettings(self):
452 """A hook that allows for complex setting initialization sequences that
453 involve references to 'self' or other settings. For example:
454 self._settings['myCalcVal'] = self._settings['someVal'] * 15
455 This method should be called by the class' __init__() method when needed.
456 The dummy implementation should be reimplemented by subclasses.
459 pass
461 ## core post startup methods
463 def setting(self, name, default=NoDefault):
465 """Get a setting from self._settings, with or without a default value."""
467 if default is NoDefault:
468 return self._settings[name]
469 else:
470 return self._settings.get(name, default)
473 def hasSetting(self, key):
474 """True/False"""
475 return self._settings.has_key(key)
477 def setSetting(self, name, value):
478 """Set a setting in self._settings."""
479 self._settings[name] = value
481 def settings(self):
482 """Return a reference to the settings dictionary"""
483 return self._settings
485 def copySettings(self):
486 """Returns a shallow copy of the settings dictionary"""
487 return copyModule.copy(self._settings)
489 def deepcopySettings(self):
490 """Returns a deep copy of the settings dictionary"""
491 return copyModule.deepcopy(self._settings)
493 def updateSettings(self, newSettings, merge=True):
495 """Update the settings with a selective merge or a complete overwrite."""
497 if merge:
498 mergeNestedDictionaries(self._settings, newSettings)
499 else:
500 self._settings.update(newSettings)
505 ## source specific update methods
507 def updateSettingsFromPySrcStr(self, theString, merge=True):
509 """Update the settings from a code in a Python src string."""
511 newSettings = self.readSettingsFromPySrcStr(theString)
512 self.updateSettings(newSettings,
513 merge=newSettings.get('mergeSettings',merge) )
515 def updateSettingsFromPySrcFile(self, path, merge=True):
517 """Update the settings from variables in a Python source file.
519 This method will temporarily add the directory of src file to sys.path so
520 that import statements relative to that dir will work properly."""
522 newSettings = self.readSettingsFromPySrcFile(path)
523 self.updateSettings(newSettings,
524 merge=newSettings.get('mergeSettings',merge) )
527 def updateSettingsFromConfigFile(self, path, **kw):
529 """Update the settings from a text file using the syntax accepted by
530 Python's standard ConfigParser module (like Windows .ini files).
533 path = self.normalizePath(path)
534 fp = open(path)
535 self.updateSettingsFromConfigFileObj(fp, **kw)
536 fp.close()
539 def updateSettingsFromConfigFileObj(self, inFile, convert=True, merge=True):
541 """See the docstring for .updateSettingsFromConfigFile()
543 The caller of this method is responsible for closing the inFile file
544 object."""
546 newSettings = self.readSettingsFromConfigFileObj(inFile, convert=convert)
547 self.updateSettings(newSettings,
548 merge=newSettings.get('mergeSettings',merge))
550 def updateSettingsFromConfigStr(self, configStr, convert=True, merge=True):
552 """See the docstring for .updateSettingsFromConfigFile()
555 configStr = '[globals]\n' + configStr
556 inFile = StringIO(configStr)
557 newSettings = self.readSettingsFromConfigFileObj(inFile, convert=convert)
558 self.updateSettings(newSettings,
559 merge=newSettings.get('mergeSettings',merge))
562 ## methods for output representations of the settings
564 def _createConfigFile(self, outFile=None):
567 Write all the settings that can be represented as strings to an .ini
568 style config string.
570 This method can only handle one level of nesting and will only work with
571 numbers, strings, and None.
574 if outFile is None:
575 outFile = StringIO()
576 iniSettings = {'Globals':{}}
577 globals = iniSettings['Globals']
579 for key, theSetting in self.settings().items():
580 if type(theSetting) in convertableToStrTypes:
581 globals[key] = theSetting
582 if type(theSetting) is DictType:
583 iniSettings[key] = {}
584 for subKey, subSetting in theSetting.items():
585 if type(subSetting) in convertableToStrTypes:
586 iniSettings[key][subKey] = subSetting
588 sections = iniSettings.keys()
589 sections.sort()
590 outFileWrite = outFile.write # short-cut namebinding for efficiency
591 for section in sections:
592 outFileWrite("[" + section + "]\n")
593 sectDict = iniSettings[section]
595 keys = sectDict.keys()
596 keys.sort()
597 for key in keys:
598 if key == "__name__":
599 continue
600 outFileWrite("%s = %s\n" % (key, sectDict[key]))
601 outFileWrite("\n")
603 return outFile
605 def writeConfigFile(self, path):
607 """Write all the settings that can be represented as strings to an .ini
608 style config file."""
610 path = self.normalizePath(path)
611 fp = open(path,'w')
612 self._createConfigFile(fp)
613 fp.close()
615 def getConfigString(self):
616 """Return a string with the settings in .ini file format."""
618 return self._createConfigFile().getvalue()
620 # vim: shiftwidth=4 tabstop=4 expandtab