import string, 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. """ ## Init ## def __init__(self): self._config = None ## Configuration 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: return self.config()[name] else: return self.config().get(name, default) 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() file.close() modloader.watchFile(filename) replacements = self.configReplacementValues() if replacements: try: contents = contents % replacements except: raise ConfigurationError, 'Unable to embed replacement text in %s.' % self.configFilename() try: config = eval(contents, {}) except: raise ConfigurationError, 'Invalid configuration file, %s.' % self.configFilename() 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(string.ljust(key, 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 = string.split(name, '.', 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)