summaryrefslogtreecommitdiffstats
path: root/cobbler/Cheetah/SettingsManager.py
diff options
context:
space:
mode:
Diffstat (limited to 'cobbler/Cheetah/SettingsManager.py')
-rw-r--r--cobbler/Cheetah/SettingsManager.py623
1 files changed, 623 insertions, 0 deletions
diff --git a/cobbler/Cheetah/SettingsManager.py b/cobbler/Cheetah/SettingsManager.py
new file mode 100644
index 0000000..0967155
--- /dev/null
+++ b/cobbler/Cheetah/SettingsManager.py
@@ -0,0 +1,623 @@
+#!/usr/bin/env python
+
+"""Provides a mixin/base class for collecting and managing application settings
+
+Meta-Data
+==========
+Author: Tavis Rudd <tavis@damnsimple.com>
+Version: $Revision: 1.28 $
+Start Date: 2001/05/30
+Last Revision Date: $Date: 2006/01/29 07:19:12 $
+"""
+
+# $Id: SettingsManager.py,v 1.28 2006/01/29 07:19:12 tavis_rudd Exp $
+__author__ = "Tavis Rudd <tavis@damnsimple.com>"
+__revision__ = "$Revision: 1.28 $"[11:-2]
+
+
+##################################################
+## DEPENDENCIES ##
+
+import sys
+import os.path
+import copy as copyModule
+from ConfigParser import ConfigParser
+import re
+from tokenize import Intnumber, Floatnumber, Number
+from types import *
+import types
+import new
+import tempfile
+import imp
+import time
+
+from StringIO import StringIO # not cStringIO because of unicode support
+
+import imp # used by SettingsManager.updateSettingsFromPySrcFile()
+
+try:
+ import threading
+ from threading import Lock # used for thread lock on sys.path manipulations
+except:
+ ## provide a dummy for non-threading Python systems
+ class Lock:
+ def acquire(self):
+ pass
+ def release(self):
+ pass
+
+class BaseErrorClass: pass
+
+##################################################
+## CONSTANTS & GLOBALS ##
+
+try:
+ True,False
+except NameError:
+ True, False = (1==1),(1==0)
+
+numberRE = re.compile(Number)
+complexNumberRE = re.compile('[\(]*' +Number + r'[ \t]*\+[ \t]*' + Number + '[\)]*')
+
+convertableToStrTypes = (StringType, IntType, FloatType,
+ LongType, ComplexType, NoneType,
+ UnicodeType)
+
+##################################################
+## FUNCTIONS ##
+
+def mergeNestedDictionaries(dict1, dict2, copy=False, deepcopy=False):
+
+ """Recursively merge the values of dict2 into dict1.
+
+ This little function is very handy for selectively overriding settings in a
+ settings dictionary that has a nested structure.
+ """
+
+ if copy:
+ dict1 = copyModule.copy(dict1)
+ elif deepcopy:
+ dict1 = copyModule.deepcopy(dict1)
+
+ for key,val in dict2.items():
+ if dict1.has_key(key) and type(val) == types.DictType and \
+ type(dict1[key]) == types.DictType:
+
+ dict1[key] = mergeNestedDictionaries(dict1[key], val)
+ else:
+ dict1[key] = val
+ return dict1
+
+def stringIsNumber(S):
+
+ """Return True if theString represents a Python number, False otherwise.
+ This also works for complex numbers and numbers with +/- in front."""
+
+ S = S.strip()
+
+ if S[0] in '-+' and len(S) > 1:
+ S = S[1:].strip()
+
+ match = complexNumberRE.match(S)
+ if not match:
+ match = numberRE.match(S)
+ if not match or (match.end() != len(S)):
+ return False
+ else:
+ return True
+
+def convStringToNum(theString):
+
+ """Convert a string representation of a Python number to the Python version"""
+
+ if not stringIsNumber(theString):
+ raise Error(theString + ' cannot be converted to a Python number')
+ return eval(theString, {}, {})
+
+
+
+######
+
+ident = r'[_a-zA-Z][_a-zA-Z0-9]*'
+firstChunk = r'^(?P<indent>\s*)(?P<class>[_a-zA-Z][_a-zA-Z0-9]*)'
+customClassRe = re.compile(firstChunk + r'\s*:')
+baseClasses = r'(?P<bases>\(\s*([_a-zA-Z][_a-zA-Z0-9]*\s*(,\s*[_a-zA-Z][_a-zA-Z0-9]*\s*)*)\))'
+customClassWithBasesRe = re.compile(firstChunk + baseClasses + '\s*:')
+
+def translateClassBasedConfigSyntax(src):
+
+ """Compiles a config file in the custom class-based SettingsContainer syntax
+ to Vanilla Python
+
+ # WebKit.config
+ Applications:
+ MyApp:
+ Dirs:
+ ROOT = '/home/www/Home'
+ Products = '/home/www/Products'
+ becomes:
+ # WebKit.config
+ from Cheetah.SettingsManager import SettingsContainer
+ class Applications(SettingsContainer):
+ class MyApp(SettingsContainer):
+ class Dirs(SettingsContainer):
+ ROOT = '/home/www/Home'
+ Products = '/home/www/Products'
+ """
+
+ outputLines = []
+ for line in src.splitlines():
+ if customClassRe.match(line) and \
+ line.strip().split(':')[0] not in ('else','try', 'except', 'finally'):
+
+ line = customClassRe.sub(
+ r'\g<indent>class \g<class>(SettingsContainer):', line)
+
+ elif customClassWithBasesRe.match(line) and not line.strip().startswith('except'):
+ line = customClassWithBasesRe.sub(
+ r'\g<indent>class \g<class>\g<bases>:', line)
+
+ outputLines.append(line)
+
+ ## prepend this to the first line to make sure that tracebacks report the right line nums
+ if outputLines[0].find('class ') == -1:
+ initLine = 'from Cheetah.SettingsManager import SettingsContainer; True, False = 1, 0; '
+ else:
+ initLine = 'from Cheetah.SettingsManager import SettingsContainer; True, False = 1, 0\n'
+ return initLine + '\n'.join(outputLines) + '\n'
+
+
+##################################################
+## CLASSES ##
+
+class Error(BaseErrorClass):
+ pass
+
+class NoDefault:
+ pass
+
+class ConfigParserCaseSensitive(ConfigParser):
+
+ """A case sensitive version of the standard Python ConfigParser."""
+
+ def optionxform(self, optionstr):
+
+ """Don't change the case as is done in the default implemenation."""
+
+ return optionstr
+
+class SettingsContainer:
+ """An abstract base class for 'classes' that are used to house settings."""
+ pass
+
+
+class _SettingsCollector:
+
+ """An abstract base class that provides the methods SettingsManager uses to
+ collect settings from config files and SettingsContainers.
+
+ This class only collects settings it doesn't modify the _settings dictionary
+ of SettingsManager instances in any way.
+
+ SettingsCollector is designed to:
+ - be able to read settings from Python src files (or strings) so that
+ complex Python objects can be stored in the application's settings
+ dictionary. For example, you might want to store references to various
+ classes that are used by the application and plugins to the application
+ might want to substitute one class for another.
+ - be able to read/write .ini style config files (or strings)
+ - allow sections in .ini config files to be extended by settings in Python
+ src files
+ - allow python literals to be used values in .ini config files
+ - maintain the case of setting names, unlike the ConfigParser module
+
+ """
+
+ _sysPathLock = Lock() # used by the updateSettingsFromPySrcFile() method
+ _ConfigParserClass = ConfigParserCaseSensitive
+
+
+ def __init__(self):
+ pass
+
+ def normalizePath(self, path):
+
+ """A hook for any neccessary path manipulations.
+
+ For example, when this is used with WebKit servlets all relative paths
+ must be converted so they are relative to the servlet's directory rather
+ than relative to the program's current working dir.
+
+ The default implementation just normalizes the path for the current
+ operating system."""
+
+ return os.path.normpath(path.replace("\\",'/'))
+
+
+ def readSettingsFromContainer(self, container, ignoreUnderscored=True):
+
+ """Returns all settings from a SettingsContainer or Python
+ module.
+
+ This method is recursive.
+ """
+
+ S = {}
+ if type(container) == ModuleType:
+ attrs = vars(container)
+ else:
+ attrs = self._getAllAttrsFromContainer(container)
+
+ for k, v in attrs.items():
+ if (ignoreUnderscored and k.startswith('_')) or v is SettingsContainer:
+ continue
+ if self._isContainer(v):
+ S[k] = self.readSettingsFromContainer(v)
+ else:
+ S[k] = v
+ return S
+
+ # provide an alias
+ readSettingsFromModule = readSettingsFromContainer
+
+ def _isContainer(self, thing):
+
+ """Check if 'thing' is a Python module or a subclass of
+ SettingsContainer."""
+
+ return type(thing) == ModuleType or (
+ type(thing) == ClassType and issubclass(thing, SettingsContainer)
+ )
+
+ def _getAllAttrsFromContainer(self, container):
+ """Extract all the attributes of a SettingsContainer subclass.
+
+ The 'container' is a class, so extracting all attributes from it, an
+ instance of it, and all its base classes.
+
+ This method is not recursive.
+ """
+
+ attrs = container.__dict__.copy()
+ # init an instance of the container and get all attributes
+ attrs.update( container().__dict__ )
+
+ for base in container.__bases__:
+ for k, v in base.__dict__.items():
+ if not attrs.has_key(k):
+ attrs[k] = v
+ return attrs
+
+ def readSettingsFromPySrcFile(self, path):
+
+ """Return new settings dict from variables in a Python source file.
+
+ This method will temporarily add the directory of src file to sys.path so
+ that import statements relative to that dir will work properly."""
+
+ path = self.normalizePath(path)
+ dirName = os.path.dirname(path)
+ tmpPath = tempfile.mkstemp('webware_temp')
+
+ pySrc = translateClassBasedConfigSyntax(open(path).read())
+ modName = path.replace('.','_').replace('/','_').replace('\\','_')
+ open(tmpPath, 'w').write(pySrc)
+ try:
+ fp = open(tmpPath)
+ self._sysPathLock.acquire()
+ sys.path.insert(0, dirName)
+ module = imp.load_source(modName, path, fp)
+ newSettings = self.readSettingsFromModule(module)
+ del sys.path[0]
+ self._sysPathLock.release()
+ return newSettings
+ finally:
+ fp.close()
+ try:
+ os.remove(tmpPath)
+ except:
+ pass
+ if os.path.exists(tmpPath + 'c'):
+ try:
+ os.remove(tmpPath + 'c')
+ except:
+ pass
+ if os.path.exists(path + 'c'):
+ try:
+ os.remove(path + 'c')
+ except:
+ pass
+
+
+ def readSettingsFromPySrcStr(self, theString):
+
+ """Return a dictionary of the settings in a Python src string."""
+
+ globalsDict = {'True':1,
+ 'False':0,
+ 'SettingsContainer':SettingsContainer,
+ }
+ newSettings = {'self':self}
+ exec theString in globalsDict, newSettings
+ del newSettings['self'], newSettings['True'], newSettings['False']
+ module = new.module('temp_settings_module')
+ module.__dict__.update(newSettings)
+ return self.readSettingsFromModule(module)
+
+ def readSettingsFromConfigFile(self, path, convert=True):
+ path = self.normalizePath(path)
+ fp = open(path)
+ settings = self.readSettingsFromConfigFileObj(fp, convert=convert)
+ fp.close()
+ return settings
+
+ def readSettingsFromConfigFileObj(self, inFile, convert=True):
+
+ """Return the settings from a config file that uses the syntax accepted by
+ Python's standard ConfigParser module (like Windows .ini files).
+
+ NOTE:
+ this method maintains case unlike the ConfigParser module, unless this
+ class was initialized with the 'caseSensitive' keyword set to False.
+
+ All setting values are initially parsed as strings. However, If the
+ 'convert' arg is True this method will do the following value
+ conversions:
+
+ * all Python numeric literals will be coverted from string to number
+
+ * The string 'None' will be converted to the Python value None
+
+ * The string 'True' will be converted to a Python truth value
+
+ * The string 'False' will be converted to a Python false value
+
+ * Any string starting with 'python:' will be treated as a Python literal
+ or expression that needs to be eval'd. This approach is useful for
+ declaring lists and dictionaries.
+
+ If a config section titled 'Globals' is present the options defined
+ under it will be treated as top-level settings.
+ """
+
+ p = self._ConfigParserClass()
+ p.readfp(inFile)
+ sects = p.sections()
+ newSettings = {}
+
+ sects = p.sections()
+ newSettings = {}
+
+ for s in sects:
+ newSettings[s] = {}
+ for o in p.options(s):
+ if o != '__name__':
+ newSettings[s][o] = p.get(s,o)
+
+ ## loop through new settings -> deal with global settings, numbers,
+ ## booleans and None ++ also deal with 'importSettings' commands
+
+ for sect, subDict in newSettings.items():
+ for key, val in subDict.items():
+ if convert:
+ if val.lower().startswith('python:'):
+ subDict[key] = eval(val[7:],{},{})
+ if val.lower() == 'none':
+ subDict[key] = None
+ if val.lower() == 'true':
+ subDict[key] = True
+ if val.lower() == 'false':
+ subDict[key] = False
+ if stringIsNumber(val):
+ subDict[key] = convStringToNum(val)
+
+ ## now deal with any 'importSettings' commands
+ if key.lower() == 'importsettings':
+ if val.find(';') < 0:
+ importedSettings = self.readSettingsFromPySrcFile(val)
+ else:
+ path = val.split(';')[0]
+ rest = ''.join(val.split(';')[1:]).strip()
+ parentDict = self.readSettingsFromPySrcFile(path)
+ importedSettings = eval('parentDict["' + rest + '"]')
+
+ subDict.update(mergeNestedDictionaries(subDict,
+ importedSettings))
+
+ if sect.lower() == 'globals':
+ newSettings.update(newSettings[sect])
+ del newSettings[sect]
+
+ return newSettings
+
+
+class SettingsManager(_SettingsCollector):
+
+ """A mixin class that provides facilities for managing application settings.
+
+ SettingsManager is designed to work well with nested settings dictionaries
+ of any depth.
+ """
+
+ ## init methods
+
+ def __init__(self):
+ """MUST BE CALLED BY SUBCLASSES"""
+ _SettingsCollector.__init__(self)
+ self._settings = {}
+ self._initializeSettings()
+
+ def _defaultSettings(self):
+ return {}
+
+ def _initializeSettings(self):
+
+ """A hook that allows for complex setting initialization sequences that
+ involve references to 'self' or other settings. For example:
+ self._settings['myCalcVal'] = self._settings['someVal'] * 15
+ This method should be called by the class' __init__() method when needed.
+ The dummy implementation should be reimplemented by subclasses.
+ """
+
+ pass
+
+ ## core post startup methods
+
+ def setting(self, name, default=NoDefault):
+
+ """Get a setting from self._settings, with or without a default value."""
+
+ if default is NoDefault:
+ return self._settings[name]
+ else:
+ return self._settings.get(name, default)
+
+
+ def hasSetting(self, key):
+ """True/False"""
+ return self._settings.has_key(key)
+
+ def setSetting(self, name, value):
+ """Set a setting in self._settings."""
+ self._settings[name] = value
+
+ def settings(self):
+ """Return a reference to the settings dictionary"""
+ return self._settings
+
+ def copySettings(self):
+ """Returns a shallow copy of the settings dictionary"""
+ return copy(self._settings)
+
+ def deepcopySettings(self):
+ """Returns a deep copy of the settings dictionary"""
+ return deepcopy(self._settings)
+
+ def updateSettings(self, newSettings, merge=True):
+
+ """Update the settings with a selective merge or a complete overwrite."""
+
+ if merge:
+ mergeNestedDictionaries(self._settings, newSettings)
+ else:
+ self._settings.update(newSettings)
+
+
+
+
+ ## source specific update methods
+
+ def updateSettingsFromPySrcStr(self, theString, merge=True):
+
+ """Update the settings from a code in a Python src string."""
+
+ newSettings = self.readSettingsFromPySrcStr(theString)
+ self.updateSettings(newSettings,
+ merge=newSettings.get('mergeSettings',merge) )
+
+ def updateSettingsFromPySrcFile(self, path, merge=True):
+
+ """Update the settings from variables in a Python source file.
+
+ This method will temporarily add the directory of src file to sys.path so
+ that import statements relative to that dir will work properly."""
+
+ newSettings = self.readSettingsFromPySrcFile(path)
+ self.updateSettings(newSettings,
+ merge=newSettings.get('mergeSettings',merge) )
+
+
+ def updateSettingsFromConfigFile(self, path, **kw):
+
+ """Update the settings from a text file using the syntax accepted by
+ Python's standard ConfigParser module (like Windows .ini files).
+ """
+
+ path = self.normalizePath(path)
+ fp = open(path)
+ self.updateSettingsFromConfigFileObj(fp, **kw)
+ fp.close()
+
+
+ def updateSettingsFromConfigFileObj(self, inFile, convert=True, merge=True):
+
+ """See the docstring for .updateSettingsFromConfigFile()
+
+ The caller of this method is responsible for closing the inFile file
+ object."""
+
+ newSettings = self.readSettingsFromConfigFileObj(inFile, convert=convert)
+ self.updateSettings(newSettings,
+ merge=newSettings.get('mergeSettings',merge))
+
+ def updateSettingsFromConfigStr(self, configStr, convert=True, merge=True):
+
+ """See the docstring for .updateSettingsFromConfigFile()
+ """
+
+ configStr = '[globals]\n' + configStr
+ inFile = StringIO(configStr)
+ newSettings = self.readSettingsFromConfigFileObj(inFile, convert=convert)
+ self.updateSettings(newSettings,
+ merge=newSettings.get('mergeSettings',merge))
+
+
+ ## methods for output representations of the settings
+
+ def _createConfigFile(self, outFile=None):
+
+ """
+ Write all the settings that can be represented as strings to an .ini
+ style config string.
+
+ This method can only handle one level of nesting and will only work with
+ numbers, strings, and None.
+ """
+
+ if outFile is None:
+ outFile = StringIO()
+ iniSettings = {'Globals':{}}
+ globals = iniSettings['Globals']
+
+ for key, theSetting in self.settings().items():
+ if type(theSetting) in convertableToStrTypes:
+ globals[key] = theSetting
+ if type(theSetting) is DictType:
+ iniSettings[key] = {}
+ for subKey, subSetting in theSetting.items():
+ if type(subSetting) in convertableToStrTypes:
+ iniSettings[key][subKey] = subSetting
+
+ sections = iniSettings.keys()
+ sections.sort()
+ outFileWrite = outFile.write # short-cut namebinding for efficiency
+ for section in sections:
+ outFileWrite("[" + section + "]\n")
+ sectDict = iniSettings[section]
+
+ keys = sectDict.keys()
+ keys.sort()
+ for key in keys:
+ if key == "__name__":
+ continue
+ outFileWrite("%s = %s\n" % (key, sectDict[key]))
+ outFileWrite("\n")
+
+ return outFile
+
+ def writeConfigFile(self, path):
+
+ """Write all the settings that can be represented as strings to an .ini
+ style config file."""
+
+ path = self.normalizePath(path)
+ fp = open(path,'w')
+ self._createConfigFile(fp)
+ fp.close()
+
+ def getConfigString(self):
+ """Return a string with the settings in .ini file format."""
+
+ return self._createConfigFile().getvalue()
+
+# vim: shiftwidth=4 tabstop=4 expandtab