Tryag File Manager
Home
-
Turbo Force
Current Path :
/
usr
/
lib
/
python2.4
/
site-packages
/
mx
/
Misc
/
Upload File :
New :
File
Dir
//usr/lib/python2.4/site-packages/mx/Misc/ConfigFile.py
""" ConfigFile -- Interface to simple template based configuration files. The format of these files is as follows: globalvar = 2 [ABC] a = 1 b = abc.html c = text with spaces [DEF] a = 2 b = a + 3 c = a string Entries in square brackets indicate new subsections. Global variables may be set prior to starting any subsection. Emtpy lines and lines starting with '#' (comments) are ignored. Indentation is not necessary; lines can start at any column. Entries may span multiple lines by using '\' continuations at the line ends, e.g. [Continuation] a = first line\ second line The lines are stripped of any white space before removing the trailing '\' and concatenating them. Comment lines are removed as well. To parse these files, a template in form of a class including subclasses (identifying the subsections) must be given to the reader. This template defines which sections and attributes are known. All others are rejected. A sample template for the above looks like this: from mx.Misc.ConfigFile import * class Template(ConfigNamespace): global = IntegerEntry() class ABC(ConfigSection): a = IntegerEntry('0') b = StringEntry('default.html') c = StringEntry() class DEF(ConfigSection): a = IntegerEntry('0') b = EvalEntry('0') c = StringEntry('default value') The main class representing the global namespace of the ini file must have ConfigNamespace as baseclass. It may contain any number of ConfigSection subclasses each defining parsed attributes. The ini file may only contain entries defined in this template. In case an unkown section is found, the main class is looked up for a ConfigSection subclass with name 'DefaultSection'. This section object is then taken as template for a new section of the given name. The same feature is available for attributes in sections. In case a given attribute is not found in the templates, the section's 'DefaultAttribute' attribute is deepcopied and the used as attribute template. Needs mxTools to be installed. XXX Add support for long strings (including embedded control characters and spanning multiple lines) Copyright (c) 2000, Marc-Andre Lemburg; All Rights Reserved. See the documentation for further information on copyrights, or contact the author. All Rights Reserved. """ import string,copy,re,types,exceptions,sys,os import mx.Tools.NewBuiltins from mx.Tools import freeze import Namespace # Print debug information ? _debug = 0 ### Errors class Error(exceptions.StandardError): pass ### Helpers shell_var = re.compile('(?:\$([a-zA-Z0-9._]+)|\${([^}]*)})') def expand_vars(text,locals,globals, split=string.split,shell_var=shell_var): """ Expands all variables of the form $var or ${var} using the dictionaries locals,globals,os.environ in that order as database. Default value is the empty string, just like for shell environment variables. """ while 1: m = shell_var.search(text) if not m: break g = m.groups() varpath = split(g[0] or g[1],'.') varname = varpath[0] # Find object with name varname value = locals.get(varname,None) if value is None: value = globals.get(varname,None) if value is None: value = os.environ.get(varname,'') # Now get the requested attribute for attr in varpath[1:]: try: value = getattr(value,attr) except AttributeError: raise Error,'attribute "%s" undefined' % attr # Reformat text by inserting value as string l,r = m.span() if isinstance(value,Entry): entry = value if entry.value is NotGiven: entry.parse(entry.default,locals,globals) value = entry.value text = text[:l] + str(value) + text[r:] return text ### Template entry fields class Entry: """ Converts the value to another datatype depending on the .converters attribute. Each of those functions is applied to the value. The first one to not raise an exception succeeds. """ # Value value = NotGiven # Default value default = '' # Converters to be tried; order is important the first successful # one wins converters = (int,float,str) def __init__(self,default=NotGiven): if default is not NotGiven: self.default = default def parse(self,text,locals,globals): """ Parse text according to the namespace dictionaries locals and globals. Sets self.value and returns the parsed value as well. """ text = self.pre_process(str(text),locals,globals) for converter in self.converters: try: value = converter(text) break except: lastexc = sys.exc_info()[1] else: # Reraise the last exception raise lastexc lastexc = None self.value = value = self.post_process(value,locals,globals) return value def pre_process(self,text,locals,globals, strip=string.strip,expand_vars=expand_vars): """ Preprocess the text value. Default behaviour is to strip the text and then apply variable expansion. """ return expand_vars(strip(text),locals,globals) def post_process(self,value,locals,globals): """ Postprocess the value after conversion has been applied. The default behaviour is to leave it as it is. """ return value def __str__(self): """ Return a stringified version of self.value. The default value is parsed in case no value has yet been set. """ if self.value is NotGiven: self.parse(self.default,{},{}) return str(self.value) ### class IntegerEntry(Entry): """ Converts the value to an integer. """ converters = (int,) class FloatEntry(Entry): """ Converts the value to a float. """ converters = (float,) class NumericEntry(Entry): """ Converts the value to a number (integer or float). """ converters = (int,float) class StringEntry(Entry): """ Converts the value to a string. """ converters = (str,) def comma_list(text, strip=string.strip,split=string.split,map=map,tuple=tuple): return map(strip,split(text,',')) def spaced_list(text, strip=string.strip,split=string.split,map=map,tuple=tuple): return map(strip,split(text)) class TupleEntry(Entry): """ Converts the value to a tuple of strings. value must be a string with entries separated by ','. Entries are split at each occurance of ',' and then stripped of surrounding spaces. """ converters = (comma_list,) def post_process(self,value,locals,globals): return tuple(value) # Alias StringTupleEntry = TupleEntry CommaTupleEntry = TupleEntry class ListEntry(Entry): """ Converts the value to a list of strings. value must be a string with entries separated by ','. Entries are split at each occurance of ',' and then stripped of surrounding spaces. """ converters = (comma_list,) # Alias StringListEntry = ListEntry CommaListEntry = ListEntry class SpacedTupleEntry(TupleEntry): """ Converts the value to a tuple of strings. value must be a string with entries separated by whitespace. Entries are split at each occurance of whitespace; surrounding spaces is removed. """ converters = (spaced_list,) class SpacedListEntry(ListEntry): """ Converts the value to a list of strings. value must be a string with entries separated by whitespace. Entries are split at each occurance of whitespace; surrounding spaces is removed. """ converters = (spaced_list,) class FileEntry(Entry): """ Entry field for an OS file. """ converters = (str,) class PathEntry(Entry): """ Entry field for an OS path. Paths will always end with os.sep if given. """ converters = (str,) def post_process(self,value,locals,globals): if value and value[-1] != os.sep: value = value + os.sep return value class AbsoluteFileEntry(Entry): """ Entry field for an absolute OS pathname. The pathname will always start with os.sep if given. """ converters = (str,) def post_process(self,value,locals,globals): if value and value[0] != os.sep: value = os.sep + value return value class AbsolutePathEntry(Entry): """ Entry field for an absolute OS path. The pathname will always start and end with os.sep if given. """ converters = (str,) def post_process(self,value,locals,globals): if value: if value[0] != os.sep: value = os.sep + value if value[-1] != os.sep: value = value + os.sep return value class RelativePathEntry(PathEntry): """ Entry field for relative paths. The path stored is the result of joining the given parameter with a basepath. basepath is subject to variable expansion at parsing time. """ def __init__(self,basepath,default=NotGiven): self.basepath = basepath PathEntry.__init__(self,default) def post_process(self,value,locals,globals): return os.path.join(expand_vars(self.basepath,locals,globals),value) class RelativeFileEntry(FileEntry): """ Entry field for relative pathnames to files. The path stored is the result of joining the given parameter with a basepath. basepath is subject to variable expansion at parsing time. """ def __init__(self,basepath,default=NotGiven): self.basepath = basepath PathEntry.__init__(self,default) def post_process(self,value,locals,globals): return os.path.join(expand_vars(self.basepath,locals,globals),value) class ExistingPathEntry(PathEntry): """ Checks value to point to an existing OS path. Raises an exception if the path does not exist. """ def post_process(self,value,locals,globals): if value and value[-1] != os.sep: value = value + os.sep if not os.path.exists(value): raise Error,'non existing path "%s"' % value return value class ExistingFileEntry(FileEntry): """ Checks value to point to an existing file. Raises an exception if the file does not exist. """ def post_process(self,value,locals,globals): if not value or not os.path.exists(value): raise Error,'non existing file "%s"' % value return value class ExistingRelativePathEntry(RelativePathEntry): """ Entry field for relative paths. The path stored is the result of joining the given parameter with a basepath. Raises an exception if the path does not exist. """ def post_process(self,value,locals,globals): value = os.path.join(expand_vars(self.basepath,locals,globals),value) if not os.path.exists(value): raise Error,'non existing path "%s"' % value return value class URLEntry(StringEntry): """ Entry field for URLs. The field takes a default value as argument. Needs mxHTMLTools to be installed. """ def post_process(self,value,locals,globals): from mx import HTMLTools return HTMLTools.URL(value) class RelativeURLEntry(URLEntry): """ Entry field for relative URLs. A base URL can be set which is then urljoined with the value given. The field also takes a default value. Needs mxHTMLTools to be installed. """ def __init__(self,baseurl='',default=NotGiven): self.baseurl = baseurl StringEntry.__init__(self,default) def post_process(self,value,locals,globals): from mx import HTMLTools return HTMLTools.urljoin(expand_vars(self.baseurl,locals,globals), value) class AbsoluteURLEntry(RelativeURLEntry): """ Entry field for absolute URLs. A base URL can be set which is then urljoined with the value given. The field also takes a default value as second argument. """ def post_process(self,value,locals,globals): url = RelativeURLEntry.post_process(self,value,locals,globals) if not url.absolute: raise Error,'need an absolute URL: "%s"' % url return url class DateEntry(Entry): """ Date entry field. The value is stored using a DateTime instance. A time part is ignored. Needs mxDateTime to be installed. """ def post_process(self,value,locals,globals): from mx.DateTime.Parser import DateFromString return DateFromString(value) class DateTimeEntry(Entry): """ Date/time entry field. The value is stored using a DateTime instance. Needs mxDateTime to be installed. """ def post_process(self,value,locals,globals): from mx.DateTime.Parser import DateTimeFromString return DateTimeFromString(value) class TimeEntry(Entry): """ Time entry field. The value is stored using a DateTimeDelta instance. A date part is ignored. Needs mxDateTime to be installed. """ def post_process(self,value,locals,globals): from mx.DateTime.Parser import TimeFromString return TimeFromString(value) class DateTimeDeltaEntry(Entry): """ Date/time delta entry field. The value is stored using a DateTimeDelta instance. Needs mxDateTime to be installed. """ def post_process(self,value,locals,globals): from mx.DateTime.Parser import DateTimeDeltaFromString return DateTimeDeltaFromString(value) class RelativeDateTimeEntry(Entry): """ Relative date/time entry field. The value is stored using a RelativeDateTime instance. Needs mxDateTime to be installed. """ def post_process(self,value,locals,globals): from mx.DateTime.Parser import RelativeDateTimeFromString return RelativeDateTimeFromString(value) class EvalEntry(Entry): """ Allows simple calculations to be done using the current locals (the symbols defined in the class where the entry is located) and globals. """ def parse(self,text,locals,globals, eval=eval): self.value = eval(text,locals,globals) return self.value class SafeEvalEntry(Entry): """ Allows simple calculations to be done using the current locals (the symbols defined in the class where the entry is located) and globals. Builtins are not available. """ def parse(self,text,locals,globals, reval=reval): self.value = reval(text,locals,globals) return self.value class DictEntry(Entry): """ Allows the construction of a Python dictionary. The entries value is taken as list of key: value pairs which are evaluated in the current locals (the symbols defined in the class where the entry is located) and globals. Parsing is left to the Python interpreter. The needed curly brackets {} are added by the parsing method. Sample: dict = 'a': (1,2,3), 'b': (3,4,5) """ def parse(self,text,locals,globals): self.value = eval('{' + text + '}',locals,globals) return self.value ### Namespace containers class ConfigNamespace(Namespace.Namespace): # This attribute may be set to allow new sections to be # created by the ini-file editor. The parser will use this # class to instantiate those sections. DefaultSection = None # This attribute may be set to allow new attributes to be # created by the ini-file editor. The parser will use this # object as template for the new attributes by deepcopying # it. DefaultAttribute = None def __init__(self, getattr=getattr, skip_types=(types.ModuleType, types.IntType, types.StringType, types.NoneType, ), del_types=(types.FunctionType, types.UnboundMethodType, types.MethodType, types.BuiltinFunctionType, types.BuiltinMethodType,), deepcopy=copy.deepcopy,ClassType=types.ClassType): # Localize class attributes in the instance dict classobj = self.__class__ freeze(classobj) dict = self.__dict__ dict.update(classobj.__dict__) # Fixup attributes for name,obj in dict.items(): if name[:1] == '_ ': # Delete all special attributes del dict[name] objtype = type(obj) if objtype is ClassType: # Handle classes if name != 'DefaultSection' and \ issubclass(obj,ConfigSection): # Instantiate ConfigSections dict[name] = obj() # leave all others as classes elif objtype in del_types: # Delete these types del dict[name] elif objtype in skip_types: # Skip these types pass else: # Deepcopy everything else try: dict[name] = deepcopy(obj) except copy.error,why: raise Error,\ 'namespace entry "%s" could not be copied: %s' % \ (name,why) # # Dictionary like interface (we hide the internally used attributes) # def _items(self): items = [] append = items.append for k,v in self.__dict__.items(): if k[:1] == '_' or \ k in ('DefaultSection', 'DefaultAttribute', 'baseobj'): continue append((k,v)) return items def _keys(self): items = [] append = items.append for k,v in self.__dict__.items(): if k[:1] == '_' or \ k in ('DefaultSection', 'DefaultAttribute', 'baseobj'): continue append(k) return items def _values(self): items = [] append = items.append for k,v in self.__dict__.items(): if k[:1] == '_' or \ k in ('DefaultSection', 'DefaultAttribute', 'baseobj'): continue append(v) return items def _dictionary(self): d = {} d.update(self.__dict__) # Remove internal attributes for k in d.keys(): if k[:1] == '_' or \ k in ('DefaultSection', 'DefaultAttribute', 'baseobj'): del d[k] return d class ConfigSection(ConfigNamespace): pass ### parse_section = re.compile('\[([a-zA-Z_][a-zA-Z_0-9]*)\]') parse_setattr = re.compile('([a-zA-Z_][a-zA-Z_0-9]*)[ \t]*=[ \t]*(.*)') class ConfigFile: """ Configuration file reader. Takes a template (a ConfigNamespace subclass) as input which defines sections using classes. Sections may include instances of Entry as attributes. These are then used to process the file input. The template is (deep-)copied and placed into the instance variable .data (a ConfigNamespace instance) prior to reading the file. Section classes are replaced with ConfigSection instances. After successful reading the file, the configuration information is available through this variable. Errors are indicated by exceptions of type Error. These always have values (line_number, explanation) where line_number is 0 for errors which do not refer to one specific line in the file. """ data = None # configuration namespace def __init__(self,template): self.template = template self.reset() def reset(self, deepcopy=copy.deepcopy,ClassType=types.ClassType, ModuleType=types.ModuleType): """ Reset the object to its initial state. This initializes self.data to a template instance. """ self.data = self.template() def read(self,file, strip=string.strip,deepcopy=copy.deepcopy): """ Read and parse the open file. The configuration is stored in the instance variable .data. """ context = {} sectionname = '' data = self.data # Configuration object section = data # Current section object globals = data.__dict__ # Global namespace locals = globals # Local namespace lineno = 0 if _debug: print 'Reading config file:' while 1: line = file.readline() lineno = lineno + 1 if not line: break line = strip(line) if not line or line[0] == '#': # Comment or emtpy line: ignore continue if _debug: print ' read %s' % repr(line) # Continuation current_lineno = lineno while line[-1] == '\\': nextline = file.readline() lineno = lineno + 1 if not nextline: # EOF: end of continuation line = line[:-1] break nextline = strip(nextline) if not nextline: # Empty line: end of continuation line = line[:-1] break if nextline[0] == '#': # Comment: ignore continue if _debug: print ' read continuation %s' % repr(nextline) # Add nextline to line line = line[:-1] + nextline if _debug: print ' processing %s' % repr(line) m = parse_section.match(line) if m: # Start a new section sectionname = m.groups()[0] section = getattr(data,sectionname,None) if section is None: if data.DefaultSection: # Instantiate default section for use as # section object and register in data section = data.DefaultSection() setattr(data,sectionname,section) else: raise Error,(current_lineno, 'unkown section "%s"' % sectionname) #print 'Switched to section:',sectionname locals = section.__dict__ continue m = parse_setattr.match(line) if m: # Add a new attribute name,value = m.groups() entry = locals.get(name,None) if sectionname: attrname = sectionname + '.' + name else: attrname = name if entry is None: if section.DefaultAttribute: # Instantiate default attribute for use as # attribute object and register in section entry = deepcopy(section.DefaultAttribute) setattr(section,name,entry) else: raise Error,(current_lineno, 'unkown attribute: "%s"' % attrname) try: entry.parse(value,locals,globals) except: raise Error,(current_lineno, 'invalid attribute value for "%s" (%s: %s)' % (attrname, sys.exc_info()[0],sys.exc_info()[1])) # Replace with parsed value locals[name] = entry.value continue # The line doesn't have a valid syntax: ignore it if _debug: print 'Ignoring invalid config file line %i: %s' % \ (current_lineno, repr(line)) # Check for missing entries and replace with parsed values for name,obj in globals.items(): if isinstance(obj,Entry): if obj.value is not NotGiven: globals[name] = obj.value else: try: obj.parse(obj.default,locals,globals) globals[name] = obj.value except: raise Error,\ (0, 'invalid default attribute ' 'value for global "%s" (%s)' % (name,str(sys.exc_info()[1]))) elif isinstance(obj,ConfigSection): locals = obj.__dict__ sectionname = name for name,obj in locals.items(): if isinstance(obj,Entry): if obj.value is not NotGiven: locals[name] = obj.value else: try: obj.parse(obj.default,locals,globals) locals[name] = obj.value except: import traceback; traceback.print_exc() raise Error,\ (0, 'invalid default attribute ' 'value for local "%s" (%s)' % (name,str(sys.exc_info()[1]))) ### Test if __name__ == '__main__': class Template(ConfigNamespace): import sys globalvar = IntegerEntry(1) # New attributes in the global section are strings DefaultAttribute = StringEntry('') class ABC(ConfigSection): a = IntegerEntry('0') b = StringEntry('default.html') c = StringEntry() d = StringTupleEntry('a,b') path = ExistingPathEntry('.') # Use this as template for new attributes DefaultAttribute = RelativePathEntry('..','') class DEF(ConfigSection): a = IntegerEntry('0') b = EvalEntry('0') c = StringEntry('default value') d = RelativePathEntry('$ABC.b','HOME') g = IntegerEntry('0') continuation = CommaTupleEntry('') databases = DictEntry('') timeout = TimeEntry('0:15:00.23') # Default section DefaultSection = ABC text = """\ # Luckily we don't have to pay attention to proper indentation # or whether strings have quotes or not... the template knows # what to do. globalvar = 2 # New global attribute test = text [ABC] a = 1 b = abc.html c = text with spaces d = heinz, kunz, philipp path = /tmp # A new attribute new = .cshrc [DEF] a = 2 b = a + 3 c = $PWD d = home continuation = first line, \\ # Comment should not hurt second line, \\ # Invalid syntax: third line \\ databases = 'iODBC': (sys,sys.exit,('c',1,2)), \\ 'Adabas': (sys,sys.exit,('x','y','z')) [NewSection] a = 3 """ import cStringIO f = cStringIO.StringIO(text) cf = ConfigFile(Template) cf.read(f)