root/Cheetah/SettingsManager.py

Revision f17b49bd2a9cb5c693518283252cdbca4d04136b, 20.5 kB (checked in by Jason Michalski <armooo@armooo.net>, 2 years ago)

Lets try the import again

  • Property mode set to 100644
Line 
1 #!/usr/bin/env python
2
3 """Provides a mixin/base class for collecting and managing application settings
4
5 Meta-Data
6 ==========
7 Author: Tavis Rudd <tavis@damnsimple.com>
8 Version: $Revision: 1.28 $
9 Start Date: 2001/05/30
10 Last Revision Date: $Date: 2006/01/29 07:19:12 $
11 """
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]
16
17
18 ##################################################
19 ## DEPENDENCIES ##
20
21 import sys
22 import os.path
23 import copy as copyModule
24 from ConfigParser import ConfigParser
25 import re
26 from tokenize import Intnumber, Floatnumber, Number
27 from types import *
28 import types
29 import new
30 import tempfile
31 import imp
32 import time
33
34 from StringIO import StringIO # not cStringIO because of unicode support
35
36 import imp                 # used by SettingsManager.updateSettingsFromPySrcFile()
37
38 try:
39     import threading
40     from threading import Lock  # used for thread lock on sys.path manipulations
41 except:
42     ## provide a dummy for non-threading Python systems
43     class Lock:
44         def acquire(self):
45             pass
46         def release(self):
47             pass
48
49 class BaseErrorClass: pass
50
51 ##################################################
52 ## CONSTANTS & GLOBALS ##
53
54 try:
55     True,False
56 except NameError:
57     True, False = (1==1),(1==0)
58
59 numberRE = re.compile(Number)
60 complexNumberRE = re.compile('[\(]*' +Number + r'[ \t]*\+[ \t]*' + Number + '[\)]*')
61
62 convertableToStrTypes = (StringType, IntType, FloatType,
63                          LongType, ComplexType, NoneType,
64                          UnicodeType)
65
66 ##################################################
67 ## FUNCTIONS ##
68
69 def mergeNestedDictionaries(dict1, dict2, copy=False, deepcopy=False):
70    
71     """Recursively merge the values of dict2 into dict1.
72
73     This little function is very handy for selectively overriding settings in a
74     settings dictionary that has a nested structure.
75     """
76
77     if copy:
78         dict1 = copyModule.copy(dict1)
79     elif deepcopy:
80         dict1 = copyModule.deepcopy(dict1)
81        
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:
85            
86             dict1[key] = mergeNestedDictionaries(dict1[key], val)
87         else:
88             dict1[key] = val
89     return dict1
90    
91 def stringIsNumber(S):
92    
93     """Return True if theString represents a Python number, False otherwise.
94     This also works for complex numbers and numbers with +/- in front."""
95
96     S = S.strip()
97    
98     if S[0] in '-+' and len(S) > 1:
99         S = S[1:].strip()
100    
101     match = complexNumberRE.match(S)
102     if not match:
103         match = numberRE.match(S)
104     if not match or (match.end() != len(S)):
105         return False
106     else:
107         return True
108        
109 def convStringToNum(theString):
110    
111     """Convert a string representation of a Python number to the Python version"""
112    
113     if not stringIsNumber(theString):
114         raise Error(theString + ' cannot be converted to a Python number')
115     return eval(theString, {}, {})
116
117
118
119 ######
120
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*:')
126
127 def translateClassBasedConfigSyntax(src):
128    
129     """Compiles a config file in the custom class-based SettingsContainer syntax
130     to Vanilla Python
131     
132     # WebKit.config
133     Applications:
134         MyApp:
135             Dirs:
136                 ROOT = '/home/www/Home'
137                 Products = '/home/www/Products'
138     becomes:
139     # WebKit.config
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'
146     """
147    
148     outputLines = []
149     for line in src.splitlines():
150         if customClassRe.match(line) and \
151            line.strip().split(':')[0] not in ('else','try', 'except', 'finally'):
152            
153             line = customClassRe.sub(
154                 r'\g<indent>class \g<class>(SettingsContainer):', line)
155            
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)
159            
160         outputLines.append(line)
161
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; '
165     else:
166         initLine = 'from Cheetah.SettingsManager import SettingsContainer; True, False = 1, 0\n'
167     return initLine + '\n'.join(outputLines) + '\n'
168
169
170 ##################################################
171 ## CLASSES ##
172
173 class Error(BaseErrorClass):
174     pass
175
176 class NoDefault:
177     pass
178
179 class ConfigParserCaseSensitive(ConfigParser):
180    
181     """A case sensitive version of the standard Python ConfigParser."""
182    
183     def optionxform(self, optionstr):
184        
185         """Don't change the case as is done in the default implemenation."""
186        
187         return optionstr
188
189 class SettingsContainer:
190     """An abstract base class for 'classes' that are used to house settings."""
191     pass
192
193
194 class _SettingsCollector:
195
196     """An abstract base class that provides the methods SettingsManager uses to
197     collect settings from config files and SettingsContainers.
198
199     This class only collects settings it doesn't modify the _settings dictionary
200     of SettingsManager instances in any way.
201
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
210       src files
211     - allow python literals to be used values in .ini config files
212     - maintain the case of setting names, unlike the ConfigParser module
213     
214     """
215
216     _sysPathLock = Lock()   # used by the updateSettingsFromPySrcFile() method
217     _ConfigParserClass = ConfigParserCaseSensitive
218    
219
220     def __init__(self):
221         pass
222
223     def normalizePath(self, path):
224        
225         """A hook for any neccessary path manipulations.
226
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.
230
231         The default implementation just normalizes the path for the current
232         operating system."""
233        
234         return os.path.normpath(path.replace("\\",'/'))
235
236
237     def readSettingsFromContainer(self, container, ignoreUnderscored=True):
238        
239         """Returns all settings from a SettingsContainer or Python
240         module.
241
242         This method is recursive.
243         """
244        
245         S = {}
246         if type(container) == ModuleType:
247             attrs = vars(container)
248         else:
249             attrs = self._getAllAttrsFromContainer(container)
250    
251         for k, v in attrs.items():
252             if (ignoreUnderscored and k.startswith('_')) or v is SettingsContainer:
253                 continue
254             if self._isContainer(v):
255                 S[k] = self.readSettingsFromContainer(v)
256             else:
257                 S[k] = v
258         return S
259
260     # provide an alias
261     readSettingsFromModule = readSettingsFromContainer
262    
263     def _isContainer(self, thing):
264
265         """Check if 'thing' is a Python module or a subclass of
266         SettingsContainer."""
267        
268         return type(thing) == ModuleType or (
269             type(thing) == ClassType and issubclass(thing, SettingsContainer)
270             )
271
272     def _getAllAttrsFromContainer(self, container):
273         """Extract all the attributes of a SettingsContainer subclass.
274
275         The 'container' is a class, so extracting all attributes from it, an
276         instance of it, and all its base classes.
277
278         This method is not recursive.
279         """
280
281         attrs = container.__dict__.copy()
282         # init an instance of the container and get all attributes
283         attrs.update( container().__dict__ )
284        
285         for base in container.__bases__:
286             for k, v in base.__dict__.items():
287                 if not attrs.has_key(k):
288                     attrs[k] = v
289         return attrs
290
291     def readSettingsFromPySrcFile(self, path):
292        
293         """Return new settings dict from variables in a Python source file.
294
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."""
297        
298         path = self.normalizePath(path)
299         dirName = os.path.dirname(path)
300         tmpPath = tempfile.mkstemp('webware_temp')
301        
302         pySrc = translateClassBasedConfigSyntax(open(path).read())
303         modName = path.replace('.','_').replace('/','_').replace('\\','_')       
304         open(tmpPath, 'w').write(pySrc)
305         try:
306             fp = open(tmpPath)
307             self._sysPathLock.acquire()
308             sys.path.insert(0, dirName)
309             module = imp.load_source(modName, path, fp)
310             newSettings = self.readSettingsFromModule(module)
311             del sys.path[0]
312             self._sysPathLock.release()           
313             return newSettings
314         finally:
315             fp.close()
316             try:
317                 os.remove(tmpPath)
318             except:
319                 pass
320             if os.path.exists(tmpPath + 'c'):
321                 try:
322                     os.remove(tmpPath + 'c')
323                 except:
324                     pass
325             if os.path.exists(path + 'c'):
326                 try:
327                     os.remove(path + 'c')
328                 except:
329                     pass
330                
331        
332     def readSettingsFromPySrcStr(self, theString):
333        
334         """Return a dictionary of the settings in a Python src string."""
335
336         globalsDict = {'True':1,
337                        'False':0,
338                        'SettingsContainer':SettingsContainer,
339                        }
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)
346
347     def readSettingsFromConfigFile(self, path, convert=True):
348         path = self.normalizePath(path)
349         fp = open(path)
350         settings = self.readSettingsFromConfigFileObj(fp, convert=convert)
351         fp.close()
352         return settings
353
354     def readSettingsFromConfigFileObj(self, inFile, convert=True):
355        
356         """Return the settings from a config file that uses the syntax accepted by
357         Python's standard ConfigParser module (like Windows .ini files).
358
359         NOTE:
360         this method maintains case unlike the ConfigParser module, unless this
361         class was initialized with the 'caseSensitive' keyword set to False.
362
363         All setting values are initially parsed as strings. However, If the
364         'convert' arg is True this method will do the following value
365         conversions:
366         
367         * all Python numeric literals will be coverted from string to number
368         
369         * The string 'None' will be converted to the Python value None
370         
371         * The string 'True' will be converted to a Python truth value
372         
373         * The string 'False' will be converted to a Python false value
374         
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.
378
379         If a config section titled 'Globals' is present the options defined
380         under it will be treated as top-level settings.       
381         """
382        
383         p = self._ConfigParserClass()
384         p.readfp(inFile)
385         sects = p.sections()
386         newSettings = {}
387
388         sects = p.sections()
389         newSettings = {}
390        
391         for s in sects:
392             newSettings[s] = {}
393             for o in p.options(s):
394                 if o != '__name__':
395                     newSettings[s][o] = p.get(s,o)
396
397         ## loop through new settings -> deal with global settings, numbers,
398         ## booleans and None ++ also deal with 'importSettings' commands
399
400         for sect, subDict in newSettings.items():
401             for key, val in subDict.items():
402                 if convert:
403                     if val.lower().startswith('python:'):
404                         subDict[key] = eval(val[7:],{},{})
405                     if val.lower() == 'none':
406                         subDict[key] = None
407                     if val.lower() == 'true':
408                         subDict[key] = True
409                     if val.lower() == 'false':
410                         subDict[key] = False
411                     if stringIsNumber(val):
412                         subDict[key] = convStringToNum(val)
413                        
414                 ## now deal with any 'importSettings' commands
415                 if key.lower() == 'importsettings':
416                     if val.find(';') < 0:
417                         importedSettings = self.readSettingsFromPySrcFile(val)
418                     else:
419                         path = val.split(';')[0]
420                         rest = ''.join(val.split(';')[1:]).strip()
421                         parentDict = self.readSettingsFromPySrcFile(path)
422                         importedSettings = eval('parentDict["' + rest + '"]')
423                        
424                     subDict.update(mergeNestedDictionaries(subDict,
425                                                            importedSettings))
426                        
427             if sect.lower() == 'globals':
428                 newSettings.update(newSettings[sect])
429                 del newSettings[sect]
430                
431         return newSettings
432
433
434 class SettingsManager(_SettingsCollector):
435    
436     """A mixin class that provides facilities for managing application settings.
437     
438     SettingsManager is designed to work well with nested settings dictionaries
439     of any depth.
440     """
441
442     ## init methods
443     
444     def __init__(self):
445         """MUST BE CALLED BY SUBCLASSES"""
446         _SettingsCollector.__init__(self)
447         self._settings = {}
448         self._initializeSettings()
449
450     def _defaultSettings(self):
451         return {}
452    
453     def _initializeSettings(self):
454        
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.
460         """
461        
462         pass
463
464     ## core post startup methods
465
466     def setting(self, name, default=NoDefault):
467        
468         """Get a setting from self._settings, with or without a default value."""
469        
470         if default is NoDefault:
471             return self._settings[name]
472         else:
473             return self._settings.get(name, default)
474
475
476     def hasSetting(self, key):
477         """True/False"""
478         return self._settings.has_key(key)
479
480     def setSetting(self, name, value):
481         """Set a setting in self._settings."""
482         self._settings[name] = value
483
484     def settings(self):
485         """Return a reference to the settings dictionary"""
486         return self._settings
487        
488     def copySettings(self):
489         """Returns a shallow copy of the settings dictionary"""
490         return copy(self._settings)
491
492     def deepcopySettings(self):
493         """Returns a deep copy of the settings dictionary"""
494         return deepcopy(self._settings)
495    
496     def updateSettings(self, newSettings, merge=True):
497        
498         """Update the settings with a selective merge or a complete overwrite."""
499        
500         if merge:
501             mergeNestedDictionaries(self._settings, newSettings)
502         else:
503             self._settings.update(newSettings)
504
505
506
507
508     ## source specific update methods
509
510     def updateSettingsFromPySrcStr(self, theString, merge=True):
511        
512         """Update the settings from a code in a Python src string."""
513        
514         newSettings = self.readSettingsFromPySrcStr(theString)
515         self.updateSettings(newSettings,
516                             merge=newSettings.get('mergeSettings',merge) )
517        
518     def updateSettingsFromPySrcFile(self, path, merge=True):
519        
520         """Update the settings from variables in a Python source file.
521
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."""
524        
525         newSettings = self.readSettingsFromPySrcFile(path)
526         self.updateSettings(newSettings,
527                             merge=newSettings.get('mergeSettings',merge) )
528
529
530     def updateSettingsFromConfigFile(self, path, **kw):
531        
532         """Update the settings from a text file using the syntax accepted by
533         Python's standard ConfigParser module (like Windows .ini files).
534         """
535        
536         path = self.normalizePath(path)
537         fp = open(path)
538         self.updateSettingsFromConfigFileObj(fp, **kw)
539         fp.close()
540
541    
542     def updateSettingsFromConfigFileObj(self, inFile, convert=True, merge=True):
543        
544         """See the docstring for .updateSettingsFromConfigFile()
545
546         The caller of this method is responsible for closing the inFile file
547         object."""
548
549         newSettings = self.readSettingsFromConfigFileObj(inFile, convert=convert)
550         self.updateSettings(newSettings,
551                             merge=newSettings.get('mergeSettings',merge))
552
553     def updateSettingsFromConfigStr(self, configStr, convert=True, merge=True):
554        
555         """See the docstring for .updateSettingsFromConfigFile()
556         """
557
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))
563
564
565     ## methods for output representations of the settings
566
567     def _createConfigFile(self, outFile=None):
568        
569         """
570         Write all the settings that can be represented as strings to an .ini
571         style config string.
572
573         This method can only handle one level of nesting and will only work with
574         numbers, strings, and None.
575             """
576
577         if outFile is None:
578             outFile = StringIO()
579         iniSettings = {'Globals':{}}
580         globals = iniSettings['Globals']
581        
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
590        
591         sections = iniSettings.keys()
592         sections.sort()
593         outFileWrite = outFile.write # short-cut namebinding for efficiency
594         for section in sections:
595             outFileWrite("[" + section + "]\n")
596             sectDict = iniSettings[section]
597            
598             keys = sectDict.keys()
599             keys.sort()
600             for key in keys:
601                 if key == "__name__":
602                     continue
603                 outFileWrite("%s = %s\n" % (key, sectDict[key]))
604             outFileWrite("\n")
605
606         return outFile
607        
608     def writeConfigFile(self, path):
609        
610         """Write all the settings that can be represented as strings to an .ini
611         style config file."""
612        
613         path = self.normalizePath(path)
614         fp = open(path,'w')
615         self._createConfigFile(fp)
616         fp.close()
617        
618     def getConfigString(self):
619         """Return a string with the settings in .ini file format."""
620        
621         return self._createConfigFile().getvalue()
622
623 # vim: shiftwidth=4 tabstop=4 expandtab
624
Note: See TracBrowser for help on using the browser.