import sys, os
from types import DictType
from MiscUtils import AbstractError, NoDefault
from WebKit.ImportSpy import modloader
from Funcs import valueForString
class ConfigurationError(Exception):
pass
class Configurable:
"""
Configurable is an abstract superclass that provides configuration
file functionality for subclasses.
Subclasses should override:
* defaultConfig() to return a dictionary of default settings
such as { 'Frequency': 5 }
* configFilename() to return the filename by which users can
override the configuration such as
'Pinger.config'
Subclasses typically use the setting() method, for example:
time.sleep(self.setting('Frequency'))
They might also use the printConfig() method, for example:
self.printConfig() # or
self.printConfig(file)
Users of your software can create a file with the same name as
configFilename() and selectively override settings. The format of
the file is a Python dictionary.
Subclasses can also override userConfig() in order to obtain the
user configuration settings from another source.
"""
def __init__(self):
self._config = None
def config(self):
""" Returns the configuration of the object as a dictionary. This is a combination of defaultConfig() and userConfig(). This method caches the config. """
if self._config is None:
self._config = self.defaultConfig()
self._config.update(self.userConfig())
self._config.update(self.commandLineConfig())
return self._config
def setting(self, name, default=NoDefault):
""" Returns the value of a particular setting in the configuration. """
if default is NoDefault:
try:
return self.config()[name]
except KeyError:
raise KeyError, '%s config keys are: %s' % (name, self.config().keys())
else:
return self.config().get(name, default)
def setSetting(self, name, value):
self.config()[name] = value
def hasSetting(self, name):
return self.config().has_key(name)
def defaultConfig(self):
""" Returns a dictionary containing all the default values for the settings. This implementation returns {}. Subclasses should override. """
return {}
def configFilename(self):
"""
Returns the filename by which users can override the
configuration. Subclasses must override to specify a
name. Returning None is valid, in which case no user
config file will be loaded."""
raise AbstractError, self.__class__
def configName(self):
"""
Returns the name of the configuration file (the portion
before the '.config'). This is used on the command-line.
"""
return os.path.splitext(os.path.basename(self.configFilename()))[0]
def configReplacementValues(self):
"""
Returns a dictionary suitable for use with "string % dict"
that should be used on the text in the config file. If an
empty dictionary (or None) is returned then no substitution
will be attempted.
"""
return {}
def userConfig(self):
""" Returns the user config overrides found in the optional config file, or {} if there is no such file. The config filename is taken from configFilename(). """
try:
filename = self.configFilename()
if filename is None:
return {}
file = open(filename)
except IOError:
return {}
else:
contents = file.read()
isDict = contents.strip().startswith('{')
file.close()
modloader.watchFile(filename)
replacements = self.configReplacementValues()
if replacements and isDict:
try:
contents = contents % replacements
except:
raise ConfigurationError, 'Unable to embed replacement text in %s.' % self.configFilename()
evalContext = replacements.copy()
evalContext['True'] = 1==1
evalContext['False'] = 1==0
try:
if isDict:
config = eval(contents, evalContext)
else:
exec contents in evalContext
config = evalContext
for name in config.keys():
if name.startswith('_'):
del config[name]
except Exception, e:
raise ConfigurationError, 'Invalid configuration file, %s (%s).' % (self.configFilename(), e)
if type(config) is not DictType:
raise ConfigurationError, 'Invalid type of configuration. Expecting dictionary, but got %s.' % type(config)
return config
def printConfig(self, dest=None):
""" Prints the configuration to the given destination, which defaults to stdout. A fixed with font is assumed for aligning the values to start at the same column. """
if dest is None:
dest = sys.stdout
keys = self.config().keys()
keys.sort()
width = max(map(lambda key: len(key), keys))
for key in keys:
dest.write(key.ljust(width)+' = '+str(self.setting(key))+'\n')
dest.write('\n')
def commandLineConfig(self):
"""
Settings that came from the command line (via
addCommandLineSetting).
"""
return _settings.get(self.configName(), {})
_settings = {}
def addCommandLineSetting(name, value):
"""
Take a setting, like "AppServer.Verbose=0", and call
addCommandLineSetting('AppServer.Verbose', '0'), and
it will override any settings in AppServer.config
"""
configName, settingName = name.split('.', 1)
value = valueForString(value)
if not _settings.has_key(configName):
_settings[configName] = {}
_settings[configName][settingName] = value
def commandLineSetting(configName, settingName, default=NoDefault):
"""
Retrieve a command-line setting. You can use this with
non-existent classes, like "Context.Root=/WK", and then
fetch it back with commandLineSetting('Context', 'Root').
"""
if default is NoDefault:
return _settings[configName][settingName]
else:
return _settings.get(configName, {}).get(settingName, default)