3 """Provides a mixin/base class for collecting and managing application settings
7 Author: Tavis Rudd <tavis@damnsimple.com>
8 Version: $Revision: 1.28 $
10 Last Revision Date: $Date: 2006/01/29 07:19:12 $
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 ##################################################
23 import copy
as copyModule
24 from ConfigParser
import ConfigParser
26 from tokenize
import Intnumber
, Floatnumber
, Number
34 from StringIO
import StringIO
# not cStringIO because of unicode support
36 import imp
# used by SettingsManager.updateSettingsFromPySrcFile()
40 from threading
import Lock
# used for thread lock on sys.path manipulations
42 ## provide a dummy for non-threading Python systems
49 class BaseErrorClass
: pass
51 ##################################################
52 ## CONSTANTS & GLOBALS ##
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
,
66 ##################################################
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.
78 dict1
= copyModule
.copy(dict1
)
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
)
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."""
98 if S
[0] in '-+' and len(S
) > 1:
101 match
= complexNumberRE
.match(S
)
103 match
= numberRE
.match(S
)
104 if not match
or (match
.end() != len(S
)):
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
, {}, {})
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
136 ROOT = '/home/www/Home'
137 Products = '/home/www/Products'
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'
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; '
166 initLine
= 'from Cheetah.SettingsManager import SettingsContainer; True, False = 1, 0\n'
167 return initLine
+ '\n'.join(outputLines
) + '\n'
170 ##################################################
173 class Error(BaseErrorClass
):
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."""
189 class SettingsContainer
:
190 """An abstract base class for 'classes' that are used to house settings."""
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
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
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
234 return os
.path
.normpath(path
.replace("\\",'/'))
237 def readSettingsFromContainer(self
, container
, ignoreUnderscored
=True):
239 """Returns all settings from a SettingsContainer or Python
242 This method is recursive.
246 if type(container
) == ModuleType
:
247 attrs
= vars(container
)
249 attrs
= self
._getAllAttrsFromContainer
(container
)
251 for k
, v
in attrs
.items():
252 if (ignoreUnderscored
and k
.startswith('_')) or v
is SettingsContainer
:
254 if self
._isContainer
(v
):
255 S
[k
] = self
.readSettingsFromContainer(v
)
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
):
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
)
307 self
._sysPathLock
.acquire()
308 sys
.path
.insert(0, dirName
)
309 module
= imp
.load_source(modName
, path
, fp
)
310 newSettings
= self
.readSettingsFromModule(module
)
312 self
._sysPathLock
.release()
320 if os
.path
.exists(tmpPath
+ 'c'):
322 os
.remove(tmpPath
+ 'c')
325 if os
.path
.exists(path
+ 'c'):
327 os
.remove(path
+ 'c')
332 def readSettingsFromPySrcStr(self
, theString
):
334 """Return a dictionary of the settings in a Python src string."""
336 globalsDict
= {'True':1,
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
)
350 settings
= self
.readSettingsFromConfigFileObj(fp
, convert
=convert
)
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).
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
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
()
393 for o
in p
.options(s
):
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():
403 if val
.lower().startswith('python:'):
404 subDict
[key
] = eval(val
[7:],{},{})
405 if val
.lower() == 'none':
407 if val
.lower() == 'true':
409 if val
.lower() == '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
)
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
,
427 if sect
.lower() == 'globals':
428 newSettings
.update(newSettings
[sect
])
429 del newSettings
[sect
]
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
445 """MUST BE CALLED BY SUBCLASSES"""
446 _SettingsCollector
.__init
__(self
)
448 self
._initializeSettings
()
450 def _defaultSettings(self
):
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.
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
]
473 return self
._settings
.get(name
, default
)
476 def hasSetting(self
, key
):
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
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."""
501 mergeNestedDictionaries(self
._settings
, newSettings
)
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
)
538 self
.updateSettingsFromConfigFileObj(fp
, **kw
)
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
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
573 This method can only handle one level of nesting and will only work with
574 numbers, strings, and None.
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()
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()
601 if key
== "__name__":
603 outFileWrite("%s = %s\n" % (key
, sectDict
[key
]))
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
)
615 self
._createConfigFile
(fp
)
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