# configobj.py# A config file reader/writer that supports nested sections in config files.# Copyright (C) 2005-2006 Michael Foord, Nicola Larosa# E-mail: fuzzyman AT voidspace DOT org DOT uk# nico AT tekNico DOT net# ConfigObj 4# http://www.voidspace.org.uk/python/configobj.html# Released subject to the BSD License# Please see http://www.voidspace.org.uk/python/license.shtml# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml# For information about bugfixes, updates and support, please join the# ConfigObj mailing list:# http://lists.sourceforge.net/lists/listinfo/configobj-develop# Comments, suggestions and bug reports welcome.from__future__importgeneratorsimportsysINTP_VER=sys.version_info[:2]ifINTP_VER<(2,2):raiseRuntimeError("Python v.2.2 or later needed")importos,recompiler=Nonetry:importcompilerexceptImportError:# for IronPythonpassfromtypesimportStringTypesfromwarningsimportwarntry:fromcodecsimportBOM_UTF8,BOM_UTF16,BOM_UTF16_BE,BOM_UTF16_LEexceptImportError:# Python 2.2 does not have these# UTF-8BOM_UTF8='\xef\xbb\xbf'# UTF-16, little endianBOM_UTF16_LE='\xff\xfe'# UTF-16, big endianBOM_UTF16_BE='\xfe\xff'ifsys.byteorder=='little':# UTF-16, native endiannessBOM_UTF16=BOM_UTF16_LEelse:# UTF-16, native endiannessBOM_UTF16=BOM_UTF16_BE# A dictionary mapping BOM to# the encoding to decode with, and what to set the# encoding attribute to.BOMS={BOM_UTF8:('utf_8',None),BOM_UTF16_BE:('utf16_be','utf_16'),BOM_UTF16_LE:('utf16_le','utf_16'),BOM_UTF16:('utf_16','utf_16'),}# All legal variants of the BOM codecs.# TODO: the list of aliases is not meant to be exhaustive, is there a# better way ?BOM_LIST={'utf_16':'utf_16','u16':'utf_16','utf16':'utf_16','utf-16':'utf_16','utf16_be':'utf16_be','utf_16_be':'utf16_be','utf-16be':'utf16_be','utf16_le':'utf16_le','utf_16_le':'utf16_le','utf-16le':'utf16_le','utf_8':'utf_8','u8':'utf_8','utf':'utf_8','utf8':'utf_8','utf-8':'utf_8',}# Map of encodings to the BOM to write.BOM_SET={'utf_8':BOM_UTF8,'utf_16':BOM_UTF16,'utf16_be':BOM_UTF16_BE,'utf16_le':BOM_UTF16_LE,None:BOM_UTF8}try:fromvalidateimportVdtMissingValueexceptImportError:VdtMissingValue=Nonetry:enumerateexceptNameError:defenumerate(obj):"""enumerate for Python 2.2."""i=-1foriteminobj:i+=1yieldi,itemtry:True,FalseexceptNameError:True,False=1,0__version__='4.4.0'__revision__='$Id: configobj.py,v 3.5 2007/07/02 18:20:24 benjamin%smedbergs.us Exp $'__docformat__="restructuredtext en"__all__=('__version__','DEFAULT_INDENT_TYPE','DEFAULT_INTERPOLATION','ConfigObjError','NestingError','ParseError','DuplicateError','ConfigspecError','ConfigObj','SimpleVal','InterpolationError','InterpolationLoopError','MissingInterpolationOption','RepeatSectionError','UnreprError','UnknownType','__docformat__','flatten_errors',)DEFAULT_INTERPOLATION='configparser'DEFAULT_INDENT_TYPE=' 'MAX_INTERPOL_DEPTH=10OPTION_DEFAULTS={'interpolation':True,'raise_errors':False,'list_values':True,'create_empty':False,'file_error':False,'configspec':None,'stringify':True,# option may be set to one of ('', ' ', '\t')'indent_type':None,'encoding':None,'default_encoding':None,'unrepr':False,'write_empty_values':False,}defgetObj(s):s="a="+sifcompilerisNone:raiseImportError('compiler module not available')p=compiler.parse(s)returnp.getChildren()[1].getChildren()[0].getChildren()[1]classUnknownType(Exception):passclassBuilder:defbuild(self,o):m=getattr(self,'build_'+o.__class__.__name__,None)ifmisNone:raiseUnknownType(o.__class__.__name__)returnm(o)defbuild_List(self,o):returnmap(self.build,o.getChildren())defbuild_Const(self,o):returno.valuedefbuild_Dict(self,o):d={}i=iter(map(self.build,o.getChildren()))forelini:d[el]=i.next()returnddefbuild_Tuple(self,o):returntuple(self.build_List(o))defbuild_Name(self,o):ifo.name=='None':returnNoneifo.name=='True':returnTrueifo.name=='False':returnFalse# An undefinted NameraiseUnknownType('Undefined Name')defbuild_Add(self,o):real,imag=map(self.build_Const,o.getChildren())try:real=float(real)exceptTypeError:raiseUnknownType('Add')ifnotisinstance(imag,complex)orimag.real!=0.0:raiseUnknownType('Add')returnreal+imagdefbuild_Getattr(self,o):parent=self.build(o.expr)returngetattr(parent,o.attrname)defbuild_UnarySub(self,o):return-self.build_Const(o.getChildren()[0])defbuild_UnaryAdd(self,o):returnself.build_Const(o.getChildren()[0])defunrepr(s):ifnots:returnsreturnBuilder().build(getObj(s))def_splitlines(instring):"""Split a string on lines, without losing line endings or truncating."""classConfigObjError(SyntaxError):""" This is the base class for all errors that ConfigObj raises. It is a subclass of SyntaxError. """def__init__(self,message='',line_number=None,line=''):self.line=lineself.line_number=line_numberself.message=messageSyntaxError.__init__(self,message)classNestingError(ConfigObjError):""" This error indicates a level of nesting that doesn't match. """classParseError(ConfigObjError):""" This error indicates that a line is badly written. It is neither a valid ``key = value`` line, nor a valid section marker line. """classDuplicateError(ConfigObjError):""" The keyword or section specified already exists. """classConfigspecError(ConfigObjError):""" An error occurred whilst parsing a configspec. """classInterpolationError(ConfigObjError):"""Base class for the two interpolation errors."""classInterpolationLoopError(InterpolationError):"""Maximum interpolation depth exceeded in string interpolation."""def__init__(self,option):InterpolationError.__init__(self,'interpolation loop detected in value "%s".'%option)classRepeatSectionError(ConfigObjError):""" This error indicates additional sections in a section with a ``__many__`` (repeated) section. """classMissingInterpolationOption(InterpolationError):"""A value specified for interpolation was missing."""def__init__(self,option):InterpolationError.__init__(self,'missing option "%s" in interpolation.'%option)classUnreprError(ConfigObjError):"""An error parsing in unrepr mode."""classInterpolationEngine(object):""" A helper class to help perform string interpolation. This class is an abstract base class; its descendants perform the actual work. """# compiled regexp to use in self.interpolate()_KEYCRE=re.compile(r"%\(([^)]*)\)s")def__init__(self,section):# the Section instance that "owns" this engineself.section=sectiondefinterpolate(self,key,value):defrecursive_interpolate(key,value,section,backtrail):"""The function that does the actual work. ``value``: the string we're trying to interpolate. ``section``: the section in which that string was found ``backtrail``: a dict to keep track of where we've been, to detect and prevent infinite recursion loops This is similar to a depth-first-search algorithm. """# Have we been here already?ifbacktrail.has_key((key,section.name)):# Yes - infinite loop detectedraiseInterpolationLoopError(key)# Place a marker on our backtrail so we won't come back here againbacktrail[(key,section.name)]=1# Now start the actual workmatch=self._KEYCRE.search(value)whilematch:# The actual parsing of the match is implementation-dependent,# so delegate to our helper functionk,v,s=self._parse_match(match)ifkisNone:# That's the signal that no further interpolation is neededreplacement=velse:# Further interpolation may be needed to obtain final valuereplacement=recursive_interpolate(k,v,s,backtrail)# Replace the matched string with its final valuestart,end=match.span()value=''.join((value[:start],replacement,value[end:]))new_search_start=start+len(replacement)# Pick up the next interpolation key, if any, for next time# through the while loopmatch=self._KEYCRE.search(value,new_search_start)# Now safe to come back here again; remove marker from backtraildelbacktrail[(key,section.name)]returnvalue# Back in interpolate(), all we have to do is kick off the recursive# function with appropriate starting valuesvalue=recursive_interpolate(key,value,self.section,{})returnvaluedef_fetch(self,key):"""Helper function to fetch values from owning section. Returns a 2-tuple: the value, and the section where it was found. """# switch off interpolation before we try and fetch anything !save_interp=self.section.main.interpolationself.section.main.interpolation=False# Start at section that "owns" this InterpolationEnginecurrent_section=self.sectionwhileTrue:# try the current section firstval=current_section.get(key)ifvalisnotNone:break# try "DEFAULT" nextval=current_section.get('DEFAULT',{}).get(key)ifvalisnotNone:break# move up to parent and try again# top-level's parent is itselfifcurrent_section.parentiscurrent_section:# reached top level, time to give upbreakcurrent_section=current_section.parent# restore interpolation to previous value before returningself.section.main.interpolation=save_interpifvalisNone:raiseMissingInterpolationOption(key)returnval,current_sectiondef_parse_match(self,match):"""Implementation-dependent helper function. Will be passed a match object corresponding to the interpolation key we just found (e.g., "%(foo)s" or "$foo"). Should look up that key in the appropriate config file section (using the ``_fetch()`` helper function) and return a 3-tuple: (key, value, section) ``key`` is the name of the key we're looking for ``value`` is the value found for that key ``section`` is a reference to the section where it was found ``key`` and ``section`` should be None if no further interpolation should be performed on the resulting value (e.g., if we interpolated "$$" and returned "$"). """raiseNotImplementedErrorclassConfigParserInterpolation(InterpolationEngine):"""Behaves like ConfigParser."""_KEYCRE=re.compile(r"%\(([^)]*)\)s")def_parse_match(self,match):key=match.group(1)value,section=self._fetch(key)returnkey,value,sectionclassTemplateInterpolation(InterpolationEngine):"""Behaves like string.Template."""_delimiter='$'_KEYCRE=re.compile(r""" \$(?: (?P<escaped>\$) | # Two $ signs (?P<named>[_a-z][_a-z0-9]*) | # $name format {(?P<braced>[^}]*)} # ${name} format ) """,re.IGNORECASE|re.VERBOSE)def_parse_match(self,match):# Valid name (in or out of braces): fetch value from sectionkey=match.group('named')ormatch.group('braced')ifkeyisnotNone:value,section=self._fetch(key)returnkey,value,section# Escaped delimiter (e.g., $$): return single delimiterifmatch.group('escaped')isnotNone:# Return None for key and section to indicate it's time to stopreturnNone,self._delimiter,None# Anything else: ignore completely, just return it unchangedreturnNone,match.group(),Noneinterpolation_engines={'configparser':ConfigParserInterpolation,'template':TemplateInterpolation,}classSection(dict):""" A dictionary-like object that represents a section in a config file. It does string interpolation if the 'interpolation' attribute of the 'main' object is set to True. Interpolation is tried first from this object, then from the 'DEFAULT' section of this object, next from the parent and its 'DEFAULT' section, and so on until the main object is reached. A Section will behave like an ordered dictionary - following the order of the ``scalars`` and ``sections`` attributes. You can use this to change the order of members. Iteration follows the order: scalars, then sections. """def__init__(self,parent,depth,main,indict=None,name=None):""" * parent is the section above * depth is the depth level of this section * main is the main ConfigObj * indict is a dictionary to initialise the section with """ifindictisNone:indict={}dict.__init__(self)# used for nesting level *and* interpolationself.parent=parent# used for the interpolation attributeself.main=main# level of nesting depth of this Sectionself.depth=depth# the sequence of scalar values in this Sectionself.scalars=[]# the sequence of sections in this Sectionself.sections=[]# purely for informationself.name=name# for comments :-)self.comments={}self.inline_comments={}# for the configspecself.configspec={}self._order=[]self._configspec_comments={}self._configspec_inline_comments={}self._cs_section_comments={}self._cs_section_inline_comments={}# for defaultsself.defaults=[]## we do this explicitly so that __setitem__ is used properly# (rather than just passing to ``dict.__init__``)forentryinindict:self[entry]=indict[entry]def_interpolate(self,key,value):try:# do we already have an interpolation engine?engine=self._interpolation_engineexceptAttributeError:# not yet: first time running _interpolate(), so pick the enginename=self.main.interpolationifname==True:# note that "if name:" would be incorrect here# backwards-compatibility: interpolation=True means use defaultname=DEFAULT_INTERPOLATIONname=name.lower()# so that "Template", "template", etc. all workclass_=interpolation_engines.get(name,None)ifclass_isNone:# invalid value for self.main.interpolationself.main.interpolation=Falsereturnvalueelse:# save reference to engine so we don't have to do this againengine=self._interpolation_engine=class_(self)# let the engine do the actual workreturnengine.interpolate(key,value)def__getitem__(self,key):"""Fetch the item and do string interpolation."""val=dict.__getitem__(self,key)ifself.main.interpolationandisinstance(val,StringTypes):returnself._interpolate(key,val)returnvaldef__setitem__(self,key,value,unrepr=False):""" Correctly set a value. Making dictionary values Section instances. (We have to special case 'Section' instances - which are also dicts) Keys must be strings. Values need only be strings (or lists of strings) if ``main.stringify`` is set. `unrepr`` must be set when setting a value to a dictionary, without creating a new sub-section. """ifnotisinstance(key,StringTypes):raiseValueError,'The key "%s" is not a string.'%key# add the commentifnotself.comments.has_key(key):self.comments[key]=[]self.inline_comments[key]=''# remove the entry from defaultsifkeyinself.defaults:self.defaults.remove(key)#ifisinstance(value,Section):ifnotself.has_key(key):self.sections.append(key)dict.__setitem__(self,key,value)elifisinstance(value,dict)andnotunrepr:# First create the new depth level,# then create the sectionifnotself.has_key(key):self.sections.append(key)new_depth=self.depth+1dict.__setitem__(self,key,Section(self,new_depth,self.main,indict=value,name=key))else:ifnotself.has_key(key):self.scalars.append(key)ifnotself.main.stringify:ifisinstance(value,StringTypes):passelifisinstance(value,(list,tuple)):forentryinvalue:ifnotisinstance(entry,StringTypes):raiseTypeError,('Value is not a string "%s".'%entry)else:raiseTypeError,'Value is not a string "%s".'%valuedict.__setitem__(self,key,value)def__delitem__(self,key):"""Remove items from the sequence when deleting."""dict.__delitem__(self,key)ifkeyinself.scalars:self.scalars.remove(key)else:self.sections.remove(key)delself.comments[key]delself.inline_comments[key]defget(self,key,default=None):"""A version of ``get`` that doesn't bypass string interpolation."""try:returnself[key]exceptKeyError:returndefaultdefupdate(self,indict):""" A version of update that uses our ``__setitem__``. """forentryinindict:self[entry]=indict[entry]defpop(self,key,*args):""" """val=dict.pop(self,key,*args)ifkeyinself.scalars:delself.comments[key]delself.inline_comments[key]self.scalars.remove(key)elifkeyinself.sections:delself.comments[key]delself.inline_comments[key]self.sections.remove(key)ifself.main.interpolationandisinstance(val,StringTypes):returnself._interpolate(key,val)returnvaldefpopitem(self):"""Pops the first (key,val)"""sequence=(self.scalars+self.sections)ifnotsequence:raiseKeyError,": 'popitem(): dictionary is empty'"key=sequence[0]val=self[key]delself[key]returnkey,valdefclear(self):""" A version of clear that also affects scalars/sections Also clears comments and configspec. Leaves other attributes alone : depth/main/parent are not affected """dict.clear(self)self.scalars=[]self.sections=[]self.comments={}self.inline_comments={}self.configspec={}defsetdefault(self,key,default=None):"""A version of setdefault that sets sequence if appropriate."""try:returnself[key]exceptKeyError:self[key]=defaultreturnself[key]defitems(self):""" """returnzip((self.scalars+self.sections),self.values())defkeys(self):""" """return(self.scalars+self.sections)defvalues(self):""" """return[self[key]forkeyin(self.scalars+self.sections)]defiteritems(self):""" """returniter(self.items())defiterkeys(self):""" """returniter((self.scalars+self.sections))__iter__=iterkeysdefitervalues(self):""" """returniter(self.values())def__repr__(self):return'{%s}'%', '.join([('%s: %s'%(repr(key),repr(self[key])))forkeyin(self.scalars+self.sections)])__str__=__repr__# Extra methods - not in a normal dictionarydefdict(self):""" Return a deepcopy of self as a dictionary. All members that are ``Section`` instances are recursively turned to ordinary dictionaries - by calling their ``dict`` method. >>> n = a.dict() >>> n == a 1 >>> n is a 0 """newdict={}forentryinself:this_entry=self[entry]ifisinstance(this_entry,Section):this_entry=this_entry.dict()elifisinstance(this_entry,list):# create a copy rather than a referencethis_entry=list(this_entry)elifisinstance(this_entry,tuple):# create a copy rather than a referencethis_entry=tuple(this_entry)newdict[entry]=this_entryreturnnewdictdefmerge(self,indict):""" A recursive update - useful for merging config files. >>> a = '''[section1] ... option1 = True ... [[subsection]] ... more_options = False ... # end of file'''.splitlines() >>> b = '''# File is user.ini ... [section1] ... option1 = False ... # end of file'''.splitlines() >>> c1 = ConfigObj(b) >>> c2 = ConfigObj(a) >>> c2.merge(c1) >>> c2 {'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}} """forkey,valinindict.items():if(keyinselfandisinstance(self[key],dict)andisinstance(val,dict)):self[key].merge(val)else:self[key]=valdefrename(self,oldkey,newkey):""" Change a keyname to another, without changing position in sequence. Implemented so that transformations can be made on keys, as well as on values. (used by encode and decode) Also renames comments. """ifoldkeyinself.scalars:the_list=self.scalarselifoldkeyinself.sections:the_list=self.sectionselse:raiseKeyError,'Key "%s" not found.'%oldkeypos=the_list.index(oldkey)#val=self[oldkey]dict.__delitem__(self,oldkey)dict.__setitem__(self,newkey,val)the_list.remove(oldkey)the_list.insert(pos,newkey)comm=self.comments[oldkey]inline_comment=self.inline_comments[oldkey]delself.comments[oldkey]delself.inline_comments[oldkey]self.comments[newkey]=commself.inline_comments[newkey]=inline_commentdefwalk(self,function,raise_errors=True,call_on_sections=False,**keywargs):""" Walk every member and call a function on the keyword and value. Return a dictionary of the return values If the function raises an exception, raise the errror unless ``raise_errors=False``, in which case set the return value to ``False``. Any unrecognised keyword arguments you pass to walk, will be pased on to the function you pass in. Note: if ``call_on_sections`` is ``True`` then - on encountering a subsection, *first* the function is called for the *whole* subsection, and then recurses into its members. This means your function must be able to handle strings, dictionaries and lists. This allows you to change the key of subsections as well as for ordinary members. The return value when called on the whole subsection has to be discarded. See the encode and decode methods for examples, including functions. .. caution:: You can use ``walk`` to transform the names of members of a section but you mustn't add or delete members. >>> config = '''[XXXXsection] ... XXXXkey = XXXXvalue'''.splitlines() >>> cfg = ConfigObj(config) >>> cfg {'XXXXsection': {'XXXXkey': 'XXXXvalue'}} >>> def transform(section, key): ... val = section[key] ... newkey = key.replace('XXXX', 'CLIENT1') ... section.rename(key, newkey) ... if isinstance(val, (tuple, list, dict)): ... pass ... else: ... val = val.replace('XXXX', 'CLIENT1') ... section[newkey] = val >>> cfg.walk(transform, call_on_sections=True) {'CLIENT1section': {'CLIENT1key': None}} >>> cfg {'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}} """out={}# scalars firstforiinrange(len(self.scalars)):entry=self.scalars[i]try:val=function(self,entry,**keywargs)# bound again in case name has changedentry=self.scalars[i]out[entry]=valexceptException:ifraise_errors:raiseelse:entry=self.scalars[i]out[entry]=False# then sectionsforiinrange(len(self.sections)):entry=self.sections[i]ifcall_on_sections:try:function(self,entry,**keywargs)exceptException:ifraise_errors:raiseelse:entry=self.sections[i]out[entry]=False# bound again in case name has changedentry=self.sections[i]# previous result is discardedout[entry]=self[entry].walk(function,raise_errors=raise_errors,call_on_sections=call_on_sections,**keywargs)returnoutdefdecode(self,encoding):""" Decode all strings and values to unicode, using the specified encoding. Works with subsections and list values. Uses the ``walk`` method. Testing ``encode`` and ``decode``. >>> m = ConfigObj(a) >>> m.decode('ascii') >>> def testuni(val): ... for entry in val: ... if not isinstance(entry, unicode): ... print >> sys.stderr, type(entry) ... raise AssertionError, 'decode failed.' ... if isinstance(val[entry], dict): ... testuni(val[entry]) ... elif not isinstance(val[entry], unicode): ... raise AssertionError, 'decode failed.' >>> testuni(m) >>> m.encode('ascii') >>> a == m 1 """warn('use of ``decode`` is deprecated.',DeprecationWarning)defdecode(section,key,encoding=encoding,warn=True):""" """val=section[key]ifisinstance(val,(list,tuple)):newval=[]forentryinval:newval.append(entry.decode(encoding))elifisinstance(val,dict):newval=valelse:newval=val.decode(encoding)newkey=key.decode(encoding)section.rename(key,newkey)section[newkey]=newval# using ``call_on_sections`` allows us to modify section namesself.walk(decode,call_on_sections=True)defencode(self,encoding):""" Encode all strings and values from unicode, using the specified encoding. Works with subsections and list values. Uses the ``walk`` method. """warn('use of ``encode`` is deprecated.',DeprecationWarning)defencode(section,key,encoding=encoding):""" """val=section[key]ifisinstance(val,(list,tuple)):newval=[]forentryinval:newval.append(entry.encode(encoding))elifisinstance(val,dict):newval=valelse:newval=val.encode(encoding)newkey=key.encode(encoding)section.rename(key,newkey)section[newkey]=newvalself.walk(encode,call_on_sections=True)defistrue(self,key):"""A deprecated version of ``as_bool``."""warn('use of ``istrue`` is deprecated. Use ``as_bool`` method ''instead.',DeprecationWarning)returnself.as_bool(key)defas_bool(self,key):""" Accepts a key as input. The corresponding value must be a string or the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to retain compatibility with Python 2.2. If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns ``True``. If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns ``False``. ``as_bool`` is not case sensitive. Any other input will raise a ``ValueError``. >>> a = ConfigObj() >>> a['a'] = 'fish' >>> a.as_bool('a') Traceback (most recent call last): ValueError: Value "fish" is neither True nor False >>> a['b'] = 'True' >>> a.as_bool('b') 1 >>> a['b'] = 'off' >>> a.as_bool('b') 0 """val=self[key]ifval==True:returnTrueelifval==False:returnFalseelse:try:ifnotisinstance(val,StringTypes):raiseKeyErrorelse:returnself.main._bools[val.lower()]exceptKeyError:raiseValueError('Value "%s" is neither True nor False'%val)defas_int(self,key):""" A convenience method which coerces the specified value to an integer. If the value is an invalid literal for ``int``, a ``ValueError`` will be raised. >>> a = ConfigObj() >>> a['a'] = 'fish' >>> a.as_int('a') Traceback (most recent call last): ValueError: invalid literal for int(): fish >>> a['b'] = '1' >>> a.as_int('b') 1 >>> a['b'] = '3.2' >>> a.as_int('b') Traceback (most recent call last): ValueError: invalid literal for int(): 3.2 """returnint(self[key])defas_float(self,key):""" A convenience method which coerces the specified value to a float. If the value is an invalid literal for ``float``, a ``ValueError`` will be raised. >>> a = ConfigObj() >>> a['a'] = 'fish' >>> a.as_float('a') Traceback (most recent call last): ValueError: invalid literal for float(): fish >>> a['b'] = '1' >>> a.as_float('b') 1.0 >>> a['b'] = '3.2' >>> a.as_float('b') 3.2000000000000002 """returnfloat(self[key])classConfigObj(Section):"""An object to read, create, and write config files."""_keyword=re.compile(r'''^ # line start (\s*) # indentation ( # keyword (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'"=].*?) # no quotes ) \s*=\s* # divider (.*) # value (including list values and comments) $ # line end ''',re.VERBOSE)_sectionmarker=re.compile(r'''^ (\s*) # 1: indentation ((?:\[\s*)+) # 2: section marker open ( # 3: section name open (?:"\s*\S.*?\s*")| # at least one non-space with double quotes (?:'\s*\S.*?\s*')| # at least one non-space with single quotes (?:[^'"\s].*?) # at least one non-space unquoted ) # section name close ((?:\s*\])+) # 4: section marker close \s*(\#.*)? # 5: optional comment $''',re.VERBOSE)# this regexp pulls list values out as a single string# or single values and comments# FIXME: this regex adds a '' to the end of comma terminated lists# workaround in ``_handle_value``_valueexp=re.compile(r'''^ (?: (?: ( (?: (?: (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\#][^,\#]*?) # unquoted ) \s*,\s* # comma )* # match all list items ending in a comma (if any) ) ( (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\#\s][^,]*?)| # unquoted (?:(?<!,)) # Empty value )? # last item in a list - or string value )| (,) # alternatively a single comma - empty list ) \s*(\#.*)? # optional comment $''',re.VERBOSE)# use findall to get the members of a list value_listvalueexp=re.compile(r''' ( (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\#].*?) # unquoted ) \s*,\s* # comma ''',re.VERBOSE)# this regexp is used for the value# when lists are switched off_nolistvalue=re.compile(r'''^ ( (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'"\#].*?)| # unquoted (?:) # Empty value ) \s*(\#.*)? # optional comment $''',re.VERBOSE)# regexes for finding triple quoted values on one line_single_line_single=re.compile(r"^'''(.*?)'''\s*(#.*)?$")_single_line_double=re.compile(r'^"""(.*?)"""\s*(#.*)?$')_multi_line_single=re.compile(r"^(.*?)'''\s*(#.*)?$")_multi_line_double=re.compile(r'^(.*?)"""\s*(#.*)?$')_triple_quote={"'''":(_single_line_single,_multi_line_single),'"""':(_single_line_double,_multi_line_double),}# Used by the ``istrue`` Section method_bools={'yes':True,'no':False,'on':True,'off':False,'1':True,'0':False,'true':True,'false':False,}def__init__(self,infile=None,options=None,**kwargs):""" Parse or create a config file object. ``ConfigObj(infile=None, options=None, **kwargs)`` """ifinfileisNone:infile=[]ifoptionsisNone:options={}else:options=dict(options)# keyword arguments take precedence over an options dictionaryoptions.update(kwargs)# init the superclassSection.__init__(self,self,0,self)#defaults=OPTION_DEFAULTS.copy()forentryinoptions.keys():ifentrynotindefaults.keys():raiseTypeError,'Unrecognised option "%s".'%entry# TODO: check the values too.## Add any explicit options to the defaultsdefaults.update(options)## initialise a few variablesself.filename=Noneself._errors=[]self.raise_errors=defaults['raise_errors']self.interpolation=defaults['interpolation']self.list_values=defaults['list_values']self.create_empty=defaults['create_empty']self.file_error=defaults['file_error']self.stringify=defaults['stringify']self.indent_type=defaults['indent_type']self.encoding=defaults['encoding']self.default_encoding=defaults['default_encoding']self.BOM=Falseself.newlines=Noneself.write_empty_values=defaults['write_empty_values']self.unrepr=defaults['unrepr']#self.initial_comment=[]self.final_comment=[]#self._terminated=False#ifisinstance(infile,StringTypes):self.filename=infileifos.path.isfile(infile):infile=open(infile).read()or[]elifself.file_error:# raise an error if the file doesn't existraiseIOError,'Config file not found: "%s".'%self.filenameelse:# file doesn't already existifself.create_empty:# this is a good test that the filename specified# isn't impossible - like on a non existent deviceh=open(infile,'w')h.write('')h.close()infile=[]elifisinstance(infile,(list,tuple)):infile=list(infile)elifisinstance(infile,dict):# initialise self# the Section class handles creating subsectionsifisinstance(infile,ConfigObj):# get a copy of our ConfigObjinfile=infile.dict()forentryininfile:self[entry]=infile[entry]delself._errorsifdefaults['configspec']isnotNone:self._handle_configspec(defaults['configspec'])else:self.configspec=Nonereturnelifhasattr(infile,'read'):# This supports file like objectsinfile=infile.read()or[]# needs splitting into lines - but needs doing *after* decoding# in case it's not an 8 bit encodingelse:raiseTypeError,('infile must be a filename,'' file like object, or list of lines.')#ifinfile:# don't do it for the empty ConfigObjinfile=self._handle_bom(infile)# infile is now *always* a list## Set the newlines attribute (first line ending it finds)# and strip trailing '\n' or '\r' from linesforlineininfile:if(notline)or(line[-1]notin('\r','\n','\r\n')):continueforendin('\r\n','\n','\r'):ifline.endswith(end):self.newlines=endbreakbreakifinfile[-1]andinfile[-1]in('\r','\n','\r\n'):self._terminated=Trueinfile=[line.rstrip('\r\n')forlineininfile]#self._parse(infile)# if we had any errors, now is the time to raise themifself._errors:info="at line %s."%self._errors[0].line_numberiflen(self._errors)>1:msg=("Parsing failed with several errors.\nFirst error %s"%info)error=ConfigObjError(msg)else:error=self._errors[0]# set the errors attribute; it's a list of tuples:# (error_type, message, line_number)error.errors=self._errors# set the config attributeerror.config=selfraiseerror# delete private attributesdelself._errors#ifdefaults['configspec']isNone:self.configspec=Noneelse:self._handle_configspec(defaults['configspec'])def__repr__(self):return'ConfigObj({%s})'%', '.join([('%s: %s'%(repr(key),repr(self[key])))forkeyin(self.scalars+self.sections)])def_handle_bom(self,infile):""" Handle any BOM, and decode if necessary. If an encoding is specified, that *must* be used - but the BOM should still be removed (and the BOM attribute set). (If the encoding is wrongly specified, then a BOM for an alternative encoding won't be discovered or removed.) If an encoding is not specified, UTF8 or UTF16 BOM will be detected and removed. The BOM attribute will be set. UTF16 will be decoded to unicode. NOTE: This method must not be called with an empty ``infile``. Specifying the *wrong* encoding is likely to cause a ``UnicodeDecodeError``. ``infile`` must always be returned as a list of lines, but may be passed in as a single string. """if((self.encodingisnotNone)and(self.encoding.lower()notinBOM_LIST)):# No need to check for a BOM# the encoding specified doesn't have one# just decodereturnself._decode(infile,self.encoding)#ifisinstance(infile,(list,tuple)):line=infile[0]else:line=infileifself.encodingisnotNone:# encoding explicitly supplied# And it could have an associated BOM# TODO: if encoding is just UTF16 - we ought to check for both# TODO: big endian and little endian versions.enc=BOM_LIST[self.encoding.lower()]ifenc=='utf_16':# For UTF16 we try big endian and little endianforBOM,(encoding,final_encoding)inBOMS.items():ifnotfinal_encoding:# skip UTF8continueifinfile.startswith(BOM):### BOM discovered##self.BOM = True# Don't need to remove BOMreturnself._decode(infile,encoding)## If we get this far, will *probably* raise a DecodeError# As it doesn't appear to start with a BOMreturnself._decode(infile,self.encoding)## Must be UTF8BOM=BOM_SET[enc]ifnotline.startswith(BOM):returnself._decode(infile,self.encoding)#newline=line[len(BOM):]## BOM removedifisinstance(infile,(list,tuple)):infile[0]=newlineelse:infile=newlineself.BOM=Truereturnself._decode(infile,self.encoding)## No encoding specified - so we need to check for UTF8/UTF16forBOM,(encoding,final_encoding)inBOMS.items():ifnotline.startswith(BOM):continueelse:# BOM discoveredself.encoding=final_encodingifnotfinal_encoding:self.BOM=True# UTF8# remove BOMnewline=line[len(BOM):]ifisinstance(infile,(list,tuple)):infile[0]=newlineelse:infile=newline# UTF8 - don't decodeifisinstance(infile,StringTypes):returninfile.splitlines(True)else:returninfile# UTF16 - have to decodereturnself._decode(infile,encoding)## No BOM discovered and no encoding specified, just returnifisinstance(infile,StringTypes):# infile read from a file will be a single stringreturninfile.splitlines(True)else:returninfiledef_a_to_u(self,aString):"""Decode ASCII strings to unicode if a self.encoding is specified."""ifself.encoding:returnaString.decode('ascii')else:returnaStringdef_decode(self,infile,encoding):""" Decode infile to unicode. Using the specified encoding. if is a string, it also needs converting to a list. """ifisinstance(infile,StringTypes):# can't be unicode# NOTE: Could raise a ``UnicodeDecodeError``returninfile.decode(encoding).splitlines(True)fori,lineinenumerate(infile):ifnotisinstance(line,unicode):# NOTE: The isinstance test here handles mixed lists of unicode/string# NOTE: But the decode will break on any non-string values# NOTE: Or could raise a ``UnicodeDecodeError``infile[i]=line.decode(encoding)returninfiledef_decode_element(self,line):"""Decode element to unicode if necessary."""ifnotself.encoding:returnlineifisinstance(line,str)andself.default_encoding:returnline.decode(self.default_encoding)returnlinedef_str(self,value):""" Used by ``stringify`` within validate, to turn non-string values into strings. """ifnotisinstance(value,StringTypes):returnstr(value)else:returnvaluedef_parse(self,infile):"""Actually parse the config file."""temp_list_values=self.list_valuesifself.unrepr:self.list_values=Falsecomment_list=[]done_start=Falsethis_section=selfmaxline=len(infile)-1cur_index=-1reset_comment=Falsewhilecur_index<maxline:ifreset_comment:comment_list=[]cur_index+=1line=infile[cur_index]sline=line.strip()# do we have anything on the line ?ifnotslineorsline.startswith('#')orsline.startswith(';'):reset_comment=Falsecomment_list.append(line)continueifnotdone_start:# preserve initial commentself.initial_comment=comment_listcomment_list=[]done_start=Truereset_comment=True# first we check if it's a section markermat=self._sectionmarker.match(line)ifmatisnotNone:# is a section line(indent,sect_open,sect_name,sect_close,comment)=(mat.groups())ifindentand(self.indent_typeisNone):self.indent_type=indentcur_depth=sect_open.count('[')ifcur_depth!=sect_close.count(']'):self._handle_error("Cannot compute the section depth at line %s.",NestingError,infile,cur_index)continue#ifcur_depth<this_section.depth:# the new section is dropping back to a previous leveltry:parent=self._match_depth(this_section,cur_depth).parentexceptSyntaxError:self._handle_error("Cannot compute nesting level at line %s.",NestingError,infile,cur_index)continueelifcur_depth==this_section.depth:# the new section is a sibling of the current sectionparent=this_section.parentelifcur_depth==this_section.depth+1:# the new section is a child the current sectionparent=this_sectionelse:self._handle_error("Section too nested at line %s.",NestingError,infile,cur_index)#sect_name=self._unquote(sect_name)ifparent.has_key(sect_name):self._handle_error('Duplicate section name at line %s.',DuplicateError,infile,cur_index)continue# create the new sectionthis_section=Section(parent,cur_depth,self,name=sect_name)parent[sect_name]=this_sectionparent.inline_comments[sect_name]=commentparent.comments[sect_name]=comment_listcontinue## it's not a section marker,# so it should be a valid ``key = value`` linemat=self._keyword.match(line)ifmatisNone:# it neither matched as a keyword# or a section markerself._handle_error('Invalid line at line "%s".',ParseError,infile,cur_index)else:# is a keyword value# value will include any inline comment(indent,key,value)=mat.groups()ifindentand(self.indent_typeisNone):self.indent_type=indent# check for a multiline valueifvalue[:3]in['"""',"'''"]:try:(value,comment,cur_index)=self._multiline(value,infile,cur_index,maxline)exceptSyntaxError:self._handle_error('Parse error in value at line %s.',ParseError,infile,cur_index)continueelse:ifself.unrepr:comment=''try:value=unrepr(value)exceptException,e:iftype(e)==UnknownType:msg='Unknown name or type in value at line %s.'else:msg='Parse error in value at line %s.'self._handle_error(msg,UnreprError,infile,cur_index)continueelse:ifself.unrepr:comment=''try:value=unrepr(value)exceptException,e:ifisinstance(e,UnknownType):msg='Unknown name or type in value at line %s.'else:msg='Parse error in value at line %s.'self._handle_error(msg,UnreprError,infile,cur_index)continueelse:# extract comment and liststry:(value,comment)=self._handle_value(value)exceptSyntaxError:self._handle_error('Parse error in value at line %s.',ParseError,infile,cur_index)continue#key=self._unquote(key)ifthis_section.has_key(key):self._handle_error('Duplicate keyword name at line %s.',DuplicateError,infile,cur_index)continue# add the key.# we set unrepr because if we have got this far we will never# be creating a new sectionthis_section.__setitem__(key,value,unrepr=True)this_section.inline_comments[key]=commentthis_section.comments[key]=comment_listcontinue#ifself.indent_typeisNone:# no indentation used, set the type accordinglyself.indent_type=''#ifself._terminated:comment_list.append('')# preserve the final commentifnotselfandnotself.initial_comment:self.initial_comment=comment_listelifnotreset_comment:self.final_comment=comment_listself.list_values=temp_list_valuesdef_match_depth(self,sect,depth):""" Given a section and a depth level, walk back through the sections parents to see if the depth level matches a previous section. Return a reference to the right section, or raise a SyntaxError. """whiledepth<sect.depth:ifsectissect.parent:# we've reached the top level alreadyraiseSyntaxErrorsect=sect.parentifsect.depth==depth:returnsect# shouldn't get hereraiseSyntaxErrordef_handle_error(self,text,ErrorClass,infile,cur_index):""" Handle an error according to the error settings. Either raise the error or store it. The error will have occurred at ``cur_index`` """line=infile[cur_index]cur_index+=1message=text%cur_indexerror=ErrorClass(message,cur_index,line)ifself.raise_errors:# raise the error - parsing stops hereraiseerror# store the error# reraise when parsing has finishedself._errors.append(error)def_unquote(self,value):"""Return an unquoted version of a value"""if(value[0]==value[-1])and(value[0]in('"',"'")):value=value[1:-1]returnvaluedef_quote(self,value,multiline=True):""" Return a safely quoted version of a value. Raise a ConfigObjError if the value cannot be safely quoted. If multiline is ``True`` (default) then use triple quotes if necessary. Don't quote values that don't need it. Recursively quote members of a list and return a comma joined list. Multiline is ``False`` for lists. Obey list syntax for empty and single member lists. If ``list_values=False`` then the value is only quoted if it contains a ``\n`` (is multiline). If ``write_empty_values`` is set, and the value is an empty string, it won't be quoted. """ifmultilineandself.write_empty_valuesandvalue=='':# Only if multiline is set, so that it is used for values not# keys, and not values that are part of a listreturn''ifmultilineandisinstance(value,(list,tuple)):ifnotvalue:return','eliflen(value)==1:returnself._quote(value[0],multiline=False)+','return', '.join([self._quote(val,multiline=False)forvalinvalue])ifnotisinstance(value,StringTypes):ifself.stringify:value=str(value)else:raiseTypeError,'Value "%s" is not a string.'%valuesquot="'%s'"dquot='"%s"'noquot="%s"wspace_plus=' \r\t\n\v\t\'"'tsquot='"""%s"""'tdquot="'''%s'''"ifnotvalue:return'""'if(notself.list_valuesand'\n'notinvalue)ornot(multilineand((("'"invalue)and('"'invalue))or('\n'invalue))):ifnotself.list_values:# we don't quote if ``list_values=False``quot=noquot# for normal values either single or double quotes will doelif'\n'invalue:# will only happen if multiline is off - e.g. '\n' in keyraiseConfigObjError,('Value "%s" cannot be safely quoted.'%value)elif((value[0]notinwspace_plus)and(value[-1]notinwspace_plus)and(','notinvalue)):quot=noquotelse:if("'"invalue)and('"'invalue):raiseConfigObjError,('Value "%s" cannot be safely quoted.'%value)elif'"'invalue:quot=squotelse:quot=dquotelse:# if value has '\n' or "'" *and* '"', it will need triple quotesif(value.find('"""')!=-1)and(value.find("'''")!=-1):raiseConfigObjError,('Value "%s" cannot be safely quoted.'%value)ifvalue.find('"""')==-1:quot=tdquotelse:quot=tsquotreturnquot%valuedef_handle_value(self,value):""" Given a value string, unquote, remove comment, handle lists. (including empty and single member lists) """# do we look for lists in values ?ifnotself.list_values:mat=self._nolistvalue.match(value)ifmatisNone:raiseSyntaxError# NOTE: we don't unquote herereturnmat.groups()#mat=self._valueexp.match(value)ifmatisNone:# the value is badly constructed, probably badly quoted,# or an invalid listraiseSyntaxError(list_values,single,empty_list,comment)=mat.groups()if(list_values=='')and(singleisNone):# change this if you want to accept empty valuesraiseSyntaxError# NOTE: note there is no error handling from here if the regex# is wrong: then incorrect values will slip throughifempty_listisnotNone:# the single comma - meaning an empty listreturn([],comment)ifsingleisnotNone:# handle empty valuesiflist_valuesandnotsingle:# FIXME: the '' is a workaround because our regex now matches# '' at the end of a list if it has a trailing commasingle=Noneelse:single=singleor'""'single=self._unquote(single)iflist_values=='':# not a list valuereturn(single,comment)the_list=self._listvalueexp.findall(list_values)the_list=[self._unquote(val)forvalinthe_list]ifsingleisnotNone:the_list+=[single]return(the_list,comment)def_multiline(self,value,infile,cur_index,maxline):"""Extract the value, where we are in a multiline situation."""quot=value[:3]newvalue=value[3:]single_line=self._triple_quote[quot][0]multi_line=self._triple_quote[quot][1]mat=single_line.match(value)ifmatisnotNone:retval=list(mat.groups())retval.append(cur_index)returnretvalelifnewvalue.find(quot)!=-1:# somehow the triple quote is missingraiseSyntaxError#whilecur_index<maxline:cur_index+=1newvalue+='\n'line=infile[cur_index]ifline.find(quot)==-1:newvalue+=lineelse:# end of multiline, process itbreakelse:# we've got to the end of the config, oops...raiseSyntaxErrormat=multi_line.match(line)ifmatisNone:# a badly formed lineraiseSyntaxError(value,comment)=mat.groups()return(newvalue+value,comment,cur_index)def_handle_configspec(self,configspec):"""Parse the configspec."""# FIXME: Should we check that the configspec was created with the # correct settings ? (i.e. ``list_values=False``)ifnotisinstance(configspec,ConfigObj):try:configspec=ConfigObj(configspec,raise_errors=True,file_error=True,list_values=False)exceptConfigObjError,e:# FIXME: Should these errors have a reference# to the already parsed ConfigObj ?raiseConfigspecError('Parsing configspec failed: %s'%e)exceptIOError,e:raiseIOError('Reading configspec failed: %s'%e)self._set_configspec_value(configspec,self)def_set_configspec_value(self,configspec,section):"""Used to recursively set configspec values."""if'__many__'inconfigspec.sections:section.configspec['__many__']=configspec['__many__']iflen(configspec.sections)>1:# FIXME: can we supply any useful information here ?raiseRepeatSectionErrorifhasattr(configspec,'initial_comment'):section._configspec_initial_comment=configspec.initial_commentsection._configspec_final_comment=configspec.final_commentsection._configspec_encoding=configspec.encodingsection._configspec_BOM=configspec.BOMsection._configspec_newlines=configspec.newlinessection._configspec_indent_type=configspec.indent_typeforentryinconfigspec.scalars:section._configspec_comments[entry]=configspec.comments[entry]section._configspec_inline_comments[entry]=(configspec.inline_comments[entry])section.configspec[entry]=configspec[entry]section._order.append(entry)forentryinconfigspec.sections:ifentry=='__many__':continuesection._cs_section_comments[entry]=configspec.comments[entry]section._cs_section_inline_comments[entry]=(configspec.inline_comments[entry])ifnotsection.has_key(entry):section[entry]={}self._set_configspec_value(configspec[entry],section[entry])def_handle_repeat(self,section,configspec):"""Dynamically assign configspec for repeated section."""try:section_keys=configspec.sectionsscalar_keys=configspec.scalarsexceptAttributeError:section_keys=[entryforentryinconfigspecifisinstance(configspec[entry],dict)]scalar_keys=[entryforentryinconfigspecifnotisinstance(configspec[entry],dict)]if'__many__'insection_keysandlen(section_keys)>1:# FIXME: can we supply any useful information here ?raiseRepeatSectionErrorscalars={}sections={}forentryinscalar_keys:val=configspec[entry]scalars[entry]=valforentryinsection_keys:val=configspec[entry]ifentry=='__many__':scalars[entry]=valcontinuesections[entry]=val#section.configspec=scalarsforentryinsections:ifnotsection.has_key(entry):section[entry]={}self._handle_repeat(section[entry],sections[entry])def_write_line(self,indent_string,entry,this_entry,comment):"""Write an individual line, for the write method"""# NOTE: the calls to self._quote here handles non-StringType values.ifnotself.unrepr:val=self._decode_element(self._quote(this_entry))else:val=repr(this_entry)return'%s%s%s%s%s'%(indent_string,self._decode_element(self._quote(entry,multiline=False)),self._a_to_u(' = '),val,self._decode_element(comment))def_write_marker(self,indent_string,depth,entry,comment):"""Write a section marker line"""return'%s%s%s%s%s'%(indent_string,self._a_to_u('['*depth),self._quote(self._decode_element(entry),multiline=False),self._a_to_u(']'*depth),self._decode_element(comment))def_handle_comment(self,comment):"""Deal with a comment."""ifnotcomment:return''start=self.indent_typeifnotcomment.startswith('#'):start+=self._a_to_u(' # ')return(start+comment)# Public methodsdefwrite(self,outfile=None,section=None):""" Write the current ConfigObj as a file tekNico: FIXME: use StringIO instead of real files >>> filename = a.filename >>> a.filename = 'test.ini' >>> a.write() >>> a.filename = filename >>> a == ConfigObj('test.ini', raise_errors=True) 1 """ifself.indent_typeisNone:# this can be true if initialised from a dictionaryself.indent_type=DEFAULT_INDENT_TYPE#out=[]cs=self._a_to_u('#')csp=self._a_to_u('# ')ifsectionisNone:int_val=self.interpolationself.interpolation=Falsesection=selfforlineinself.initial_comment:line=self._decode_element(line)stripped_line=line.strip()ifstripped_lineandnotstripped_line.startswith(cs):line=csp+lineout.append(line)#indent_string=self.indent_type*section.depthforentryin(section.scalars+section.sections):ifentryinsection.defaults:# don't write out default valuescontinueforcomment_lineinsection.comments[entry]:comment_line=self._decode_element(comment_line.lstrip())ifcomment_lineandnotcomment_line.startswith(cs):comment_line=csp+comment_lineout.append(indent_string+comment_line)this_entry=section[entry]comment=self._handle_comment(section.inline_comments[entry])#ifisinstance(this_entry,dict):# a sectionout.append(self._write_marker(indent_string,this_entry.depth,entry,comment))out.extend(self.write(section=this_entry))else:out.append(self._write_line(indent_string,entry,this_entry,comment))#ifsectionisself:forlineinself.final_comment:line=self._decode_element(line)stripped_line=line.strip()ifstripped_lineandnotstripped_line.startswith(cs):line=csp+lineout.append(line)self.interpolation=int_val#ifsectionisnotself:returnout#if(self.filenameisNone)and(outfileisNone):# output a list of lines# might need to encode# NOTE: This will *screw* UTF16, each line will start with the BOMifself.encoding:out=[l.encode(self.encoding)forlinout]if(self.BOMand((self.encodingisNone)or(BOM_LIST.get(self.encoding.lower())=='utf_8'))):# Add the UTF8 BOMifnotout:out.append('')out[0]=BOM_UTF8+out[0]returnout## Turn the list to a string, joined with correct newlinesoutput=(self._a_to_u(self.newlinesoros.linesep)).join(out)ifself.encoding:output=output.encode(self.encoding)if(self.BOMand((self.encodingisNone)or(BOM_LIST.get(self.encoding.lower())=='utf_8'))):# Add the UTF8 BOMoutput=BOM_UTF8+outputifoutfileisnotNone:outfile.write(output)else:h=open(self.filename,'wb')h.write(output)h.close()defvalidate(self,validator,preserve_errors=False,copy=False,section=None):""" Test the ConfigObj against a configspec. It uses the ``validator`` object from *validate.py*. To run ``validate`` on the current ConfigObj, call: :: test = config.validate(validator) (Normally having previously passed in the configspec when the ConfigObj was created - you can dynamically assign a dictionary of checks to the ``configspec`` attribute of a section though). It returns ``True`` if everything passes, or a dictionary of pass/fails (True/False). If every member of a subsection passes, it will just have the value ``True``. (It also returns ``False`` if all members fail). In addition, it converts the values from strings to their native types if their checks pass (and ``stringify`` is set). If ``preserve_errors`` is ``True`` (``False`` is default) then instead of a marking a fail with a ``False``, it will preserve the actual exception object. This can contain info about the reason for failure. For example the ``VdtValueTooSmallError`` indeicates that the value supplied was too small. If a value (or section) is missing it will still be marked as ``False``. You must have the validate module to use ``preserve_errors=True``. You can then use the ``flatten_errors`` function to turn your nested results dictionary into a flattened list of failures - useful for displaying meaningful error messages. """ifsectionisNone:ifself.configspecisNone:raiseValueError,'No configspec supplied.'ifpreserve_errors:ifVdtMissingValueisNone:raiseImportError('Missing validate module.')section=self#spec_section=section.configspecifcopyandhasattr(section,'_configspec_initial_comment'):section.initial_comment=section._configspec_initial_commentsection.final_comment=section._configspec_final_commentsection.encoding=section._configspec_encodingsection.BOM=section._configspec_BOMsection.newlines=section._configspec_newlinessection.indent_type=section._configspec_indent_typeif'__many__'insection.configspec:many=spec_section['__many__']# dynamically assign the configspecs# for the sections belowforentryinsection.sections:self._handle_repeat(section[entry],many)#out={}ret_true=Trueret_false=Trueorder=[kforkinsection._orderifkinspec_section]order+=[kforkinspec_sectionifknotinorder]forentryinorder:ifentry=='__many__':continueif(notentryinsection.scalars)or(entryinsection.defaults):# missing entries# or entries from defaultsmissing=Trueval=Noneifcopyandnotentryinsection.scalars:# copy commentssection.comments[entry]=(section._configspec_comments.get(entry,[]))section.inline_comments[entry]=(section._configspec_inline_comments.get(entry,''))#else:missing=Falseval=section[entry]try:check=validator.check(spec_section[entry],val,missing=missing)exceptvalidator.baseErrorClass,e:ifnotpreserve_errorsorisinstance(e,VdtMissingValue):out[entry]=Falseelse:# preserve the errorout[entry]=eret_false=Falseret_true=Falseelse:ret_false=Falseout[entry]=Trueifself.stringifyormissing:# if we are doing type conversion# or the value is a supplied defaultifnotself.stringify:ifisinstance(check,(list,tuple)):# preserve listscheck=[self._str(item)foritemincheck]elifmissingandcheckisNone:# convert the None from a default to a ''check=''else:check=self._str(check)if(check!=val)ormissing:section[entry]=checkifnotcopyandmissingandentrynotinsection.defaults:section.defaults.append(entry)## Missing sections will have been created as empty ones when the# configspec was read.forentryinsection.sections:# FIXME: this means DEFAULT is not copied in copy modeifsectionisselfandentry=='DEFAULT':continueifcopy:section.comments[entry]=section._cs_section_comments[entry]section.inline_comments[entry]=(section._cs_section_inline_comments[entry])check=self.validate(validator,preserve_errors=preserve_errors,copy=copy,section=section[entry])out[entry]=checkifcheck==False:ret_true=Falseelifcheck==True:ret_false=Falseelse:ret_true=Falseret_false=False#ifret_true:returnTrueelifret_false:returnFalseelse:returnoutclassSimpleVal(object):""" A simple validator. Can be used to check that all members expected are present. To use it, provide a configspec with all your members in (the value given will be ignored). Pass an instance of ``SimpleVal`` to the ``validate`` method of your ``ConfigObj``. ``validate`` will return ``True`` if all members are present, or a dictionary with True/False meaning present/missing. (Whole missing sections will be replaced with ``False``) """def__init__(self):self.baseErrorClass=ConfigObjErrordefcheck(self,check,member,missing=False):"""A dummy check method, always returns the value unchanged."""ifmissing:raiseself.baseErrorClassreturnmember# Check / processing functions for optionsdefflatten_errors(cfg,res,levels=None,results=None):""" An example function that will turn a nested dictionary of results (as returned by ``ConfigObj.validate``) into a flat list. ``cfg`` is the ConfigObj instance being checked, ``res`` is the results dictionary returned by ``validate``. (This is a recursive function, so you shouldn't use the ``levels`` or ``results`` arguments - they are used by the function. Returns a list of keys that failed. Each member of the list is a tuple : :: ([list of sections...], key, result) If ``validate`` was called with ``preserve_errors=False`` (the default) then ``result`` will always be ``False``. *list of sections* is a flattened list of sections that the key was found in. If the section was missing then key will be ``None``. If the value (or section) was missing then ``result`` will be ``False``. If ``validate`` was called with ``preserve_errors=True`` and a value was present, but failed the check, then ``result`` will be the exception object returned. You can use this as a string that describes the failure. For example *The value "3" is of the wrong type*. >>> import validate >>> vtor = validate.Validator() >>> my_ini = ''' ... option1 = True ... [section1] ... option1 = True ... [section2] ... another_option = Probably ... [section3] ... another_option = True ... [[section3b]] ... value = 3 ... value2 = a ... value3 = 11 ... ''' >>> my_cfg = ''' ... option1 = boolean() ... option2 = boolean() ... option3 = boolean(default=Bad_value) ... [section1] ... option1 = boolean() ... option2 = boolean() ... option3 = boolean(default=Bad_value) ... [section2] ... another_option = boolean() ... [section3] ... another_option = boolean() ... [[section3b]] ... value = integer ... value2 = integer ... value3 = integer(0, 10) ... [[[section3b-sub]]] ... value = string ... [section4] ... another_option = boolean() ... ''' >>> cs = my_cfg.split('\\n') >>> ini = my_ini.split('\\n') >>> cfg = ConfigObj(ini, configspec=cs) >>> res = cfg.validate(vtor, preserve_errors=True) >>> errors = [] >>> for entry in flatten_errors(cfg, res): ... section_list, key, error = entry ... section_list.insert(0, '[root]') ... if key is not None: ... section_list.append(key) ... else: ... section_list.append('[missing]') ... section_string = ', '.join(section_list) ... errors.append((section_string, ' = ', error)) >>> errors.sort() >>> for entry in errors: ... print entry[0], entry[1], (entry[2] or 0) [root], option2 = 0 [root], option3 = the value "Bad_value" is of the wrong type. [root], section1, option2 = 0 [root], section1, option3 = the value "Bad_value" is of the wrong type. [root], section2, another_option = the value "Probably" is of the wrong type. [root], section3, section3b, section3b-sub, [missing] = 0 [root], section3, section3b, value2 = the value "a" is of the wrong type. [root], section3, section3b, value3 = the value "11" is too big. [root], section4, [missing] = 0 """iflevelsisNone:# first time calledlevels=[]results=[]ifresisTrue:returnresultsifresisFalse:results.append((levels[:],None,False))iflevels:levels.pop()returnresultsfor(key,val)inres.items():ifval==True:continueifisinstance(cfg.get(key),dict):# Go down one levellevels.append(key)flatten_errors(cfg[key],val,levels,results)continueresults.append((levels[:],key,val))## Go up one leveliflevels:levels.pop()#returnresults"""*A programming language is a medium of expression.* - Paul Graham"""