michael@0: # configobj.py michael@0: # A config file reader/writer that supports nested sections in config files. michael@0: # Copyright (C) 2005-2010 Michael Foord, Nicola Larosa michael@0: # E-mail: fuzzyman AT voidspace DOT org DOT uk michael@0: # nico AT tekNico DOT net michael@0: michael@0: # ConfigObj 4 michael@0: # http://www.voidspace.org.uk/python/configobj.html michael@0: michael@0: # Released subject to the BSD License michael@0: # Please see http://www.voidspace.org.uk/python/license.shtml michael@0: michael@0: # Scripts maintained at http://www.voidspace.org.uk/python/index.shtml michael@0: # For information about bugfixes, updates and support, please join the michael@0: # ConfigObj mailing list: michael@0: # http://lists.sourceforge.net/lists/listinfo/configobj-develop michael@0: # Comments, suggestions and bug reports welcome. michael@0: michael@0: from __future__ import generators michael@0: michael@0: import os michael@0: import re michael@0: import sys michael@0: michael@0: from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE michael@0: michael@0: michael@0: # imported lazily to avoid startup performance hit if it isn't used michael@0: compiler = None michael@0: michael@0: # A dictionary mapping BOM to michael@0: # the encoding to decode with, and what to set the michael@0: # encoding attribute to. michael@0: BOMS = { michael@0: BOM_UTF8: ('utf_8', None), michael@0: BOM_UTF16_BE: ('utf16_be', 'utf_16'), michael@0: BOM_UTF16_LE: ('utf16_le', 'utf_16'), michael@0: BOM_UTF16: ('utf_16', 'utf_16'), michael@0: } michael@0: # All legal variants of the BOM codecs. michael@0: # TODO: the list of aliases is not meant to be exhaustive, is there a michael@0: # better way ? michael@0: BOM_LIST = { michael@0: 'utf_16': 'utf_16', michael@0: 'u16': 'utf_16', michael@0: 'utf16': 'utf_16', michael@0: 'utf-16': 'utf_16', michael@0: 'utf16_be': 'utf16_be', michael@0: 'utf_16_be': 'utf16_be', michael@0: 'utf-16be': 'utf16_be', michael@0: 'utf16_le': 'utf16_le', michael@0: 'utf_16_le': 'utf16_le', michael@0: 'utf-16le': 'utf16_le', michael@0: 'utf_8': 'utf_8', michael@0: 'u8': 'utf_8', michael@0: 'utf': 'utf_8', michael@0: 'utf8': 'utf_8', michael@0: 'utf-8': 'utf_8', michael@0: } michael@0: michael@0: # Map of encodings to the BOM to write. michael@0: BOM_SET = { michael@0: 'utf_8': BOM_UTF8, michael@0: 'utf_16': BOM_UTF16, michael@0: 'utf16_be': BOM_UTF16_BE, michael@0: 'utf16_le': BOM_UTF16_LE, michael@0: None: BOM_UTF8 michael@0: } michael@0: michael@0: michael@0: def match_utf8(encoding): michael@0: return BOM_LIST.get(encoding.lower()) == 'utf_8' michael@0: michael@0: michael@0: # Quote strings used for writing values michael@0: squot = "'%s'" michael@0: dquot = '"%s"' michael@0: noquot = "%s" michael@0: wspace_plus = ' \r\n\v\t\'"' michael@0: tsquot = '"""%s"""' michael@0: tdquot = "'''%s'''" michael@0: michael@0: # Sentinel for use in getattr calls to replace hasattr michael@0: MISSING = object() michael@0: michael@0: __version__ = '4.7.2' michael@0: michael@0: try: michael@0: any michael@0: except NameError: michael@0: def any(iterable): michael@0: for entry in iterable: michael@0: if entry: michael@0: return True michael@0: return False michael@0: michael@0: michael@0: __all__ = ( michael@0: '__version__', michael@0: 'DEFAULT_INDENT_TYPE', michael@0: 'DEFAULT_INTERPOLATION', michael@0: 'ConfigObjError', michael@0: 'NestingError', michael@0: 'ParseError', michael@0: 'DuplicateError', michael@0: 'ConfigspecError', michael@0: 'ConfigObj', michael@0: 'SimpleVal', michael@0: 'InterpolationError', michael@0: 'InterpolationLoopError', michael@0: 'MissingInterpolationOption', michael@0: 'RepeatSectionError', michael@0: 'ReloadError', michael@0: 'UnreprError', michael@0: 'UnknownType', michael@0: 'flatten_errors', michael@0: 'get_extra_values' michael@0: ) michael@0: michael@0: DEFAULT_INTERPOLATION = 'configparser' michael@0: DEFAULT_INDENT_TYPE = ' ' michael@0: MAX_INTERPOL_DEPTH = 10 michael@0: michael@0: OPTION_DEFAULTS = { michael@0: 'interpolation': True, michael@0: 'raise_errors': False, michael@0: 'list_values': True, michael@0: 'create_empty': False, michael@0: 'file_error': False, michael@0: 'configspec': None, michael@0: 'stringify': True, michael@0: # option may be set to one of ('', ' ', '\t') michael@0: 'indent_type': None, michael@0: 'encoding': None, michael@0: 'default_encoding': None, michael@0: 'unrepr': False, michael@0: 'write_empty_values': False, michael@0: } michael@0: michael@0: michael@0: michael@0: def getObj(s): michael@0: global compiler michael@0: if compiler is None: michael@0: import compiler michael@0: s = "a=" + s michael@0: p = compiler.parse(s) michael@0: return p.getChildren()[1].getChildren()[0].getChildren()[1] michael@0: michael@0: michael@0: class UnknownType(Exception): michael@0: pass michael@0: michael@0: michael@0: class Builder(object): michael@0: michael@0: def build(self, o): michael@0: m = getattr(self, 'build_' + o.__class__.__name__, None) michael@0: if m is None: michael@0: raise UnknownType(o.__class__.__name__) michael@0: return m(o) michael@0: michael@0: def build_List(self, o): michael@0: return map(self.build, o.getChildren()) michael@0: michael@0: def build_Const(self, o): michael@0: return o.value michael@0: michael@0: def build_Dict(self, o): michael@0: d = {} michael@0: i = iter(map(self.build, o.getChildren())) michael@0: for el in i: michael@0: d[el] = i.next() michael@0: return d michael@0: michael@0: def build_Tuple(self, o): michael@0: return tuple(self.build_List(o)) michael@0: michael@0: def build_Name(self, o): michael@0: if o.name == 'None': michael@0: return None michael@0: if o.name == 'True': michael@0: return True michael@0: if o.name == 'False': michael@0: return False michael@0: michael@0: # An undefined Name michael@0: raise UnknownType('Undefined Name') michael@0: michael@0: def build_Add(self, o): michael@0: real, imag = map(self.build_Const, o.getChildren()) michael@0: try: michael@0: real = float(real) michael@0: except TypeError: michael@0: raise UnknownType('Add') michael@0: if not isinstance(imag, complex) or imag.real != 0.0: michael@0: raise UnknownType('Add') michael@0: return real+imag michael@0: michael@0: def build_Getattr(self, o): michael@0: parent = self.build(o.expr) michael@0: return getattr(parent, o.attrname) michael@0: michael@0: def build_UnarySub(self, o): michael@0: return -self.build_Const(o.getChildren()[0]) michael@0: michael@0: def build_UnaryAdd(self, o): michael@0: return self.build_Const(o.getChildren()[0]) michael@0: michael@0: michael@0: _builder = Builder() michael@0: michael@0: michael@0: def unrepr(s): michael@0: if not s: michael@0: return s michael@0: return _builder.build(getObj(s)) michael@0: michael@0: michael@0: michael@0: class ConfigObjError(SyntaxError): michael@0: """ michael@0: This is the base class for all errors that ConfigObj raises. michael@0: It is a subclass of SyntaxError. michael@0: """ michael@0: def __init__(self, message='', line_number=None, line=''): michael@0: self.line = line michael@0: self.line_number = line_number michael@0: SyntaxError.__init__(self, message) michael@0: michael@0: michael@0: class NestingError(ConfigObjError): michael@0: """ michael@0: This error indicates a level of nesting that doesn't match. michael@0: """ michael@0: michael@0: michael@0: class ParseError(ConfigObjError): michael@0: """ michael@0: This error indicates that a line is badly written. michael@0: It is neither a valid ``key = value`` line, michael@0: nor a valid section marker line. michael@0: """ michael@0: michael@0: michael@0: class ReloadError(IOError): michael@0: """ michael@0: A 'reload' operation failed. michael@0: This exception is a subclass of ``IOError``. michael@0: """ michael@0: def __init__(self): michael@0: IOError.__init__(self, 'reload failed, filename is not set.') michael@0: michael@0: michael@0: class DuplicateError(ConfigObjError): michael@0: """ michael@0: The keyword or section specified already exists. michael@0: """ michael@0: michael@0: michael@0: class ConfigspecError(ConfigObjError): michael@0: """ michael@0: An error occured whilst parsing a configspec. michael@0: """ michael@0: michael@0: michael@0: class InterpolationError(ConfigObjError): michael@0: """Base class for the two interpolation errors.""" michael@0: michael@0: michael@0: class InterpolationLoopError(InterpolationError): michael@0: """Maximum interpolation depth exceeded in string interpolation.""" michael@0: michael@0: def __init__(self, option): michael@0: InterpolationError.__init__( michael@0: self, michael@0: 'interpolation loop detected in value "%s".' % option) michael@0: michael@0: michael@0: class RepeatSectionError(ConfigObjError): michael@0: """ michael@0: This error indicates additional sections in a section with a michael@0: ``__many__`` (repeated) section. michael@0: """ michael@0: michael@0: michael@0: class MissingInterpolationOption(InterpolationError): michael@0: """A value specified for interpolation was missing.""" michael@0: def __init__(self, option): michael@0: msg = 'missing option "%s" in interpolation.' % option michael@0: InterpolationError.__init__(self, msg) michael@0: michael@0: michael@0: class UnreprError(ConfigObjError): michael@0: """An error parsing in unrepr mode.""" michael@0: michael@0: michael@0: michael@0: class InterpolationEngine(object): michael@0: """ michael@0: A helper class to help perform string interpolation. michael@0: michael@0: This class is an abstract base class; its descendants perform michael@0: the actual work. michael@0: """ michael@0: michael@0: # compiled regexp to use in self.interpolate() michael@0: _KEYCRE = re.compile(r"%\(([^)]*)\)s") michael@0: _cookie = '%' michael@0: michael@0: def __init__(self, section): michael@0: # the Section instance that "owns" this engine michael@0: self.section = section michael@0: michael@0: michael@0: def interpolate(self, key, value): michael@0: # short-cut michael@0: if not self._cookie in value: michael@0: return value michael@0: michael@0: def recursive_interpolate(key, value, section, backtrail): michael@0: """The function that does the actual work. michael@0: michael@0: ``value``: the string we're trying to interpolate. michael@0: ``section``: the section in which that string was found michael@0: ``backtrail``: a dict to keep track of where we've been, michael@0: to detect and prevent infinite recursion loops michael@0: michael@0: This is similar to a depth-first-search algorithm. michael@0: """ michael@0: # Have we been here already? michael@0: if (key, section.name) in backtrail: michael@0: # Yes - infinite loop detected michael@0: raise InterpolationLoopError(key) michael@0: # Place a marker on our backtrail so we won't come back here again michael@0: backtrail[(key, section.name)] = 1 michael@0: michael@0: # Now start the actual work michael@0: match = self._KEYCRE.search(value) michael@0: while match: michael@0: # The actual parsing of the match is implementation-dependent, michael@0: # so delegate to our helper function michael@0: k, v, s = self._parse_match(match) michael@0: if k is None: michael@0: # That's the signal that no further interpolation is needed michael@0: replacement = v michael@0: else: michael@0: # Further interpolation may be needed to obtain final value michael@0: replacement = recursive_interpolate(k, v, s, backtrail) michael@0: # Replace the matched string with its final value michael@0: start, end = match.span() michael@0: value = ''.join((value[:start], replacement, value[end:])) michael@0: new_search_start = start + len(replacement) michael@0: # Pick up the next interpolation key, if any, for next time michael@0: # through the while loop michael@0: match = self._KEYCRE.search(value, new_search_start) michael@0: michael@0: # Now safe to come back here again; remove marker from backtrail michael@0: del backtrail[(key, section.name)] michael@0: michael@0: return value michael@0: michael@0: # Back in interpolate(), all we have to do is kick off the recursive michael@0: # function with appropriate starting values michael@0: value = recursive_interpolate(key, value, self.section, {}) michael@0: return value michael@0: michael@0: michael@0: def _fetch(self, key): michael@0: """Helper function to fetch values from owning section. michael@0: michael@0: Returns a 2-tuple: the value, and the section where it was found. michael@0: """ michael@0: # switch off interpolation before we try and fetch anything ! michael@0: save_interp = self.section.main.interpolation michael@0: self.section.main.interpolation = False michael@0: michael@0: # Start at section that "owns" this InterpolationEngine michael@0: current_section = self.section michael@0: while True: michael@0: # try the current section first michael@0: val = current_section.get(key) michael@0: if val is not None and not isinstance(val, Section): michael@0: break michael@0: # try "DEFAULT" next michael@0: val = current_section.get('DEFAULT', {}).get(key) michael@0: if val is not None and not isinstance(val, Section): michael@0: break michael@0: # move up to parent and try again michael@0: # top-level's parent is itself michael@0: if current_section.parent is current_section: michael@0: # reached top level, time to give up michael@0: break michael@0: current_section = current_section.parent michael@0: michael@0: # restore interpolation to previous value before returning michael@0: self.section.main.interpolation = save_interp michael@0: if val is None: michael@0: raise MissingInterpolationOption(key) michael@0: return val, current_section michael@0: michael@0: michael@0: def _parse_match(self, match): michael@0: """Implementation-dependent helper function. michael@0: michael@0: Will be passed a match object corresponding to the interpolation michael@0: key we just found (e.g., "%(foo)s" or "$foo"). Should look up that michael@0: key in the appropriate config file section (using the ``_fetch()`` michael@0: helper function) and return a 3-tuple: (key, value, section) michael@0: michael@0: ``key`` is the name of the key we're looking for michael@0: ``value`` is the value found for that key michael@0: ``section`` is a reference to the section where it was found michael@0: michael@0: ``key`` and ``section`` should be None if no further michael@0: interpolation should be performed on the resulting value michael@0: (e.g., if we interpolated "$$" and returned "$"). michael@0: """ michael@0: raise NotImplementedError() michael@0: michael@0: michael@0: michael@0: class ConfigParserInterpolation(InterpolationEngine): michael@0: """Behaves like ConfigParser.""" michael@0: _cookie = '%' michael@0: _KEYCRE = re.compile(r"%\(([^)]*)\)s") michael@0: michael@0: def _parse_match(self, match): michael@0: key = match.group(1) michael@0: value, section = self._fetch(key) michael@0: return key, value, section michael@0: michael@0: michael@0: michael@0: class TemplateInterpolation(InterpolationEngine): michael@0: """Behaves like string.Template.""" michael@0: _cookie = '$' michael@0: _delimiter = '$' michael@0: _KEYCRE = re.compile(r""" michael@0: \$(?: michael@0: (?P\$) | # Two $ signs michael@0: (?P[_a-z][_a-z0-9]*) | # $name format michael@0: {(?P[^}]*)} # ${name} format michael@0: ) michael@0: """, re.IGNORECASE | re.VERBOSE) michael@0: michael@0: def _parse_match(self, match): michael@0: # Valid name (in or out of braces): fetch value from section michael@0: key = match.group('named') or match.group('braced') michael@0: if key is not None: michael@0: value, section = self._fetch(key) michael@0: return key, value, section michael@0: # Escaped delimiter (e.g., $$): return single delimiter michael@0: if match.group('escaped') is not None: michael@0: # Return None for key and section to indicate it's time to stop michael@0: return None, self._delimiter, None michael@0: # Anything else: ignore completely, just return it unchanged michael@0: return None, match.group(), None michael@0: michael@0: michael@0: interpolation_engines = { michael@0: 'configparser': ConfigParserInterpolation, michael@0: 'template': TemplateInterpolation, michael@0: } michael@0: michael@0: michael@0: def __newobj__(cls, *args): michael@0: # Hack for pickle michael@0: return cls.__new__(cls, *args) michael@0: michael@0: class Section(dict): michael@0: """ michael@0: A dictionary-like object that represents a section in a config file. michael@0: michael@0: It does string interpolation if the 'interpolation' attribute michael@0: of the 'main' object is set to True. michael@0: michael@0: Interpolation is tried first from this object, then from the 'DEFAULT' michael@0: section of this object, next from the parent and its 'DEFAULT' section, michael@0: and so on until the main object is reached. michael@0: michael@0: A Section will behave like an ordered dictionary - following the michael@0: order of the ``scalars`` and ``sections`` attributes. michael@0: You can use this to change the order of members. michael@0: michael@0: Iteration follows the order: scalars, then sections. michael@0: """ michael@0: michael@0: michael@0: def __setstate__(self, state): michael@0: dict.update(self, state[0]) michael@0: self.__dict__.update(state[1]) michael@0: michael@0: def __reduce__(self): michael@0: state = (dict(self), self.__dict__) michael@0: return (__newobj__, (self.__class__,), state) michael@0: michael@0: michael@0: def __init__(self, parent, depth, main, indict=None, name=None): michael@0: """ michael@0: * parent is the section above michael@0: * depth is the depth level of this section michael@0: * main is the main ConfigObj michael@0: * indict is a dictionary to initialise the section with michael@0: """ michael@0: if indict is None: michael@0: indict = {} michael@0: dict.__init__(self) michael@0: # used for nesting level *and* interpolation michael@0: self.parent = parent michael@0: # used for the interpolation attribute michael@0: self.main = main michael@0: # level of nesting depth of this Section michael@0: self.depth = depth michael@0: # purely for information michael@0: self.name = name michael@0: # michael@0: self._initialise() michael@0: # we do this explicitly so that __setitem__ is used properly michael@0: # (rather than just passing to ``dict.__init__``) michael@0: for entry, value in indict.iteritems(): michael@0: self[entry] = value michael@0: michael@0: michael@0: def _initialise(self): michael@0: # the sequence of scalar values in this Section michael@0: self.scalars = [] michael@0: # the sequence of sections in this Section michael@0: self.sections = [] michael@0: # for comments :-) michael@0: self.comments = {} michael@0: self.inline_comments = {} michael@0: # the configspec michael@0: self.configspec = None michael@0: # for defaults michael@0: self.defaults = [] michael@0: self.default_values = {} michael@0: self.extra_values = [] michael@0: self._created = False michael@0: michael@0: michael@0: def _interpolate(self, key, value): michael@0: try: michael@0: # do we already have an interpolation engine? michael@0: engine = self._interpolation_engine michael@0: except AttributeError: michael@0: # not yet: first time running _interpolate(), so pick the engine michael@0: name = self.main.interpolation michael@0: if name == True: # note that "if name:" would be incorrect here michael@0: # backwards-compatibility: interpolation=True means use default michael@0: name = DEFAULT_INTERPOLATION michael@0: name = name.lower() # so that "Template", "template", etc. all work michael@0: class_ = interpolation_engines.get(name, None) michael@0: if class_ is None: michael@0: # invalid value for self.main.interpolation michael@0: self.main.interpolation = False michael@0: return value michael@0: else: michael@0: # save reference to engine so we don't have to do this again michael@0: engine = self._interpolation_engine = class_(self) michael@0: # let the engine do the actual work michael@0: return engine.interpolate(key, value) michael@0: michael@0: michael@0: def __getitem__(self, key): michael@0: """Fetch the item and do string interpolation.""" michael@0: val = dict.__getitem__(self, key) michael@0: if self.main.interpolation: michael@0: if isinstance(val, basestring): michael@0: return self._interpolate(key, val) michael@0: if isinstance(val, list): michael@0: def _check(entry): michael@0: if isinstance(entry, basestring): michael@0: return self._interpolate(key, entry) michael@0: return entry michael@0: new = [_check(entry) for entry in val] michael@0: if new != val: michael@0: return new michael@0: return val michael@0: michael@0: michael@0: def __setitem__(self, key, value, unrepr=False): michael@0: """ michael@0: Correctly set a value. michael@0: michael@0: Making dictionary values Section instances. michael@0: (We have to special case 'Section' instances - which are also dicts) michael@0: michael@0: Keys must be strings. michael@0: Values need only be strings (or lists of strings) if michael@0: ``main.stringify`` is set. michael@0: michael@0: ``unrepr`` must be set when setting a value to a dictionary, without michael@0: creating a new sub-section. michael@0: """ michael@0: if not isinstance(key, basestring): michael@0: raise ValueError('The key "%s" is not a string.' % key) michael@0: michael@0: # add the comment michael@0: if key not in self.comments: michael@0: self.comments[key] = [] michael@0: self.inline_comments[key] = '' michael@0: # remove the entry from defaults michael@0: if key in self.defaults: michael@0: self.defaults.remove(key) michael@0: # michael@0: if isinstance(value, Section): michael@0: if key not in self: michael@0: self.sections.append(key) michael@0: dict.__setitem__(self, key, value) michael@0: elif isinstance(value, dict) and not unrepr: michael@0: # First create the new depth level, michael@0: # then create the section michael@0: if key not in self: michael@0: self.sections.append(key) michael@0: new_depth = self.depth + 1 michael@0: dict.__setitem__( michael@0: self, michael@0: key, michael@0: Section( michael@0: self, michael@0: new_depth, michael@0: self.main, michael@0: indict=value, michael@0: name=key)) michael@0: else: michael@0: if key not in self: michael@0: self.scalars.append(key) michael@0: if not self.main.stringify: michael@0: if isinstance(value, basestring): michael@0: pass michael@0: elif isinstance(value, (list, tuple)): michael@0: for entry in value: michael@0: if not isinstance(entry, basestring): michael@0: raise TypeError('Value is not a string "%s".' % entry) michael@0: else: michael@0: raise TypeError('Value is not a string "%s".' % value) michael@0: dict.__setitem__(self, key, value) michael@0: michael@0: michael@0: def __delitem__(self, key): michael@0: """Remove items from the sequence when deleting.""" michael@0: dict. __delitem__(self, key) michael@0: if key in self.scalars: michael@0: self.scalars.remove(key) michael@0: else: michael@0: self.sections.remove(key) michael@0: del self.comments[key] michael@0: del self.inline_comments[key] michael@0: michael@0: michael@0: def get(self, key, default=None): michael@0: """A version of ``get`` that doesn't bypass string interpolation.""" michael@0: try: michael@0: return self[key] michael@0: except KeyError: michael@0: return default michael@0: michael@0: michael@0: def update(self, indict): michael@0: """ michael@0: A version of update that uses our ``__setitem__``. michael@0: """ michael@0: for entry in indict: michael@0: self[entry] = indict[entry] michael@0: michael@0: michael@0: def pop(self, key, default=MISSING): michael@0: """ michael@0: 'D.pop(k[,d]) -> v, remove specified key and return the corresponding value. michael@0: If key is not found, d is returned if given, otherwise KeyError is raised' michael@0: """ michael@0: try: michael@0: val = self[key] michael@0: except KeyError: michael@0: if default is MISSING: michael@0: raise michael@0: val = default michael@0: else: michael@0: del self[key] michael@0: return val michael@0: michael@0: michael@0: def popitem(self): michael@0: """Pops the first (key,val)""" michael@0: sequence = (self.scalars + self.sections) michael@0: if not sequence: michael@0: raise KeyError(": 'popitem(): dictionary is empty'") michael@0: key = sequence[0] michael@0: val = self[key] michael@0: del self[key] michael@0: return key, val michael@0: michael@0: michael@0: def clear(self): michael@0: """ michael@0: A version of clear that also affects scalars/sections michael@0: Also clears comments and configspec. michael@0: michael@0: Leaves other attributes alone : michael@0: depth/main/parent are not affected michael@0: """ michael@0: dict.clear(self) michael@0: self.scalars = [] michael@0: self.sections = [] michael@0: self.comments = {} michael@0: self.inline_comments = {} michael@0: self.configspec = None michael@0: self.defaults = [] michael@0: self.extra_values = [] michael@0: michael@0: michael@0: def setdefault(self, key, default=None): michael@0: """A version of setdefault that sets sequence if appropriate.""" michael@0: try: michael@0: return self[key] michael@0: except KeyError: michael@0: self[key] = default michael@0: return self[key] michael@0: michael@0: michael@0: def items(self): michael@0: """D.items() -> list of D's (key, value) pairs, as 2-tuples""" michael@0: return zip((self.scalars + self.sections), self.values()) michael@0: michael@0: michael@0: def keys(self): michael@0: """D.keys() -> list of D's keys""" michael@0: return (self.scalars + self.sections) michael@0: michael@0: michael@0: def values(self): michael@0: """D.values() -> list of D's values""" michael@0: return [self[key] for key in (self.scalars + self.sections)] michael@0: michael@0: michael@0: def iteritems(self): michael@0: """D.iteritems() -> an iterator over the (key, value) items of D""" michael@0: return iter(self.items()) michael@0: michael@0: michael@0: def iterkeys(self): michael@0: """D.iterkeys() -> an iterator over the keys of D""" michael@0: return iter((self.scalars + self.sections)) michael@0: michael@0: __iter__ = iterkeys michael@0: michael@0: michael@0: def itervalues(self): michael@0: """D.itervalues() -> an iterator over the values of D""" michael@0: return iter(self.values()) michael@0: michael@0: michael@0: def __repr__(self): michael@0: """x.__repr__() <==> repr(x)""" michael@0: def _getval(key): michael@0: try: michael@0: return self[key] michael@0: except MissingInterpolationOption: michael@0: return dict.__getitem__(self, key) michael@0: return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) michael@0: for key in (self.scalars + self.sections)]) michael@0: michael@0: __str__ = __repr__ michael@0: __str__.__doc__ = "x.__str__() <==> str(x)" michael@0: michael@0: michael@0: # Extra methods - not in a normal dictionary michael@0: michael@0: def dict(self): michael@0: """ michael@0: Return a deepcopy of self as a dictionary. michael@0: michael@0: All members that are ``Section`` instances are recursively turned to michael@0: ordinary dictionaries - by calling their ``dict`` method. michael@0: michael@0: >>> n = a.dict() michael@0: >>> n == a michael@0: 1 michael@0: >>> n is a michael@0: 0 michael@0: """ michael@0: newdict = {} michael@0: for entry in self: michael@0: this_entry = self[entry] michael@0: if isinstance(this_entry, Section): michael@0: this_entry = this_entry.dict() michael@0: elif isinstance(this_entry, list): michael@0: # create a copy rather than a reference michael@0: this_entry = list(this_entry) michael@0: elif isinstance(this_entry, tuple): michael@0: # create a copy rather than a reference michael@0: this_entry = tuple(this_entry) michael@0: newdict[entry] = this_entry michael@0: return newdict michael@0: michael@0: michael@0: def merge(self, indict): michael@0: """ michael@0: A recursive update - useful for merging config files. michael@0: michael@0: >>> a = '''[section1] michael@0: ... option1 = True michael@0: ... [[subsection]] michael@0: ... more_options = False michael@0: ... # end of file'''.splitlines() michael@0: >>> b = '''# File is user.ini michael@0: ... [section1] michael@0: ... option1 = False michael@0: ... # end of file'''.splitlines() michael@0: >>> c1 = ConfigObj(b) michael@0: >>> c2 = ConfigObj(a) michael@0: >>> c2.merge(c1) michael@0: >>> c2 michael@0: ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}}) michael@0: """ michael@0: for key, val in indict.items(): michael@0: if (key in self and isinstance(self[key], dict) and michael@0: isinstance(val, dict)): michael@0: self[key].merge(val) michael@0: else: michael@0: self[key] = val michael@0: michael@0: michael@0: def rename(self, oldkey, newkey): michael@0: """ michael@0: Change a keyname to another, without changing position in sequence. michael@0: michael@0: Implemented so that transformations can be made on keys, michael@0: as well as on values. (used by encode and decode) michael@0: michael@0: Also renames comments. michael@0: """ michael@0: if oldkey in self.scalars: michael@0: the_list = self.scalars michael@0: elif oldkey in self.sections: michael@0: the_list = self.sections michael@0: else: michael@0: raise KeyError('Key "%s" not found.' % oldkey) michael@0: pos = the_list.index(oldkey) michael@0: # michael@0: val = self[oldkey] michael@0: dict.__delitem__(self, oldkey) michael@0: dict.__setitem__(self, newkey, val) michael@0: the_list.remove(oldkey) michael@0: the_list.insert(pos, newkey) michael@0: comm = self.comments[oldkey] michael@0: inline_comment = self.inline_comments[oldkey] michael@0: del self.comments[oldkey] michael@0: del self.inline_comments[oldkey] michael@0: self.comments[newkey] = comm michael@0: self.inline_comments[newkey] = inline_comment michael@0: michael@0: michael@0: def walk(self, function, raise_errors=True, michael@0: call_on_sections=False, **keywargs): michael@0: """ michael@0: Walk every member and call a function on the keyword and value. michael@0: michael@0: Return a dictionary of the return values michael@0: michael@0: If the function raises an exception, raise the errror michael@0: unless ``raise_errors=False``, in which case set the return value to michael@0: ``False``. michael@0: michael@0: Any unrecognised keyword arguments you pass to walk, will be pased on michael@0: to the function you pass in. michael@0: michael@0: Note: if ``call_on_sections`` is ``True`` then - on encountering a michael@0: subsection, *first* the function is called for the *whole* subsection, michael@0: and then recurses into it's members. This means your function must be michael@0: able to handle strings, dictionaries and lists. This allows you michael@0: to change the key of subsections as well as for ordinary members. The michael@0: return value when called on the whole subsection has to be discarded. michael@0: michael@0: See the encode and decode methods for examples, including functions. michael@0: michael@0: .. admonition:: caution michael@0: michael@0: You can use ``walk`` to transform the names of members of a section michael@0: but you mustn't add or delete members. michael@0: michael@0: >>> config = '''[XXXXsection] michael@0: ... XXXXkey = XXXXvalue'''.splitlines() michael@0: >>> cfg = ConfigObj(config) michael@0: >>> cfg michael@0: ConfigObj({'XXXXsection': {'XXXXkey': 'XXXXvalue'}}) michael@0: >>> def transform(section, key): michael@0: ... val = section[key] michael@0: ... newkey = key.replace('XXXX', 'CLIENT1') michael@0: ... section.rename(key, newkey) michael@0: ... if isinstance(val, (tuple, list, dict)): michael@0: ... pass michael@0: ... else: michael@0: ... val = val.replace('XXXX', 'CLIENT1') michael@0: ... section[newkey] = val michael@0: >>> cfg.walk(transform, call_on_sections=True) michael@0: {'CLIENT1section': {'CLIENT1key': None}} michael@0: >>> cfg michael@0: ConfigObj({'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}}) michael@0: """ michael@0: out = {} michael@0: # scalars first michael@0: for i in range(len(self.scalars)): michael@0: entry = self.scalars[i] michael@0: try: michael@0: val = function(self, entry, **keywargs) michael@0: # bound again in case name has changed michael@0: entry = self.scalars[i] michael@0: out[entry] = val michael@0: except Exception: michael@0: if raise_errors: michael@0: raise michael@0: else: michael@0: entry = self.scalars[i] michael@0: out[entry] = False michael@0: # then sections michael@0: for i in range(len(self.sections)): michael@0: entry = self.sections[i] michael@0: if call_on_sections: michael@0: try: michael@0: function(self, entry, **keywargs) michael@0: except Exception: michael@0: if raise_errors: michael@0: raise michael@0: else: michael@0: entry = self.sections[i] michael@0: out[entry] = False michael@0: # bound again in case name has changed michael@0: entry = self.sections[i] michael@0: # previous result is discarded michael@0: out[entry] = self[entry].walk( michael@0: function, michael@0: raise_errors=raise_errors, michael@0: call_on_sections=call_on_sections, michael@0: **keywargs) michael@0: return out michael@0: michael@0: michael@0: def as_bool(self, key): michael@0: """ michael@0: Accepts a key as input. The corresponding value must be a string or michael@0: the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to michael@0: retain compatibility with Python 2.2. michael@0: michael@0: If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns michael@0: ``True``. michael@0: michael@0: If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns michael@0: ``False``. michael@0: michael@0: ``as_bool`` is not case sensitive. michael@0: michael@0: Any other input will raise a ``ValueError``. michael@0: michael@0: >>> a = ConfigObj() michael@0: >>> a['a'] = 'fish' michael@0: >>> a.as_bool('a') michael@0: Traceback (most recent call last): michael@0: ValueError: Value "fish" is neither True nor False michael@0: >>> a['b'] = 'True' michael@0: >>> a.as_bool('b') michael@0: 1 michael@0: >>> a['b'] = 'off' michael@0: >>> a.as_bool('b') michael@0: 0 michael@0: """ michael@0: val = self[key] michael@0: if val == True: michael@0: return True michael@0: elif val == False: michael@0: return False michael@0: else: michael@0: try: michael@0: if not isinstance(val, basestring): michael@0: # TODO: Why do we raise a KeyError here? michael@0: raise KeyError() michael@0: else: michael@0: return self.main._bools[val.lower()] michael@0: except KeyError: michael@0: raise ValueError('Value "%s" is neither True nor False' % val) michael@0: michael@0: michael@0: def as_int(self, key): michael@0: """ michael@0: A convenience method which coerces the specified value to an integer. michael@0: michael@0: If the value is an invalid literal for ``int``, a ``ValueError`` will michael@0: be raised. michael@0: michael@0: >>> a = ConfigObj() michael@0: >>> a['a'] = 'fish' michael@0: >>> a.as_int('a') michael@0: Traceback (most recent call last): michael@0: ValueError: invalid literal for int() with base 10: 'fish' michael@0: >>> a['b'] = '1' michael@0: >>> a.as_int('b') michael@0: 1 michael@0: >>> a['b'] = '3.2' michael@0: >>> a.as_int('b') michael@0: Traceback (most recent call last): michael@0: ValueError: invalid literal for int() with base 10: '3.2' michael@0: """ michael@0: return int(self[key]) michael@0: michael@0: michael@0: def as_float(self, key): michael@0: """ michael@0: A convenience method which coerces the specified value to a float. michael@0: michael@0: If the value is an invalid literal for ``float``, a ``ValueError`` will michael@0: be raised. michael@0: michael@0: >>> a = ConfigObj() michael@0: >>> a['a'] = 'fish' michael@0: >>> a.as_float('a') michael@0: Traceback (most recent call last): michael@0: ValueError: invalid literal for float(): fish michael@0: >>> a['b'] = '1' michael@0: >>> a.as_float('b') michael@0: 1.0 michael@0: >>> a['b'] = '3.2' michael@0: >>> a.as_float('b') michael@0: 3.2000000000000002 michael@0: """ michael@0: return float(self[key]) michael@0: michael@0: michael@0: def as_list(self, key): michael@0: """ michael@0: A convenience method which fetches the specified value, guaranteeing michael@0: that it is a list. michael@0: michael@0: >>> a = ConfigObj() michael@0: >>> a['a'] = 1 michael@0: >>> a.as_list('a') michael@0: [1] michael@0: >>> a['a'] = (1,) michael@0: >>> a.as_list('a') michael@0: [1] michael@0: >>> a['a'] = [1] michael@0: >>> a.as_list('a') michael@0: [1] michael@0: """ michael@0: result = self[key] michael@0: if isinstance(result, (tuple, list)): michael@0: return list(result) michael@0: return [result] michael@0: michael@0: michael@0: def restore_default(self, key): michael@0: """ michael@0: Restore (and return) default value for the specified key. michael@0: michael@0: This method will only work for a ConfigObj that was created michael@0: with a configspec and has been validated. michael@0: michael@0: If there is no default value for this key, ``KeyError`` is raised. michael@0: """ michael@0: default = self.default_values[key] michael@0: dict.__setitem__(self, key, default) michael@0: if key not in self.defaults: michael@0: self.defaults.append(key) michael@0: return default michael@0: michael@0: michael@0: def restore_defaults(self): michael@0: """ michael@0: Recursively restore default values to all members michael@0: that have them. michael@0: michael@0: This method will only work for a ConfigObj that was created michael@0: with a configspec and has been validated. michael@0: michael@0: It doesn't delete or modify entries without default values. michael@0: """ michael@0: for key in self.default_values: michael@0: self.restore_default(key) michael@0: michael@0: for section in self.sections: michael@0: self[section].restore_defaults() michael@0: michael@0: michael@0: class ConfigObj(Section): michael@0: """An object to read, create, and write config files.""" michael@0: michael@0: _keyword = re.compile(r'''^ # line start michael@0: (\s*) # indentation michael@0: ( # keyword michael@0: (?:".*?")| # double quotes michael@0: (?:'.*?')| # single quotes michael@0: (?:[^'"=].*?) # no quotes michael@0: ) michael@0: \s*=\s* # divider michael@0: (.*) # value (including list values and comments) michael@0: $ # line end michael@0: ''', michael@0: re.VERBOSE) michael@0: michael@0: _sectionmarker = re.compile(r'''^ michael@0: (\s*) # 1: indentation michael@0: ((?:\[\s*)+) # 2: section marker open michael@0: ( # 3: section name open michael@0: (?:"\s*\S.*?\s*")| # at least one non-space with double quotes michael@0: (?:'\s*\S.*?\s*')| # at least one non-space with single quotes michael@0: (?:[^'"\s].*?) # at least one non-space unquoted michael@0: ) # section name close michael@0: ((?:\s*\])+) # 4: section marker close michael@0: \s*(\#.*)? # 5: optional comment michael@0: $''', michael@0: re.VERBOSE) michael@0: michael@0: # this regexp pulls list values out as a single string michael@0: # or single values and comments michael@0: # FIXME: this regex adds a '' to the end of comma terminated lists michael@0: # workaround in ``_handle_value`` michael@0: _valueexp = re.compile(r'''^ michael@0: (?: michael@0: (?: michael@0: ( michael@0: (?: michael@0: (?: michael@0: (?:".*?")| # double quotes michael@0: (?:'.*?')| # single quotes michael@0: (?:[^'",\#][^,\#]*?) # unquoted michael@0: ) michael@0: \s*,\s* # comma michael@0: )* # match all list items ending in a comma (if any) michael@0: ) michael@0: ( michael@0: (?:".*?")| # double quotes michael@0: (?:'.*?')| # single quotes michael@0: (?:[^'",\#\s][^,]*?)| # unquoted michael@0: (?:(? 1: michael@0: msg = "Parsing failed with several errors.\nFirst error %s" % info michael@0: error = ConfigObjError(msg) michael@0: else: michael@0: error = self._errors[0] michael@0: # set the errors attribute; it's a list of tuples: michael@0: # (error_type, message, line_number) michael@0: error.errors = self._errors michael@0: # set the config attribute michael@0: error.config = self michael@0: raise error michael@0: # delete private attributes michael@0: del self._errors michael@0: michael@0: if configspec is None: michael@0: self.configspec = None michael@0: else: michael@0: self._handle_configspec(configspec) michael@0: michael@0: michael@0: def _initialise(self, options=None): michael@0: if options is None: michael@0: options = OPTION_DEFAULTS michael@0: michael@0: # initialise a few variables michael@0: self.filename = None michael@0: self._errors = [] michael@0: self.raise_errors = options['raise_errors'] michael@0: self.interpolation = options['interpolation'] michael@0: self.list_values = options['list_values'] michael@0: self.create_empty = options['create_empty'] michael@0: self.file_error = options['file_error'] michael@0: self.stringify = options['stringify'] michael@0: self.indent_type = options['indent_type'] michael@0: self.encoding = options['encoding'] michael@0: self.default_encoding = options['default_encoding'] michael@0: self.BOM = False michael@0: self.newlines = None michael@0: self.write_empty_values = options['write_empty_values'] michael@0: self.unrepr = options['unrepr'] michael@0: michael@0: self.initial_comment = [] michael@0: self.final_comment = [] michael@0: self.configspec = None michael@0: michael@0: if self._inspec: michael@0: self.list_values = False michael@0: michael@0: # Clear section attributes as well michael@0: Section._initialise(self) michael@0: michael@0: michael@0: def __repr__(self): michael@0: def _getval(key): michael@0: try: michael@0: return self[key] michael@0: except MissingInterpolationOption: michael@0: return dict.__getitem__(self, key) michael@0: return ('ConfigObj({%s})' % michael@0: ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) michael@0: for key in (self.scalars + self.sections)])) michael@0: michael@0: michael@0: def _handle_bom(self, infile): michael@0: """ michael@0: Handle any BOM, and decode if necessary. michael@0: michael@0: If an encoding is specified, that *must* be used - but the BOM should michael@0: still be removed (and the BOM attribute set). michael@0: michael@0: (If the encoding is wrongly specified, then a BOM for an alternative michael@0: encoding won't be discovered or removed.) michael@0: michael@0: If an encoding is not specified, UTF8 or UTF16 BOM will be detected and michael@0: removed. The BOM attribute will be set. UTF16 will be decoded to michael@0: unicode. michael@0: michael@0: NOTE: This method must not be called with an empty ``infile``. michael@0: michael@0: Specifying the *wrong* encoding is likely to cause a michael@0: ``UnicodeDecodeError``. michael@0: michael@0: ``infile`` must always be returned as a list of lines, but may be michael@0: passed in as a single string. michael@0: """ michael@0: if ((self.encoding is not None) and michael@0: (self.encoding.lower() not in BOM_LIST)): michael@0: # No need to check for a BOM michael@0: # the encoding specified doesn't have one michael@0: # just decode michael@0: return self._decode(infile, self.encoding) michael@0: michael@0: if isinstance(infile, (list, tuple)): michael@0: line = infile[0] michael@0: else: michael@0: line = infile michael@0: if self.encoding is not None: michael@0: # encoding explicitly supplied michael@0: # And it could have an associated BOM michael@0: # TODO: if encoding is just UTF16 - we ought to check for both michael@0: # TODO: big endian and little endian versions. michael@0: enc = BOM_LIST[self.encoding.lower()] michael@0: if enc == 'utf_16': michael@0: # For UTF16 we try big endian and little endian michael@0: for BOM, (encoding, final_encoding) in BOMS.items(): michael@0: if not final_encoding: michael@0: # skip UTF8 michael@0: continue michael@0: if infile.startswith(BOM): michael@0: ### BOM discovered michael@0: ##self.BOM = True michael@0: # Don't need to remove BOM michael@0: return self._decode(infile, encoding) michael@0: michael@0: # If we get this far, will *probably* raise a DecodeError michael@0: # As it doesn't appear to start with a BOM michael@0: return self._decode(infile, self.encoding) michael@0: michael@0: # Must be UTF8 michael@0: BOM = BOM_SET[enc] michael@0: if not line.startswith(BOM): michael@0: return self._decode(infile, self.encoding) michael@0: michael@0: newline = line[len(BOM):] michael@0: michael@0: # BOM removed michael@0: if isinstance(infile, (list, tuple)): michael@0: infile[0] = newline michael@0: else: michael@0: infile = newline michael@0: self.BOM = True michael@0: return self._decode(infile, self.encoding) michael@0: michael@0: # No encoding specified - so we need to check for UTF8/UTF16 michael@0: for BOM, (encoding, final_encoding) in BOMS.items(): michael@0: if not line.startswith(BOM): michael@0: continue michael@0: else: michael@0: # BOM discovered michael@0: self.encoding = final_encoding michael@0: if not final_encoding: michael@0: self.BOM = True michael@0: # UTF8 michael@0: # remove BOM michael@0: newline = line[len(BOM):] michael@0: if isinstance(infile, (list, tuple)): michael@0: infile[0] = newline michael@0: else: michael@0: infile = newline michael@0: # UTF8 - don't decode michael@0: if isinstance(infile, basestring): michael@0: return infile.splitlines(True) michael@0: else: michael@0: return infile michael@0: # UTF16 - have to decode michael@0: return self._decode(infile, encoding) michael@0: michael@0: # No BOM discovered and no encoding specified, just return michael@0: if isinstance(infile, basestring): michael@0: # infile read from a file will be a single string michael@0: return infile.splitlines(True) michael@0: return infile michael@0: michael@0: michael@0: def _a_to_u(self, aString): michael@0: """Decode ASCII strings to unicode if a self.encoding is specified.""" michael@0: if self.encoding: michael@0: return aString.decode('ascii') michael@0: else: michael@0: return aString michael@0: michael@0: michael@0: def _decode(self, infile, encoding): michael@0: """ michael@0: Decode infile to unicode. Using the specified encoding. michael@0: michael@0: if is a string, it also needs converting to a list. michael@0: """ michael@0: if isinstance(infile, basestring): michael@0: # can't be unicode michael@0: # NOTE: Could raise a ``UnicodeDecodeError`` michael@0: return infile.decode(encoding).splitlines(True) michael@0: for i, line in enumerate(infile): michael@0: if not isinstance(line, unicode): michael@0: # NOTE: The isinstance test here handles mixed lists of unicode/string michael@0: # NOTE: But the decode will break on any non-string values michael@0: # NOTE: Or could raise a ``UnicodeDecodeError`` michael@0: infile[i] = line.decode(encoding) michael@0: return infile michael@0: michael@0: michael@0: def _decode_element(self, line): michael@0: """Decode element to unicode if necessary.""" michael@0: if not self.encoding: michael@0: return line michael@0: if isinstance(line, str) and self.default_encoding: michael@0: return line.decode(self.default_encoding) michael@0: return line michael@0: michael@0: michael@0: def _str(self, value): michael@0: """ michael@0: Used by ``stringify`` within validate, to turn non-string values michael@0: into strings. michael@0: """ michael@0: if not isinstance(value, basestring): michael@0: return str(value) michael@0: else: michael@0: return value michael@0: michael@0: michael@0: def _parse(self, infile): michael@0: """Actually parse the config file.""" michael@0: temp_list_values = self.list_values michael@0: if self.unrepr: michael@0: self.list_values = False michael@0: michael@0: comment_list = [] michael@0: done_start = False michael@0: this_section = self michael@0: maxline = len(infile) - 1 michael@0: cur_index = -1 michael@0: reset_comment = False michael@0: michael@0: while cur_index < maxline: michael@0: if reset_comment: michael@0: comment_list = [] michael@0: cur_index += 1 michael@0: line = infile[cur_index] michael@0: sline = line.strip() michael@0: # do we have anything on the line ? michael@0: if not sline or sline.startswith('#'): michael@0: reset_comment = False michael@0: comment_list.append(line) michael@0: continue michael@0: michael@0: if not done_start: michael@0: # preserve initial comment michael@0: self.initial_comment = comment_list michael@0: comment_list = [] michael@0: done_start = True michael@0: michael@0: reset_comment = True michael@0: # first we check if it's a section marker michael@0: mat = self._sectionmarker.match(line) michael@0: if mat is not None: michael@0: # is a section line michael@0: (indent, sect_open, sect_name, sect_close, comment) = mat.groups() michael@0: if indent and (self.indent_type is None): michael@0: self.indent_type = indent michael@0: cur_depth = sect_open.count('[') michael@0: if cur_depth != sect_close.count(']'): michael@0: self._handle_error("Cannot compute the section depth at line %s.", michael@0: NestingError, infile, cur_index) michael@0: continue michael@0: michael@0: if cur_depth < this_section.depth: michael@0: # the new section is dropping back to a previous level michael@0: try: michael@0: parent = self._match_depth(this_section, michael@0: cur_depth).parent michael@0: except SyntaxError: michael@0: self._handle_error("Cannot compute nesting level at line %s.", michael@0: NestingError, infile, cur_index) michael@0: continue michael@0: elif cur_depth == this_section.depth: michael@0: # the new section is a sibling of the current section michael@0: parent = this_section.parent michael@0: elif cur_depth == this_section.depth + 1: michael@0: # the new section is a child the current section michael@0: parent = this_section michael@0: else: michael@0: self._handle_error("Section too nested at line %s.", michael@0: NestingError, infile, cur_index) michael@0: michael@0: sect_name = self._unquote(sect_name) michael@0: if sect_name in parent: michael@0: self._handle_error('Duplicate section name at line %s.', michael@0: DuplicateError, infile, cur_index) michael@0: continue michael@0: michael@0: # create the new section michael@0: this_section = Section( michael@0: parent, michael@0: cur_depth, michael@0: self, michael@0: name=sect_name) michael@0: parent[sect_name] = this_section michael@0: parent.inline_comments[sect_name] = comment michael@0: parent.comments[sect_name] = comment_list michael@0: continue michael@0: # michael@0: # it's not a section marker, michael@0: # so it should be a valid ``key = value`` line michael@0: mat = self._keyword.match(line) michael@0: if mat is None: michael@0: # it neither matched as a keyword michael@0: # or a section marker michael@0: self._handle_error( michael@0: 'Invalid line at line "%s".', michael@0: ParseError, infile, cur_index) michael@0: else: michael@0: # is a keyword value michael@0: # value will include any inline comment michael@0: (indent, key, value) = mat.groups() michael@0: if indent and (self.indent_type is None): michael@0: self.indent_type = indent michael@0: # check for a multiline value michael@0: if value[:3] in ['"""', "'''"]: michael@0: try: michael@0: value, comment, cur_index = self._multiline( michael@0: value, infile, cur_index, maxline) michael@0: except SyntaxError: michael@0: self._handle_error( michael@0: 'Parse error in value at line %s.', michael@0: ParseError, infile, cur_index) michael@0: continue michael@0: else: michael@0: if self.unrepr: michael@0: comment = '' michael@0: try: michael@0: value = unrepr(value) michael@0: except Exception, e: michael@0: if type(e) == UnknownType: michael@0: msg = 'Unknown name or type in value at line %s.' michael@0: else: michael@0: msg = 'Parse error in value at line %s.' michael@0: self._handle_error(msg, UnreprError, infile, michael@0: cur_index) michael@0: continue michael@0: else: michael@0: if self.unrepr: michael@0: comment = '' michael@0: try: michael@0: value = unrepr(value) michael@0: except Exception, e: michael@0: if isinstance(e, UnknownType): michael@0: msg = 'Unknown name or type in value at line %s.' michael@0: else: michael@0: msg = 'Parse error in value at line %s.' michael@0: self._handle_error(msg, UnreprError, infile, michael@0: cur_index) michael@0: continue michael@0: else: michael@0: # extract comment and lists michael@0: try: michael@0: (value, comment) = self._handle_value(value) michael@0: except SyntaxError: michael@0: self._handle_error( michael@0: 'Parse error in value at line %s.', michael@0: ParseError, infile, cur_index) michael@0: continue michael@0: # michael@0: key = self._unquote(key) michael@0: if key in this_section: michael@0: self._handle_error( michael@0: 'Duplicate keyword name at line %s.', michael@0: DuplicateError, infile, cur_index) michael@0: continue michael@0: # add the key. michael@0: # we set unrepr because if we have got this far we will never michael@0: # be creating a new section michael@0: this_section.__setitem__(key, value, unrepr=True) michael@0: this_section.inline_comments[key] = comment michael@0: this_section.comments[key] = comment_list michael@0: continue michael@0: # michael@0: if self.indent_type is None: michael@0: # no indentation used, set the type accordingly michael@0: self.indent_type = '' michael@0: michael@0: # preserve the final comment michael@0: if not self and not self.initial_comment: michael@0: self.initial_comment = comment_list michael@0: elif not reset_comment: michael@0: self.final_comment = comment_list michael@0: self.list_values = temp_list_values michael@0: michael@0: michael@0: def _match_depth(self, sect, depth): michael@0: """ michael@0: Given a section and a depth level, walk back through the sections michael@0: parents to see if the depth level matches a previous section. michael@0: michael@0: Return a reference to the right section, michael@0: or raise a SyntaxError. michael@0: """ michael@0: while depth < sect.depth: michael@0: if sect is sect.parent: michael@0: # we've reached the top level already michael@0: raise SyntaxError() michael@0: sect = sect.parent michael@0: if sect.depth == depth: michael@0: return sect michael@0: # shouldn't get here michael@0: raise SyntaxError() michael@0: michael@0: michael@0: def _handle_error(self, text, ErrorClass, infile, cur_index): michael@0: """ michael@0: Handle an error according to the error settings. michael@0: michael@0: Either raise the error or store it. michael@0: The error will have occured at ``cur_index`` michael@0: """ michael@0: line = infile[cur_index] michael@0: cur_index += 1 michael@0: message = text % cur_index michael@0: error = ErrorClass(message, cur_index, line) michael@0: if self.raise_errors: michael@0: # raise the error - parsing stops here michael@0: raise error michael@0: # store the error michael@0: # reraise when parsing has finished michael@0: self._errors.append(error) michael@0: michael@0: michael@0: def _unquote(self, value): michael@0: """Return an unquoted version of a value""" michael@0: if not value: michael@0: # should only happen during parsing of lists michael@0: raise SyntaxError michael@0: if (value[0] == value[-1]) and (value[0] in ('"', "'")): michael@0: value = value[1:-1] michael@0: return value michael@0: michael@0: michael@0: def _quote(self, value, multiline=True): michael@0: """ michael@0: Return a safely quoted version of a value. michael@0: michael@0: Raise a ConfigObjError if the value cannot be safely quoted. michael@0: If multiline is ``True`` (default) then use triple quotes michael@0: if necessary. michael@0: michael@0: * Don't quote values that don't need it. michael@0: * Recursively quote members of a list and return a comma joined list. michael@0: * Multiline is ``False`` for lists. michael@0: * Obey list syntax for empty and single member lists. michael@0: michael@0: If ``list_values=False`` then the value is only quoted if it contains michael@0: a ``\\n`` (is multiline) or '#'. michael@0: michael@0: If ``write_empty_values`` is set, and the value is an empty string, it michael@0: won't be quoted. michael@0: """ michael@0: if multiline and self.write_empty_values and value == '': michael@0: # Only if multiline is set, so that it is used for values not michael@0: # keys, and not values that are part of a list michael@0: return '' michael@0: michael@0: if multiline and isinstance(value, (list, tuple)): michael@0: if not value: michael@0: return ',' michael@0: elif len(value) == 1: michael@0: return self._quote(value[0], multiline=False) + ',' michael@0: return ', '.join([self._quote(val, multiline=False) michael@0: for val in value]) michael@0: if not isinstance(value, basestring): michael@0: if self.stringify: michael@0: value = str(value) michael@0: else: michael@0: raise TypeError('Value "%s" is not a string.' % value) michael@0: michael@0: if not value: michael@0: return '""' michael@0: michael@0: no_lists_no_quotes = not self.list_values and '\n' not in value and '#' not in value michael@0: need_triple = multiline and ((("'" in value) and ('"' in value)) or ('\n' in value )) michael@0: hash_triple_quote = multiline and not need_triple and ("'" in value) and ('"' in value) and ('#' in value) michael@0: check_for_single = (no_lists_no_quotes or not need_triple) and not hash_triple_quote michael@0: michael@0: if check_for_single: michael@0: if not self.list_values: michael@0: # we don't quote if ``list_values=False`` michael@0: quot = noquot michael@0: # for normal values either single or double quotes will do michael@0: elif '\n' in value: michael@0: # will only happen if multiline is off - e.g. '\n' in key michael@0: raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) michael@0: elif ((value[0] not in wspace_plus) and michael@0: (value[-1] not in wspace_plus) and michael@0: (',' not in value)): michael@0: quot = noquot michael@0: else: michael@0: quot = self._get_single_quote(value) michael@0: else: michael@0: # if value has '\n' or "'" *and* '"', it will need triple quotes michael@0: quot = self._get_triple_quote(value) michael@0: michael@0: if quot == noquot and '#' in value and self.list_values: michael@0: quot = self._get_single_quote(value) michael@0: michael@0: return quot % value michael@0: michael@0: michael@0: def _get_single_quote(self, value): michael@0: if ("'" in value) and ('"' in value): michael@0: raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) michael@0: elif '"' in value: michael@0: quot = squot michael@0: else: michael@0: quot = dquot michael@0: return quot michael@0: michael@0: michael@0: def _get_triple_quote(self, value): michael@0: if (value.find('"""') != -1) and (value.find("'''") != -1): michael@0: raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) michael@0: if value.find('"""') == -1: michael@0: quot = tdquot michael@0: else: michael@0: quot = tsquot michael@0: return quot michael@0: michael@0: michael@0: def _handle_value(self, value): michael@0: """ michael@0: Given a value string, unquote, remove comment, michael@0: handle lists. (including empty and single member lists) michael@0: """ michael@0: if self._inspec: michael@0: # Parsing a configspec so don't handle comments michael@0: return (value, '') michael@0: # do we look for lists in values ? michael@0: if not self.list_values: michael@0: mat = self._nolistvalue.match(value) michael@0: if mat is None: michael@0: raise SyntaxError() michael@0: # NOTE: we don't unquote here michael@0: return mat.groups() michael@0: # michael@0: mat = self._valueexp.match(value) michael@0: if mat is None: michael@0: # the value is badly constructed, probably badly quoted, michael@0: # or an invalid list michael@0: raise SyntaxError() michael@0: (list_values, single, empty_list, comment) = mat.groups() michael@0: if (list_values == '') and (single is None): michael@0: # change this if you want to accept empty values michael@0: raise SyntaxError() michael@0: # NOTE: note there is no error handling from here if the regex michael@0: # is wrong: then incorrect values will slip through michael@0: if empty_list is not None: michael@0: # the single comma - meaning an empty list michael@0: return ([], comment) michael@0: if single is not None: michael@0: # handle empty values michael@0: if list_values and not single: michael@0: # FIXME: the '' is a workaround because our regex now matches michael@0: # '' at the end of a list if it has a trailing comma michael@0: single = None michael@0: else: michael@0: single = single or '""' michael@0: single = self._unquote(single) michael@0: if list_values == '': michael@0: # not a list value michael@0: return (single, comment) michael@0: the_list = self._listvalueexp.findall(list_values) michael@0: the_list = [self._unquote(val) for val in the_list] michael@0: if single is not None: michael@0: the_list += [single] michael@0: return (the_list, comment) michael@0: michael@0: michael@0: def _multiline(self, value, infile, cur_index, maxline): michael@0: """Extract the value, where we are in a multiline situation.""" michael@0: quot = value[:3] michael@0: newvalue = value[3:] michael@0: single_line = self._triple_quote[quot][0] michael@0: multi_line = self._triple_quote[quot][1] michael@0: mat = single_line.match(value) michael@0: if mat is not None: michael@0: retval = list(mat.groups()) michael@0: retval.append(cur_index) michael@0: return retval michael@0: elif newvalue.find(quot) != -1: michael@0: # somehow the triple quote is missing michael@0: raise SyntaxError() michael@0: # michael@0: while cur_index < maxline: michael@0: cur_index += 1 michael@0: newvalue += '\n' michael@0: line = infile[cur_index] michael@0: if line.find(quot) == -1: michael@0: newvalue += line michael@0: else: michael@0: # end of multiline, process it michael@0: break michael@0: else: michael@0: # we've got to the end of the config, oops... michael@0: raise SyntaxError() michael@0: mat = multi_line.match(line) michael@0: if mat is None: michael@0: # a badly formed line michael@0: raise SyntaxError() michael@0: (value, comment) = mat.groups() michael@0: return (newvalue + value, comment, cur_index) michael@0: michael@0: michael@0: def _handle_configspec(self, configspec): michael@0: """Parse the configspec.""" michael@0: # FIXME: Should we check that the configspec was created with the michael@0: # correct settings ? (i.e. ``list_values=False``) michael@0: if not isinstance(configspec, ConfigObj): michael@0: try: michael@0: configspec = ConfigObj(configspec, michael@0: raise_errors=True, michael@0: file_error=True, michael@0: _inspec=True) michael@0: except ConfigObjError, e: michael@0: # FIXME: Should these errors have a reference michael@0: # to the already parsed ConfigObj ? michael@0: raise ConfigspecError('Parsing configspec failed: %s' % e) michael@0: except IOError, e: michael@0: raise IOError('Reading configspec failed: %s' % e) michael@0: michael@0: self.configspec = configspec michael@0: michael@0: michael@0: michael@0: def _set_configspec(self, section, copy): michael@0: """ michael@0: Called by validate. Handles setting the configspec on subsections michael@0: including sections to be validated by __many__ michael@0: """ michael@0: configspec = section.configspec michael@0: many = configspec.get('__many__') michael@0: if isinstance(many, dict): michael@0: for entry in section.sections: michael@0: if entry not in configspec: michael@0: section[entry].configspec = many michael@0: michael@0: for entry in configspec.sections: michael@0: if entry == '__many__': michael@0: continue michael@0: if entry not in section: michael@0: section[entry] = {} michael@0: section[entry]._created = True michael@0: if copy: michael@0: # copy comments michael@0: section.comments[entry] = configspec.comments.get(entry, []) michael@0: section.inline_comments[entry] = configspec.inline_comments.get(entry, '') michael@0: michael@0: # Could be a scalar when we expect a section michael@0: if isinstance(section[entry], Section): michael@0: section[entry].configspec = configspec[entry] michael@0: michael@0: michael@0: def _write_line(self, indent_string, entry, this_entry, comment): michael@0: """Write an individual line, for the write method""" michael@0: # NOTE: the calls to self._quote here handles non-StringType values. michael@0: if not self.unrepr: michael@0: val = self._decode_element(self._quote(this_entry)) michael@0: else: michael@0: val = repr(this_entry) michael@0: return '%s%s%s%s%s' % (indent_string, michael@0: self._decode_element(self._quote(entry, multiline=False)), michael@0: self._a_to_u(' = '), michael@0: val, michael@0: self._decode_element(comment)) michael@0: michael@0: michael@0: def _write_marker(self, indent_string, depth, entry, comment): michael@0: """Write a section marker line""" michael@0: return '%s%s%s%s%s' % (indent_string, michael@0: self._a_to_u('[' * depth), michael@0: self._quote(self._decode_element(entry), multiline=False), michael@0: self._a_to_u(']' * depth), michael@0: self._decode_element(comment)) michael@0: michael@0: michael@0: def _handle_comment(self, comment): michael@0: """Deal with a comment.""" michael@0: if not comment: michael@0: return '' michael@0: start = self.indent_type michael@0: if not comment.startswith('#'): michael@0: start += self._a_to_u(' # ') michael@0: return (start + comment) michael@0: michael@0: michael@0: # Public methods michael@0: michael@0: def write(self, outfile=None, section=None): michael@0: """ michael@0: Write the current ConfigObj as a file michael@0: michael@0: tekNico: FIXME: use StringIO instead of real files michael@0: michael@0: >>> filename = a.filename michael@0: >>> a.filename = 'test.ini' michael@0: >>> a.write() michael@0: >>> a.filename = filename michael@0: >>> a == ConfigObj('test.ini', raise_errors=True) michael@0: 1 michael@0: >>> import os michael@0: >>> os.remove('test.ini') michael@0: """ michael@0: if self.indent_type is None: michael@0: # this can be true if initialised from a dictionary michael@0: self.indent_type = DEFAULT_INDENT_TYPE michael@0: michael@0: out = [] michael@0: cs = self._a_to_u('#') michael@0: csp = self._a_to_u('# ') michael@0: if section is None: michael@0: int_val = self.interpolation michael@0: self.interpolation = False michael@0: section = self michael@0: for line in self.initial_comment: michael@0: line = self._decode_element(line) michael@0: stripped_line = line.strip() michael@0: if stripped_line and not stripped_line.startswith(cs): michael@0: line = csp + line michael@0: out.append(line) michael@0: michael@0: indent_string = self.indent_type * section.depth michael@0: for entry in (section.scalars + section.sections): michael@0: if entry in section.defaults: michael@0: # don't write out default values michael@0: continue michael@0: for comment_line in section.comments[entry]: michael@0: comment_line = self._decode_element(comment_line.lstrip()) michael@0: if comment_line and not comment_line.startswith(cs): michael@0: comment_line = csp + comment_line michael@0: out.append(indent_string + comment_line) michael@0: this_entry = section[entry] michael@0: comment = self._handle_comment(section.inline_comments[entry]) michael@0: michael@0: if isinstance(this_entry, dict): michael@0: # a section michael@0: out.append(self._write_marker( michael@0: indent_string, michael@0: this_entry.depth, michael@0: entry, michael@0: comment)) michael@0: out.extend(self.write(section=this_entry)) michael@0: else: michael@0: out.append(self._write_line( michael@0: indent_string, michael@0: entry, michael@0: this_entry, michael@0: comment)) michael@0: michael@0: if section is self: michael@0: for line in self.final_comment: michael@0: line = self._decode_element(line) michael@0: stripped_line = line.strip() michael@0: if stripped_line and not stripped_line.startswith(cs): michael@0: line = csp + line michael@0: out.append(line) michael@0: self.interpolation = int_val michael@0: michael@0: if section is not self: michael@0: return out michael@0: michael@0: if (self.filename is None) and (outfile is None): michael@0: # output a list of lines michael@0: # might need to encode michael@0: # NOTE: This will *screw* UTF16, each line will start with the BOM michael@0: if self.encoding: michael@0: out = [l.encode(self.encoding) for l in out] michael@0: if (self.BOM and ((self.encoding is None) or michael@0: (BOM_LIST.get(self.encoding.lower()) == 'utf_8'))): michael@0: # Add the UTF8 BOM michael@0: if not out: michael@0: out.append('') michael@0: out[0] = BOM_UTF8 + out[0] michael@0: return out michael@0: michael@0: # Turn the list to a string, joined with correct newlines michael@0: newline = self.newlines or os.linesep michael@0: if (getattr(outfile, 'mode', None) is not None and outfile.mode == 'w' michael@0: and sys.platform == 'win32' and newline == '\r\n'): michael@0: # Windows specific hack to avoid writing '\r\r\n' michael@0: newline = '\n' michael@0: output = self._a_to_u(newline).join(out) michael@0: if self.encoding: michael@0: output = output.encode(self.encoding) michael@0: if self.BOM and ((self.encoding is None) or match_utf8(self.encoding)): michael@0: # Add the UTF8 BOM michael@0: output = BOM_UTF8 + output michael@0: michael@0: if not output.endswith(newline): michael@0: output += newline michael@0: if outfile is not None: michael@0: outfile.write(output) michael@0: else: michael@0: h = open(self.filename, 'wb') michael@0: h.write(output) michael@0: h.close() michael@0: michael@0: michael@0: def validate(self, validator, preserve_errors=False, copy=False, michael@0: section=None): michael@0: """ michael@0: Test the ConfigObj against a configspec. michael@0: michael@0: It uses the ``validator`` object from *validate.py*. michael@0: michael@0: To run ``validate`` on the current ConfigObj, call: :: michael@0: michael@0: test = config.validate(validator) michael@0: michael@0: (Normally having previously passed in the configspec when the ConfigObj michael@0: was created - you can dynamically assign a dictionary of checks to the michael@0: ``configspec`` attribute of a section though). michael@0: michael@0: It returns ``True`` if everything passes, or a dictionary of michael@0: pass/fails (True/False). If every member of a subsection passes, it michael@0: will just have the value ``True``. (It also returns ``False`` if all michael@0: members fail). michael@0: michael@0: In addition, it converts the values from strings to their native michael@0: types if their checks pass (and ``stringify`` is set). michael@0: michael@0: If ``preserve_errors`` is ``True`` (``False`` is default) then instead michael@0: of a marking a fail with a ``False``, it will preserve the actual michael@0: exception object. This can contain info about the reason for failure. michael@0: For example the ``VdtValueTooSmallError`` indicates that the value michael@0: supplied was too small. If a value (or section) is missing it will michael@0: still be marked as ``False``. michael@0: michael@0: You must have the validate module to use ``preserve_errors=True``. michael@0: michael@0: You can then use the ``flatten_errors`` function to turn your nested michael@0: results dictionary into a flattened list of failures - useful for michael@0: displaying meaningful error messages. michael@0: """ michael@0: if section is None: michael@0: if self.configspec is None: michael@0: raise ValueError('No configspec supplied.') michael@0: if preserve_errors: michael@0: # We do this once to remove a top level dependency on the validate module michael@0: # Which makes importing configobj faster michael@0: from validate import VdtMissingValue michael@0: self._vdtMissingValue = VdtMissingValue michael@0: michael@0: section = self michael@0: michael@0: if copy: michael@0: section.initial_comment = section.configspec.initial_comment michael@0: section.final_comment = section.configspec.final_comment michael@0: section.encoding = section.configspec.encoding michael@0: section.BOM = section.configspec.BOM michael@0: section.newlines = section.configspec.newlines michael@0: section.indent_type = section.configspec.indent_type michael@0: michael@0: # michael@0: # section.default_values.clear() #?? michael@0: configspec = section.configspec michael@0: self._set_configspec(section, copy) michael@0: michael@0: michael@0: def validate_entry(entry, spec, val, missing, ret_true, ret_false): michael@0: section.default_values.pop(entry, None) michael@0: michael@0: try: michael@0: section.default_values[entry] = validator.get_default_value(configspec[entry]) michael@0: except (KeyError, AttributeError, validator.baseErrorClass): michael@0: # No default, bad default or validator has no 'get_default_value' michael@0: # (e.g. SimpleVal) michael@0: pass michael@0: michael@0: try: michael@0: check = validator.check(spec, michael@0: val, michael@0: missing=missing michael@0: ) michael@0: except validator.baseErrorClass, e: michael@0: if not preserve_errors or isinstance(e, self._vdtMissingValue): michael@0: out[entry] = False michael@0: else: michael@0: # preserve the error michael@0: out[entry] = e michael@0: ret_false = False michael@0: ret_true = False michael@0: else: michael@0: ret_false = False michael@0: out[entry] = True michael@0: if self.stringify or missing: michael@0: # if we are doing type conversion michael@0: # or the value is a supplied default michael@0: if not self.stringify: michael@0: if isinstance(check, (list, tuple)): michael@0: # preserve lists michael@0: check = [self._str(item) for item in check] michael@0: elif missing and check is None: michael@0: # convert the None from a default to a '' michael@0: check = '' michael@0: else: michael@0: check = self._str(check) michael@0: if (check != val) or missing: michael@0: section[entry] = check michael@0: if not copy and missing and entry not in section.defaults: michael@0: section.defaults.append(entry) michael@0: return ret_true, ret_false michael@0: michael@0: # michael@0: out = {} michael@0: ret_true = True michael@0: ret_false = True michael@0: michael@0: unvalidated = [k for k in section.scalars if k not in configspec] michael@0: incorrect_sections = [k for k in configspec.sections if k in section.scalars] michael@0: incorrect_scalars = [k for k in configspec.scalars if k in section.sections] michael@0: michael@0: for entry in configspec.scalars: michael@0: if entry in ('__many__', '___many___'): michael@0: # reserved names michael@0: continue michael@0: if (not entry in section.scalars) or (entry in section.defaults): michael@0: # missing entries michael@0: # or entries from defaults michael@0: missing = True michael@0: val = None michael@0: if copy and entry not in section.scalars: michael@0: # copy comments michael@0: section.comments[entry] = ( michael@0: configspec.comments.get(entry, [])) michael@0: section.inline_comments[entry] = ( michael@0: configspec.inline_comments.get(entry, '')) michael@0: # michael@0: else: michael@0: missing = False michael@0: val = section[entry] michael@0: michael@0: ret_true, ret_false = validate_entry(entry, configspec[entry], val, michael@0: missing, ret_true, ret_false) michael@0: michael@0: many = None michael@0: if '__many__' in configspec.scalars: michael@0: many = configspec['__many__'] michael@0: elif '___many___' in configspec.scalars: michael@0: many = configspec['___many___'] michael@0: michael@0: if many is not None: michael@0: for entry in unvalidated: michael@0: val = section[entry] michael@0: ret_true, ret_false = validate_entry(entry, many, val, False, michael@0: ret_true, ret_false) michael@0: unvalidated = [] michael@0: michael@0: for entry in incorrect_scalars: michael@0: ret_true = False michael@0: if not preserve_errors: michael@0: out[entry] = False michael@0: else: michael@0: ret_false = False michael@0: msg = 'Value %r was provided as a section' % entry michael@0: out[entry] = validator.baseErrorClass(msg) michael@0: for entry in incorrect_sections: michael@0: ret_true = False michael@0: if not preserve_errors: michael@0: out[entry] = False michael@0: else: michael@0: ret_false = False michael@0: msg = 'Section %r was provided as a single value' % entry michael@0: out[entry] = validator.baseErrorClass(msg) michael@0: michael@0: # Missing sections will have been created as empty ones when the michael@0: # configspec was read. michael@0: for entry in section.sections: michael@0: # FIXME: this means DEFAULT is not copied in copy mode michael@0: if section is self and entry == 'DEFAULT': michael@0: continue michael@0: if section[entry].configspec is None: michael@0: unvalidated.append(entry) michael@0: continue michael@0: if copy: michael@0: section.comments[entry] = configspec.comments.get(entry, []) michael@0: section.inline_comments[entry] = configspec.inline_comments.get(entry, '') michael@0: check = self.validate(validator, preserve_errors=preserve_errors, copy=copy, section=section[entry]) michael@0: out[entry] = check michael@0: if check == False: michael@0: ret_true = False michael@0: elif check == True: michael@0: ret_false = False michael@0: else: michael@0: ret_true = False michael@0: michael@0: section.extra_values = unvalidated michael@0: if preserve_errors and not section._created: michael@0: # If the section wasn't created (i.e. it wasn't missing) michael@0: # then we can't return False, we need to preserve errors michael@0: ret_false = False michael@0: # michael@0: if ret_false and preserve_errors and out: michael@0: # If we are preserving errors, but all michael@0: # the failures are from missing sections / values michael@0: # then we can return False. Otherwise there is a michael@0: # real failure that we need to preserve. michael@0: ret_false = not any(out.values()) michael@0: if ret_true: michael@0: return True michael@0: elif ret_false: michael@0: return False michael@0: return out michael@0: michael@0: michael@0: def reset(self): michael@0: """Clear ConfigObj instance and restore to 'freshly created' state.""" michael@0: self.clear() michael@0: self._initialise() michael@0: # FIXME: Should be done by '_initialise', but ConfigObj constructor (and reload) michael@0: # requires an empty dictionary michael@0: self.configspec = None michael@0: # Just to be sure ;-) michael@0: self._original_configspec = None michael@0: michael@0: michael@0: def reload(self): michael@0: """ michael@0: Reload a ConfigObj from file. michael@0: michael@0: This method raises a ``ReloadError`` if the ConfigObj doesn't have michael@0: a filename attribute pointing to a file. michael@0: """ michael@0: if not isinstance(self.filename, basestring): michael@0: raise ReloadError() michael@0: michael@0: filename = self.filename michael@0: current_options = {} michael@0: for entry in OPTION_DEFAULTS: michael@0: if entry == 'configspec': michael@0: continue michael@0: current_options[entry] = getattr(self, entry) michael@0: michael@0: configspec = self._original_configspec michael@0: current_options['configspec'] = configspec michael@0: michael@0: self.clear() michael@0: self._initialise(current_options) michael@0: self._load(filename, configspec) michael@0: michael@0: michael@0: michael@0: class SimpleVal(object): michael@0: """ michael@0: A simple validator. michael@0: Can be used to check that all members expected are present. michael@0: michael@0: To use it, provide a configspec with all your members in (the value given michael@0: will be ignored). Pass an instance of ``SimpleVal`` to the ``validate`` michael@0: method of your ``ConfigObj``. ``validate`` will return ``True`` if all michael@0: members are present, or a dictionary with True/False meaning michael@0: present/missing. (Whole missing sections will be replaced with ``False``) michael@0: """ michael@0: michael@0: def __init__(self): michael@0: self.baseErrorClass = ConfigObjError michael@0: michael@0: def check(self, check, member, missing=False): michael@0: """A dummy check method, always returns the value unchanged.""" michael@0: if missing: michael@0: raise self.baseErrorClass() michael@0: return member michael@0: michael@0: michael@0: def flatten_errors(cfg, res, levels=None, results=None): michael@0: """ michael@0: An example function that will turn a nested dictionary of results michael@0: (as returned by ``ConfigObj.validate``) into a flat list. michael@0: michael@0: ``cfg`` is the ConfigObj instance being checked, ``res`` is the results michael@0: dictionary returned by ``validate``. michael@0: michael@0: (This is a recursive function, so you shouldn't use the ``levels`` or michael@0: ``results`` arguments - they are used by the function.) michael@0: michael@0: Returns a list of keys that failed. Each member of the list is a tuple:: michael@0: michael@0: ([list of sections...], key, result) michael@0: michael@0: If ``validate`` was called with ``preserve_errors=False`` (the default) michael@0: then ``result`` will always be ``False``. michael@0: michael@0: *list of sections* is a flattened list of sections that the key was found michael@0: in. michael@0: michael@0: If the section was missing (or a section was expected and a scalar provided michael@0: - or vice-versa) then key will be ``None``. michael@0: michael@0: If the value (or section) was missing then ``result`` will be ``False``. michael@0: michael@0: If ``validate`` was called with ``preserve_errors=True`` and a value michael@0: was present, but failed the check, then ``result`` will be the exception michael@0: object returned. You can use this as a string that describes the failure. michael@0: michael@0: For example *The value "3" is of the wrong type*. michael@0: """ michael@0: if levels is None: michael@0: # first time called michael@0: levels = [] michael@0: results = [] michael@0: if res == True: michael@0: return results michael@0: if res == False or isinstance(res, Exception): michael@0: results.append((levels[:], None, res)) michael@0: if levels: michael@0: levels.pop() michael@0: return results michael@0: for (key, val) in res.items(): michael@0: if val == True: michael@0: continue michael@0: if isinstance(cfg.get(key), dict): michael@0: # Go down one level michael@0: levels.append(key) michael@0: flatten_errors(cfg[key], val, levels, results) michael@0: continue michael@0: results.append((levels[:], key, val)) michael@0: # michael@0: # Go up one level michael@0: if levels: michael@0: levels.pop() michael@0: # michael@0: return results michael@0: michael@0: michael@0: def get_extra_values(conf, _prepend=()): michael@0: """ michael@0: Find all the values and sections not in the configspec from a validated michael@0: ConfigObj. michael@0: michael@0: ``get_extra_values`` returns a list of tuples where each tuple represents michael@0: either an extra section, or an extra value. michael@0: michael@0: The tuples contain two values, a tuple representing the section the value michael@0: is in and the name of the extra values. For extra values in the top level michael@0: section the first member will be an empty tuple. For values in the 'foo' michael@0: section the first member will be ``('foo',)``. For members in the 'bar' michael@0: subsection of the 'foo' section the first member will be ``('foo', 'bar')``. michael@0: michael@0: NOTE: If you call ``get_extra_values`` on a ConfigObj instance that hasn't michael@0: been validated it will return an empty list. michael@0: """ michael@0: out = [] michael@0: michael@0: out.extend([(_prepend, name) for name in conf.extra_values]) michael@0: for name in conf.sections: michael@0: if name not in conf.extra_values: michael@0: out.extend(get_extra_values(conf[name], _prepend + (name,))) michael@0: return out michael@0: michael@0: michael@0: """*A programming language is a medium of expression.* - Paul Graham"""