3 """Provides a mixin/base class for collecting and managing application settings
7 Author: Tavis Rudd <tavis@damnsimple.com>
8 Version: $Revision: 1.29 $
10 Last Revision Date: $Date: 2007/04/03 02:03:26 $
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 ##################################################
23 import copy
as copyModule
24 from ConfigParser
import ConfigParser
26 from tokenize
import Intnumber
, Floatnumber
, Number
32 from StringIO
import StringIO
# not cStringIO because of unicode support
33 import imp
# used by SettingsManager.updateSettingsFromPySrcFile()
37 from threading
import Lock
# used for thread lock on sys.path manipulations
39 ## provide a dummy for non-threading Python systems
46 class BaseErrorClass
: pass
48 ##################################################
49 ## CONSTANTS & GLOBALS ##
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
,
63 ##################################################
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.
75 dict1
= copyModule
.copy(dict1
)
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
)
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."""
95 if S
[0] in '-+' and len(S
) > 1:
98 match
= complexNumberRE
.match(S
)
100 match
= numberRE
.match(S
)
101 if not match
or (match
.end() != len(S
)):
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
, {}, {})
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
133 ROOT = '/home/www/Home'
134 Products = '/home/www/Products'
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'
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; '
163 initLine
= 'from Cheetah.SettingsManager import SettingsContainer; True, False = 1, 0\n'
164 return initLine
+ '\n'.join(outputLines
) + '\n'
167 ##################################################
170 class Error(BaseErrorClass
):
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."""
186 class SettingsContainer
:
187 """An abstract base class for 'classes' that are used to house settings."""
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
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
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
231 return os
.path
.normpath(path
.replace("\\",'/'))
234 def readSettingsFromContainer(self
, container
, ignoreUnderscored
=True):
236 """Returns all settings from a SettingsContainer or Python
239 This method is recursive.
243 if type(container
) == ModuleType
:
244 attrs
= vars(container
)
246 attrs
= self
._getAllAttrsFromContainer
(container
)
248 for k
, v
in attrs
.items():
249 if (ignoreUnderscored
and k
.startswith('_')) or v
is SettingsContainer
:
251 if self
._isContainer
(v
):
252 S
[k
] = self
.readSettingsFromContainer(v
)
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
):
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
)
304 self
._sysPathLock
.acquire()
305 sys
.path
.insert(0, dirName
)
306 module
= imp
.load_source(modName
, path
, fp
)
307 newSettings
= self
.readSettingsFromModule(module
)
309 self
._sysPathLock
.release()
317 if os
.path
.exists(tmpPath
+ 'c'):
319 os
.remove(tmpPath
+ 'c')
322 if os
.path
.exists(path
+ 'c'):
324 os
.remove(path
+ 'c')
329 def readSettingsFromPySrcStr(self
, theString
):
331 """Return a dictionary of the settings in a Python src string."""
333 globalsDict
= {'True':1,
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
)
347 settings
= self
.readSettingsFromConfigFileObj(fp
, convert
=convert
)
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).
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
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
()
390 for o
in p
.options(s
):
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():
400 if val
.lower().startswith('python:'):
401 subDict
[key
] = eval(val
[7:],{},{})
402 if val
.lower() == 'none':
404 if val
.lower() == 'true':
406 if val
.lower() == '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
)
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
,
424 if sect
.lower() == 'globals':
425 newSettings
.update(newSettings
[sect
])
426 del newSettings
[sect
]
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
442 """MUST BE CALLED BY SUBCLASSES"""
443 _SettingsCollector
.__init
__(self
)
445 self
._initializeSettings
()
447 def _defaultSettings(self
):
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.
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
]
470 return self
._settings
.get(name
, default
)
473 def hasSetting(self
, key
):
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
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."""
498 mergeNestedDictionaries(self
._settings
, newSettings
)
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
)
535 self
.updateSettingsFromConfigFileObj(fp
, **kw
)
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
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
570 This method can only handle one level of nesting and will only work with
571 numbers, strings, and None.
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()
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()
598 if key
== "__name__":
600 outFileWrite("%s = %s\n" % (key
, sectDict
[key
]))
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
)
612 self
._createConfigFile
(fp
)
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