| # configobj.py |
| # A config file reader/writer that supports nested sections in config files. |
| # Copyright (C) 2005-2010 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__ import generators |
| |
| import os |
| import re |
| import sys |
| |
| from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE |
| |
| |
| # imported lazily to avoid startup performance hit if it isn't used |
| compiler = None |
| |
| # 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 |
| } |
| |
| |
| def match_utf8(encoding): |
| return BOM_LIST.get(encoding.lower()) == 'utf_8' |
| |
| |
| # Quote strings used for writing values |
| squot = "'%s'" |
| dquot = '"%s"' |
| noquot = "%s" |
| wspace_plus = ' \r\n\v\t\'"' |
| tsquot = '"""%s"""' |
| tdquot = "'''%s'''" |
| |
| # Sentinel for use in getattr calls to replace hasattr |
| MISSING = object() |
| |
| __version__ = '4.7.2' |
| |
| try: |
| any |
| except NameError: |
| def any(iterable): |
| for entry in iterable: |
| if entry: |
| return True |
| return False |
| |
| |
| __all__ = ( |
| '__version__', |
| 'DEFAULT_INDENT_TYPE', |
| 'DEFAULT_INTERPOLATION', |
| 'ConfigObjError', |
| 'NestingError', |
| 'ParseError', |
| 'DuplicateError', |
| 'ConfigspecError', |
| 'ConfigObj', |
| 'SimpleVal', |
| 'InterpolationError', |
| 'InterpolationLoopError', |
| 'MissingInterpolationOption', |
| 'RepeatSectionError', |
| 'ReloadError', |
| 'UnreprError', |
| 'UnknownType', |
| 'flatten_errors', |
| 'get_extra_values' |
| ) |
| |
| DEFAULT_INTERPOLATION = 'configparser' |
| DEFAULT_INDENT_TYPE = ' ' |
| MAX_INTERPOL_DEPTH = 10 |
| |
| OPTION_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, |
| } |
| |
| |
| |
| def getObj(s): |
| global compiler |
| if compiler is None: |
| import compiler |
| s = "a=" + s |
| p = compiler.parse(s) |
| return p.getChildren()[1].getChildren()[0].getChildren()[1] |
| |
| |
| class UnknownType(Exception): |
| pass |
| |
| |
| class Builder(object): |
| |
| def build(self, o): |
| m = getattr(self, 'build_' + o.__class__.__name__, None) |
| if m is None: |
| raise UnknownType(o.__class__.__name__) |
| return m(o) |
| |
| def build_List(self, o): |
| return map(self.build, o.getChildren()) |
| |
| def build_Const(self, o): |
| return o.value |
| |
| def build_Dict(self, o): |
| d = {} |
| i = iter(map(self.build, o.getChildren())) |
| for el in i: |
| d[el] = i.next() |
| return d |
| |
| def build_Tuple(self, o): |
| return tuple(self.build_List(o)) |
| |
| def build_Name(self, o): |
| if o.name == 'None': |
| return None |
| if o.name == 'True': |
| return True |
| if o.name == 'False': |
| return False |
| |
| # An undefined Name |
| raise UnknownType('Undefined Name') |
| |
| def build_Add(self, o): |
| real, imag = map(self.build_Const, o.getChildren()) |
| try: |
| real = float(real) |
| except TypeError: |
| raise UnknownType('Add') |
| if not isinstance(imag, complex) or imag.real != 0.0: |
| raise UnknownType('Add') |
| return real+imag |
| |
| def build_Getattr(self, o): |
| parent = self.build(o.expr) |
| return getattr(parent, o.attrname) |
| |
| def build_UnarySub(self, o): |
| return -self.build_Const(o.getChildren()[0]) |
| |
| def build_UnaryAdd(self, o): |
| return self.build_Const(o.getChildren()[0]) |
| |
| |
| _builder = Builder() |
| |
| |
| def unrepr(s): |
| if not s: |
| return s |
| return _builder.build(getObj(s)) |
| |
| |
| |
| class ConfigObjError(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 = line |
| self.line_number = line_number |
| SyntaxError.__init__(self, message) |
| |
| |
| class NestingError(ConfigObjError): |
| """ |
| This error indicates a level of nesting that doesn't match. |
| """ |
| |
| |
| class ParseError(ConfigObjError): |
| """ |
| This error indicates that a line is badly written. |
| It is neither a valid ``key = value`` line, |
| nor a valid section marker line. |
| """ |
| |
| |
| class ReloadError(IOError): |
| """ |
| A 'reload' operation failed. |
| This exception is a subclass of ``IOError``. |
| """ |
| def __init__(self): |
| IOError.__init__(self, 'reload failed, filename is not set.') |
| |
| |
| class DuplicateError(ConfigObjError): |
| """ |
| The keyword or section specified already exists. |
| """ |
| |
| |
| class ConfigspecError(ConfigObjError): |
| """ |
| An error occured whilst parsing a configspec. |
| """ |
| |
| |
| class InterpolationError(ConfigObjError): |
| """Base class for the two interpolation errors.""" |
| |
| |
| class InterpolationLoopError(InterpolationError): |
| """Maximum interpolation depth exceeded in string interpolation.""" |
| |
| def __init__(self, option): |
| InterpolationError.__init__( |
| self, |
| 'interpolation loop detected in value "%s".' % option) |
| |
| |
| class RepeatSectionError(ConfigObjError): |
| """ |
| This error indicates additional sections in a section with a |
| ``__many__`` (repeated) section. |
| """ |
| |
| |
| class MissingInterpolationOption(InterpolationError): |
| """A value specified for interpolation was missing.""" |
| def __init__(self, option): |
| msg = 'missing option "%s" in interpolation.' % option |
| InterpolationError.__init__(self, msg) |
| |
| |
| class UnreprError(ConfigObjError): |
| """An error parsing in unrepr mode.""" |
| |
| |
| |
| class InterpolationEngine(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") |
| _cookie = '%' |
| |
| def __init__(self, section): |
| # the Section instance that "owns" this engine |
| self.section = section |
| |
| |
| def interpolate(self, key, value): |
| # short-cut |
| if not self._cookie in value: |
| return value |
| |
| def recursive_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? |
| if (key, section.name) in backtrail: |
| # Yes - infinite loop detected |
| raise InterpolationLoopError(key) |
| # Place a marker on our backtrail so we won't come back here again |
| backtrail[(key, section.name)] = 1 |
| |
| # Now start the actual work |
| match = self._KEYCRE.search(value) |
| while match: |
| # The actual parsing of the match is implementation-dependent, |
| # so delegate to our helper function |
| k, v, s = self._parse_match(match) |
| if k is None: |
| # That's the signal that no further interpolation is needed |
| replacement = v |
| else: |
| # Further interpolation may be needed to obtain final value |
| replacement = recursive_interpolate(k, v, s, backtrail) |
| # Replace the matched string with its final value |
| start, 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 loop |
| match = self._KEYCRE.search(value, new_search_start) |
| |
| # Now safe to come back here again; remove marker from backtrail |
| del backtrail[(key, section.name)] |
| |
| return value |
| |
| # Back in interpolate(), all we have to do is kick off the recursive |
| # function with appropriate starting values |
| value = recursive_interpolate(key, value, self.section, {}) |
| return value |
| |
| |
| def _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.interpolation |
| self.section.main.interpolation = False |
| |
| # Start at section that "owns" this InterpolationEngine |
| current_section = self.section |
| while True: |
| # try the current section first |
| val = current_section.get(key) |
| if val is not None and not isinstance(val, Section): |
| break |
| # try "DEFAULT" next |
| val = current_section.get('DEFAULT', {}).get(key) |
| if val is not None and not isinstance(val, Section): |
| break |
| # move up to parent and try again |
| # top-level's parent is itself |
| if current_section.parent is current_section: |
| # reached top level, time to give up |
| break |
| current_section = current_section.parent |
| |
| # restore interpolation to previous value before returning |
| self.section.main.interpolation = save_interp |
| if val is None: |
| raise MissingInterpolationOption(key) |
| return val, current_section |
| |
| |
| def _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 "$"). |
| """ |
| raise NotImplementedError() |
| |
| |
| |
| class ConfigParserInterpolation(InterpolationEngine): |
| """Behaves like ConfigParser.""" |
| _cookie = '%' |
| _KEYCRE = re.compile(r"%\(([^)]*)\)s") |
| |
| def _parse_match(self, match): |
| key = match.group(1) |
| value, section = self._fetch(key) |
| return key, value, section |
| |
| |
| |
| class TemplateInterpolation(InterpolationEngine): |
| """Behaves like string.Template.""" |
| _cookie = '$' |
| _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 section |
| key = match.group('named') or match.group('braced') |
| if key is not None: |
| value, section = self._fetch(key) |
| return key, value, section |
| # Escaped delimiter (e.g., $$): return single delimiter |
| if match.group('escaped') is not None: |
| # Return None for key and section to indicate it's time to stop |
| return None, self._delimiter, None |
| # Anything else: ignore completely, just return it unchanged |
| return None, match.group(), None |
| |
| |
| interpolation_engines = { |
| 'configparser': ConfigParserInterpolation, |
| 'template': TemplateInterpolation, |
| } |
| |
| |
| def __newobj__(cls, *args): |
| # Hack for pickle |
| return cls.__new__(cls, *args) |
| |
| class Section(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 __setstate__(self, state): |
| dict.update(self, state[0]) |
| self.__dict__.update(state[1]) |
| |
| def __reduce__(self): |
| state = (dict(self), self.__dict__) |
| return (__newobj__, (self.__class__,), state) |
| |
| |
| 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 |
| """ |
| if indict is None: |
| indict = {} |
| dict.__init__(self) |
| # used for nesting level *and* interpolation |
| self.parent = parent |
| # used for the interpolation attribute |
| self.main = main |
| # level of nesting depth of this Section |
| self.depth = depth |
| # purely for information |
| self.name = name |
| # |
| self._initialise() |
| # we do this explicitly so that __setitem__ is used properly |
| # (rather than just passing to ``dict.__init__``) |
| for entry, value in indict.iteritems(): |
| self[entry] = value |
| |
| |
| def _initialise(self): |
| # the sequence of scalar values in this Section |
| self.scalars = [] |
| # the sequence of sections in this Section |
| self.sections = [] |
| # for comments :-) |
| self.comments = {} |
| self.inline_comments = {} |
| # the configspec |
| self.configspec = None |
| # for defaults |
| self.defaults = [] |
| self.default_values = {} |
| self.extra_values = [] |
| self._created = False |
| |
| |
| def _interpolate(self, key, value): |
| try: |
| # do we already have an interpolation engine? |
| engine = self._interpolation_engine |
| except AttributeError: |
| # not yet: first time running _interpolate(), so pick the engine |
| name = self.main.interpolation |
| if name == True: # note that "if name:" would be incorrect here |
| # backwards-compatibility: interpolation=True means use default |
| name = DEFAULT_INTERPOLATION |
| name = name.lower() # so that "Template", "template", etc. all work |
| class_ = interpolation_engines.get(name, None) |
| if class_ is None: |
| # invalid value for self.main.interpolation |
| self.main.interpolation = False |
| return value |
| else: |
| # save reference to engine so we don't have to do this again |
| engine = self._interpolation_engine = class_(self) |
| # let the engine do the actual work |
| return engine.interpolate(key, value) |
| |
| |
| def __getitem__(self, key): |
| """Fetch the item and do string interpolation.""" |
| val = dict.__getitem__(self, key) |
| if self.main.interpolation: |
| if isinstance(val, basestring): |
| return self._interpolate(key, val) |
| if isinstance(val, list): |
| def _check(entry): |
| if isinstance(entry, basestring): |
| return self._interpolate(key, entry) |
| return entry |
| new = [_check(entry) for entry in val] |
| if new != val: |
| return new |
| return val |
| |
| |
| def __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. |
| """ |
| if not isinstance(key, basestring): |
| raise ValueError('The key "%s" is not a string.' % key) |
| |
| # add the comment |
| if key not in self.comments: |
| self.comments[key] = [] |
| self.inline_comments[key] = '' |
| # remove the entry from defaults |
| if key in self.defaults: |
| self.defaults.remove(key) |
| # |
| if isinstance(value, Section): |
| if key not in self: |
| self.sections.append(key) |
| dict.__setitem__(self, key, value) |
| elif isinstance(value, dict) and not unrepr: |
| # First create the new depth level, |
| # then create the section |
| if key not in self: |
| self.sections.append(key) |
| new_depth = self.depth + 1 |
| dict.__setitem__( |
| self, |
| key, |
| Section( |
| self, |
| new_depth, |
| self.main, |
| indict=value, |
| name=key)) |
| else: |
| if key not in self: |
| self.scalars.append(key) |
| if not self.main.stringify: |
| if isinstance(value, basestring): |
| pass |
| elif isinstance(value, (list, tuple)): |
| for entry in value: |
| if not isinstance(entry, basestring): |
| raise TypeError('Value is not a string "%s".' % entry) |
| else: |
| raise TypeError('Value is not a string "%s".' % value) |
| dict.__setitem__(self, key, value) |
| |
| |
| def __delitem__(self, key): |
| """Remove items from the sequence when deleting.""" |
| dict. __delitem__(self, key) |
| if key in self.scalars: |
| self.scalars.remove(key) |
| else: |
| self.sections.remove(key) |
| del self.comments[key] |
| del self.inline_comments[key] |
| |
| |
| def get(self, key, default=None): |
| """A version of ``get`` that doesn't bypass string interpolation.""" |
| try: |
| return self[key] |
| except KeyError: |
| return default |
| |
| |
| def update(self, indict): |
| """ |
| A version of update that uses our ``__setitem__``. |
| """ |
| for entry in indict: |
| self[entry] = indict[entry] |
| |
| |
| def pop(self, key, default=MISSING): |
| """ |
| 'D.pop(k[,d]) -> v, remove specified key and return the corresponding value. |
| If key is not found, d is returned if given, otherwise KeyError is raised' |
| """ |
| try: |
| val = self[key] |
| except KeyError: |
| if default is MISSING: |
| raise |
| val = default |
| else: |
| del self[key] |
| return val |
| |
| |
| def popitem(self): |
| """Pops the first (key,val)""" |
| sequence = (self.scalars + self.sections) |
| if not sequence: |
| raise KeyError(": 'popitem(): dictionary is empty'") |
| key = sequence[0] |
| val = self[key] |
| del self[key] |
| return key, val |
| |
| |
| def clear(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 = None |
| self.defaults = [] |
| self.extra_values = [] |
| |
| |
| def setdefault(self, key, default=None): |
| """A version of setdefault that sets sequence if appropriate.""" |
| try: |
| return self[key] |
| except KeyError: |
| self[key] = default |
| return self[key] |
| |
| |
| def items(self): |
| """D.items() -> list of D's (key, value) pairs, as 2-tuples""" |
| return zip((self.scalars + self.sections), self.values()) |
| |
| |
| def keys(self): |
| """D.keys() -> list of D's keys""" |
| return (self.scalars + self.sections) |
| |
| |
| def values(self): |
| """D.values() -> list of D's values""" |
| return [self[key] for key in (self.scalars + self.sections)] |
| |
| |
| def iteritems(self): |
| """D.iteritems() -> an iterator over the (key, value) items of D""" |
| return iter(self.items()) |
| |
| |
| def iterkeys(self): |
| """D.iterkeys() -> an iterator over the keys of D""" |
| return iter((self.scalars + self.sections)) |
| |
| __iter__ = iterkeys |
| |
| |
| def itervalues(self): |
| """D.itervalues() -> an iterator over the values of D""" |
| return iter(self.values()) |
| |
| |
| def __repr__(self): |
| """x.__repr__() <==> repr(x)""" |
| def _getval(key): |
| try: |
| return self[key] |
| except MissingInterpolationOption: |
| return dict.__getitem__(self, key) |
| return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) |
| for key in (self.scalars + self.sections)]) |
| |
| __str__ = __repr__ |
| __str__.__doc__ = "x.__str__() <==> str(x)" |
| |
| |
| # Extra methods - not in a normal dictionary |
| |
| def dict(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 = {} |
| for entry in self: |
| this_entry = self[entry] |
| if isinstance(this_entry, Section): |
| this_entry = this_entry.dict() |
| elif isinstance(this_entry, list): |
| # create a copy rather than a reference |
| this_entry = list(this_entry) |
| elif isinstance(this_entry, tuple): |
| # create a copy rather than a reference |
| this_entry = tuple(this_entry) |
| newdict[entry] = this_entry |
| return newdict |
| |
| |
| def merge(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 |
| ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}}) |
| """ |
| for key, val in indict.items(): |
| if (key in self and isinstance(self[key], dict) and |
| isinstance(val, dict)): |
| self[key].merge(val) |
| else: |
| self[key] = val |
| |
| |
| def rename(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. |
| """ |
| if oldkey in self.scalars: |
| the_list = self.scalars |
| elif oldkey in self.sections: |
| the_list = self.sections |
| else: |
| raise KeyError('Key "%s" not found.' % oldkey) |
| pos = 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] |
| del self.comments[oldkey] |
| del self.inline_comments[oldkey] |
| self.comments[newkey] = comm |
| self.inline_comments[newkey] = inline_comment |
| |
| |
| def walk(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 it's 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. |
| |
| .. admonition:: 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 |
| ConfigObj({'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 |
| ConfigObj({'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}}) |
| """ |
| out = {} |
| # scalars first |
| for i in range(len(self.scalars)): |
| entry = self.scalars[i] |
| try: |
| val = function(self, entry, **keywargs) |
| # bound again in case name has changed |
| entry = self.scalars[i] |
| out[entry] = val |
| except Exception: |
| if raise_errors: |
| raise |
| else: |
| entry = self.scalars[i] |
| out[entry] = False |
| # then sections |
| for i in range(len(self.sections)): |
| entry = self.sections[i] |
| if call_on_sections: |
| try: |
| function(self, entry, **keywargs) |
| except Exception: |
| if raise_errors: |
| raise |
| else: |
| entry = self.sections[i] |
| out[entry] = False |
| # bound again in case name has changed |
| entry = self.sections[i] |
| # previous result is discarded |
| out[entry] = self[entry].walk( |
| function, |
| raise_errors=raise_errors, |
| call_on_sections=call_on_sections, |
| **keywargs) |
| return out |
| |
| |
| def as_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] |
| if val == True: |
| return True |
| elif val == False: |
| return False |
| else: |
| try: |
| if not isinstance(val, basestring): |
| # TODO: Why do we raise a KeyError here? |
| raise KeyError() |
| else: |
| return self.main._bools[val.lower()] |
| except KeyError: |
| raise ValueError('Value "%s" is neither True nor False' % val) |
| |
| |
| def as_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() with base 10: '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() with base 10: '3.2' |
| """ |
| return int(self[key]) |
| |
| |
| def as_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 |
| """ |
| return float(self[key]) |
| |
| |
| def as_list(self, key): |
| """ |
| A convenience method which fetches the specified value, guaranteeing |
| that it is a list. |
| |
| >>> a = ConfigObj() |
| >>> a['a'] = 1 |
| >>> a.as_list('a') |
| [1] |
| >>> a['a'] = (1,) |
| >>> a.as_list('a') |
| [1] |
| >>> a['a'] = [1] |
| >>> a.as_list('a') |
| [1] |
| """ |
| result = self[key] |
| if isinstance(result, (tuple, list)): |
| return list(result) |
| return [result] |
| |
| |
| def restore_default(self, key): |
| """ |
| Restore (and return) default value for the specified key. |
| |
| This method will only work for a ConfigObj that was created |
| with a configspec and has been validated. |
| |
| If there is no default value for this key, ``KeyError`` is raised. |
| """ |
| default = self.default_values[key] |
| dict.__setitem__(self, key, default) |
| if key not in self.defaults: |
| self.defaults.append(key) |
| return default |
| |
| |
| def restore_defaults(self): |
| """ |
| Recursively restore default values to all members |
| that have them. |
| |
| This method will only work for a ConfigObj that was created |
| with a configspec and has been validated. |
| |
| It doesn't delete or modify entries without default values. |
| """ |
| for key in self.default_values: |
| self.restore_default(key) |
| |
| for section in self.sections: |
| self[section].restore_defaults() |
| |
| |
| class ConfigObj(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, configspec=None, encoding=None, |
| interpolation=True, raise_errors=False, list_values=True, |
| create_empty=False, file_error=False, stringify=True, |
| indent_type=None, default_encoding=None, unrepr=False, |
| write_empty_values=False, _inspec=False): |
| """ |
| Parse a config file or create a config file object. |
| |
| ``ConfigObj(infile=None, configspec=None, encoding=None, |
| interpolation=True, raise_errors=False, list_values=True, |
| create_empty=False, file_error=False, stringify=True, |
| indent_type=None, default_encoding=None, unrepr=False, |
| write_empty_values=False, _inspec=False)`` |
| """ |
| self._inspec = _inspec |
| # init the superclass |
| Section.__init__(self, self, 0, self) |
| |
| infile = infile or [] |
| |
| _options = {'configspec': configspec, |
| 'encoding': encoding, 'interpolation': interpolation, |
| 'raise_errors': raise_errors, 'list_values': list_values, |
| 'create_empty': create_empty, 'file_error': file_error, |
| 'stringify': stringify, 'indent_type': indent_type, |
| 'default_encoding': default_encoding, 'unrepr': unrepr, |
| 'write_empty_values': write_empty_values} |
| |
| if options is None: |
| options = _options |
| else: |
| import warnings |
| warnings.warn('Passing in an options dictionary to ConfigObj() is ' |
| 'deprecated. Use **options instead.', |
| DeprecationWarning, stacklevel=2) |
| |
| # TODO: check the values too. |
| for entry in options: |
| if entry not in OPTION_DEFAULTS: |
| raise TypeError('Unrecognised option "%s".' % entry) |
| for entry, value in OPTION_DEFAULTS.items(): |
| if entry not in options: |
| options[entry] = value |
| keyword_value = _options[entry] |
| if value != keyword_value: |
| options[entry] = keyword_value |
| |
| # XXXX this ignores an explicit list_values = True in combination |
| # with _inspec. The user should *never* do that anyway, but still... |
| if _inspec: |
| options['list_values'] = False |
| |
| self._initialise(options) |
| configspec = options['configspec'] |
| self._original_configspec = configspec |
| self._load(infile, configspec) |
| |
| |
| def _load(self, infile, configspec): |
| if isinstance(infile, basestring): |
| self.filename = infile |
| if os.path.isfile(infile): |
| h = open(infile, 'rb') |
| infile = h.read() or [] |
| h.close() |
| elif self.file_error: |
| # raise an error if the file doesn't exist |
| raise IOError('Config file not found: "%s".' % self.filename) |
| else: |
| # file doesn't already exist |
| if self.create_empty: |
| # this is a good test that the filename specified |
| # isn't impossible - like on a non-existent device |
| h = open(infile, 'w') |
| h.write('') |
| h.close() |
| infile = [] |
| |
| elif isinstance(infile, (list, tuple)): |
| infile = list(infile) |
| |
| elif isinstance(infile, dict): |
| # initialise self |
| # the Section class handles creating subsections |
| if isinstance(infile, ConfigObj): |
| # get a copy of our ConfigObj |
| def set_section(in_section, this_section): |
| for entry in in_section.scalars: |
| this_section[entry] = in_section[entry] |
| for section in in_section.sections: |
| this_section[section] = {} |
| set_section(in_section[section], this_section[section]) |
| set_section(infile, self) |
| |
| else: |
| for entry in infile: |
| self[entry] = infile[entry] |
| del self._errors |
| |
| if configspec is not None: |
| self._handle_configspec(configspec) |
| else: |
| self.configspec = None |
| return |
| |
| elif getattr(infile, 'read', MISSING) is not MISSING: |
| # This supports file like objects |
| infile = infile.read() or [] |
| # needs splitting into lines - but needs doing *after* decoding |
| # in case it's not an 8 bit encoding |
| else: |
| raise TypeError('infile must be a filename, file like object, or list of lines.') |
| |
| if infile: |
| # don't do it for the empty ConfigObj |
| infile = 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 lines |
| for line in infile: |
| if (not line) or (line[-1] not in ('\r', '\n', '\r\n')): |
| continue |
| for end in ('\r\n', '\n', '\r'): |
| if line.endswith(end): |
| self.newlines = end |
| break |
| break |
| |
| infile = [line.rstrip('\r\n') for line in infile] |
| |
| self._parse(infile) |
| # if we had any errors, now is the time to raise them |
| if self._errors: |
| info = "at line %s." % self._errors[0].line_number |
| if len(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 attribute |
| error.config = self |
| raise error |
| # delete private attributes |
| del self._errors |
| |
| if configspec is None: |
| self.configspec = None |
| else: |
| self._handle_configspec(configspec) |
| |
| |
| def _initialise(self, options=None): |
| if options is None: |
| options = OPTION_DEFAULTS |
| |
| # initialise a few variables |
| self.filename = None |
| self._errors = [] |
| self.raise_errors = options['raise_errors'] |
| self.interpolation = options['interpolation'] |
| self.list_values = options['list_values'] |
| self.create_empty = options['create_empty'] |
| self.file_error = options['file_error'] |
| self.stringify = options['stringify'] |
| self.indent_type = options['indent_type'] |
| self.encoding = options['encoding'] |
| self.default_encoding = options['default_encoding'] |
| self.BOM = False |
| self.newlines = None |
| self.write_empty_values = options['write_empty_values'] |
| self.unrepr = options['unrepr'] |
| |
| self.initial_comment = [] |
| self.final_comment = [] |
| self.configspec = None |
| |
| if self._inspec: |
| self.list_values = False |
| |
| # Clear section attributes as well |
| Section._initialise(self) |
| |
| |
| def __repr__(self): |
| def _getval(key): |
| try: |
| return self[key] |
| except MissingInterpolationOption: |
| return dict.__getitem__(self, key) |
| return ('ConfigObj({%s})' % |
| ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) |
| for key in (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.encoding is not None) and |
| (self.encoding.lower() not in BOM_LIST)): |
| # No need to check for a BOM |
| # the encoding specified doesn't have one |
| # just decode |
| return self._decode(infile, self.encoding) |
| |
| if isinstance(infile, (list, tuple)): |
| line = infile[0] |
| else: |
| line = infile |
| if self.encoding is not None: |
| # 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()] |
| if enc == 'utf_16': |
| # For UTF16 we try big endian and little endian |
| for BOM, (encoding, final_encoding) in BOMS.items(): |
| if not final_encoding: |
| # skip UTF8 |
| continue |
| if infile.startswith(BOM): |
| ### BOM discovered |
| ##self.BOM = True |
| # Don't need to remove BOM |
| return self._decode(infile, encoding) |
| |
| # If we get this far, will *probably* raise a DecodeError |
| # As it doesn't appear to start with a BOM |
| return self._decode(infile, self.encoding) |
| |
| # Must be UTF8 |
| BOM = BOM_SET[enc] |
| if not line.startswith(BOM): |
| return self._decode(infile, self.encoding) |
| |
| newline = line[len(BOM):] |
| |
| # BOM removed |
| if isinstance(infile, (list, tuple)): |
| infile[0] = newline |
| else: |
| infile = newline |
| self.BOM = True |
| return self._decode(infile, self.encoding) |
| |
| # No encoding specified - so we need to check for UTF8/UTF16 |
| for BOM, (encoding, final_encoding) in BOMS.items(): |
| if not line.startswith(BOM): |
| continue |
| else: |
| # BOM discovered |
| self.encoding = final_encoding |
| if not final_encoding: |
| self.BOM = True |
| # UTF8 |
| # remove BOM |
| newline = line[len(BOM):] |
| if isinstance(infile, (list, tuple)): |
| infile[0] = newline |
| else: |
| infile = newline |
| # UTF8 - don't decode |
| if isinstance(infile, basestring): |
| return infile.splitlines(True) |
| else: |
| return infile |
| # UTF16 - have to decode |
| return self._decode(infile, encoding) |
| |
| # No BOM discovered and no encoding specified, just return |
| if isinstance(infile, basestring): |
| # infile read from a file will be a single string |
| return infile.splitlines(True) |
| return infile |
| |
| |
| def _a_to_u(self, aString): |
| """Decode ASCII strings to unicode if a self.encoding is specified.""" |
| if self.encoding: |
| return aString.decode('ascii') |
| else: |
| return aString |
| |
| |
| def _decode(self, infile, encoding): |
| """ |
| Decode infile to unicode. Using the specified encoding. |
| |
| if is a string, it also needs converting to a list. |
| """ |
| if isinstance(infile, basestring): |
| # can't be unicode |
| # NOTE: Could raise a ``UnicodeDecodeError`` |
| return infile.decode(encoding).splitlines(True) |
| for i, line in enumerate(infile): |
| if not isinstance(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) |
| return infile |
| |
| |
| def _decode_element(self, line): |
| """Decode element to unicode if necessary.""" |
| if not self.encoding: |
| return line |
| if isinstance(line, str) and self.default_encoding: |
| return line.decode(self.default_encoding) |
| return line |
| |
| |
| def _str(self, value): |
| """ |
| Used by ``stringify`` within validate, to turn non-string values |
| into strings. |
| """ |
| if not isinstance(value, basestring): |
| return str(value) |
| else: |
| return value |
| |
| |
| def _parse(self, infile): |
| """Actually parse the config file.""" |
| temp_list_values = self.list_values |
| if self.unrepr: |
| self.list_values = False |
| |
| comment_list = [] |
| done_start = False |
| this_section = self |
| maxline = len(infile) - 1 |
| cur_index = -1 |
| reset_comment = False |
| |
| while cur_index < maxline: |
| if reset_comment: |
| comment_list = [] |
| cur_index += 1 |
| line = infile[cur_index] |
| sline = line.strip() |
| # do we have anything on the line ? |
| if not sline or sline.startswith('#'): |
| reset_comment = False |
| comment_list.append(line) |
| continue |
| |
| if not done_start: |
| # preserve initial comment |
| self.initial_comment = comment_list |
| comment_list = [] |
| done_start = True |
| |
| reset_comment = True |
| # first we check if it's a section marker |
| mat = self._sectionmarker.match(line) |
| if mat is not None: |
| # is a section line |
| (indent, sect_open, sect_name, sect_close, comment) = mat.groups() |
| if indent and (self.indent_type is None): |
| self.indent_type = indent |
| cur_depth = sect_open.count('[') |
| if cur_depth != sect_close.count(']'): |
| self._handle_error("Cannot compute the section depth at line %s.", |
| NestingError, infile, cur_index) |
| continue |
| |
| if cur_depth < this_section.depth: |
| # the new section is dropping back to a previous level |
| try: |
| parent = self._match_depth(this_section, |
| cur_depth).parent |
| except SyntaxError: |
| self._handle_error("Cannot compute nesting level at line %s.", |
| NestingError, infile, cur_index) |
| continue |
| elif cur_depth == this_section.depth: |
| # the new section is a sibling of the current section |
| parent = this_section.parent |
| elif cur_depth == this_section.depth + 1: |
| # the new section is a child the current section |
| parent = this_section |
| else: |
| self._handle_error("Section too nested at line %s.", |
| NestingError, infile, cur_index) |
| |
| sect_name = self._unquote(sect_name) |
| if sect_name in parent: |
| self._handle_error('Duplicate section name at line %s.', |
| DuplicateError, infile, cur_index) |
| continue |
| |
| # create the new section |
| this_section = Section( |
| parent, |
| cur_depth, |
| self, |
| name=sect_name) |
| parent[sect_name] = this_section |
| parent.inline_comments[sect_name] = comment |
| parent.comments[sect_name] = comment_list |
| continue |
| # |
| # it's not a section marker, |
| # so it should be a valid ``key = value`` line |
| mat = self._keyword.match(line) |
| if mat is None: |
| # it neither matched as a keyword |
| # or a section marker |
| self._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() |
| if indent and (self.indent_type is None): |
| self.indent_type = indent |
| # check for a multiline value |
| if value[:3] in ['"""', "'''"]: |
| try: |
| value, comment, cur_index = self._multiline( |
| value, infile, cur_index, maxline) |
| except SyntaxError: |
| self._handle_error( |
| 'Parse error in value at line %s.', |
| ParseError, infile, cur_index) |
| continue |
| else: |
| if self.unrepr: |
| comment = '' |
| try: |
| value = unrepr(value) |
| except Exception, e: |
| if type(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) |
| continue |
| else: |
| if self.unrepr: |
| comment = '' |
| try: |
| value = unrepr(value) |
| except Exception, e: |
| if isinstance(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) |
| continue |
| else: |
| # extract comment and lists |
| try: |
| (value, comment) = self._handle_value(value) |
| except SyntaxError: |
| self._handle_error( |
| 'Parse error in value at line %s.', |
| ParseError, infile, cur_index) |
| continue |
| # |
| key = self._unquote(key) |
| if key in this_section: |
| 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 section |
| this_section.__setitem__(key, value, unrepr=True) |
| this_section.inline_comments[key] = comment |
| this_section.comments[key] = comment_list |
| continue |
| # |
| if self.indent_type is None: |
| # no indentation used, set the type accordingly |
| self.indent_type = '' |
| |
| # preserve the final comment |
| if not self and not self.initial_comment: |
| self.initial_comment = comment_list |
| elif not reset_comment: |
| self.final_comment = comment_list |
| self.list_values = temp_list_values |
| |
| |
| def _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. |
| """ |
| while depth < sect.depth: |
| if sect is sect.parent: |
| # we've reached the top level already |
| raise SyntaxError() |
| sect = sect.parent |
| if sect.depth == depth: |
| return sect |
| # shouldn't get here |
| raise SyntaxError() |
| |
| |
| def _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 occured at ``cur_index`` |
| """ |
| line = infile[cur_index] |
| cur_index += 1 |
| message = text % cur_index |
| error = ErrorClass(message, cur_index, line) |
| if self.raise_errors: |
| # raise the error - parsing stops here |
| raise error |
| # store the error |
| # reraise when parsing has finished |
| self._errors.append(error) |
| |
| |
| def _unquote(self, value): |
| """Return an unquoted version of a value""" |
| if not value: |
| # should only happen during parsing of lists |
| raise SyntaxError |
| if (value[0] == value[-1]) and (value[0] in ('"', "'")): |
| value = value[1:-1] |
| return value |
| |
| |
| def _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) or '#'. |
| |
| If ``write_empty_values`` is set, and the value is an empty string, it |
| won't be quoted. |
| """ |
| if multiline and self.write_empty_values and value == '': |
| # Only if multiline is set, so that it is used for values not |
| # keys, and not values that are part of a list |
| return '' |
| |
| if multiline and isinstance(value, (list, tuple)): |
| if not value: |
| return ',' |
| elif len(value) == 1: |
| return self._quote(value[0], multiline=False) + ',' |
| return ', '.join([self._quote(val, multiline=False) |
| for val in value]) |
| if not isinstance(value, basestring): |
| if self.stringify: |
| value = str(value) |
| else: |
| raise TypeError('Value "%s" is not a string.' % value) |
| |
| if not value: |
| return '""' |
| |
| no_lists_no_quotes = not self.list_values and '\n' not in value and '#' not in value |
| need_triple = multiline and ((("'" in value) and ('"' in value)) or ('\n' in value )) |
| hash_triple_quote = multiline and not need_triple and ("'" in value) and ('"' in value) and ('#' in value) |
| check_for_single = (no_lists_no_quotes or not need_triple) and not hash_triple_quote |
| |
| if check_for_single: |
| if not self.list_values: |
| # we don't quote if ``list_values=False`` |
| quot = noquot |
| # for normal values either single or double quotes will do |
| elif '\n' in value: |
| # will only happen if multiline is off - e.g. '\n' in key |
| raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) |
| elif ((value[0] not in wspace_plus) and |
| (value[-1] not in wspace_plus) and |
| (',' not in value)): |
| quot = noquot |
| else: |
| quot = self._get_single_quote(value) |
| else: |
| # if value has '\n' or "'" *and* '"', it will need triple quotes |
| quot = self._get_triple_quote(value) |
| |
| if quot == noquot and '#' in value and self.list_values: |
| quot = self._get_single_quote(value) |
| |
| return quot % value |
| |
| |
| def _get_single_quote(self, value): |
| if ("'" in value) and ('"' in value): |
| raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) |
| elif '"' in value: |
| quot = squot |
| else: |
| quot = dquot |
| return quot |
| |
| |
| def _get_triple_quote(self, value): |
| if (value.find('"""') != -1) and (value.find("'''") != -1): |
| raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) |
| if value.find('"""') == -1: |
| quot = tdquot |
| else: |
| quot = tsquot |
| return quot |
| |
| |
| def _handle_value(self, value): |
| """ |
| Given a value string, unquote, remove comment, |
| handle lists. (including empty and single member lists) |
| """ |
| if self._inspec: |
| # Parsing a configspec so don't handle comments |
| return (value, '') |
| # do we look for lists in values ? |
| if not self.list_values: |
| mat = self._nolistvalue.match(value) |
| if mat is None: |
| raise SyntaxError() |
| # NOTE: we don't unquote here |
| return mat.groups() |
| # |
| mat = self._valueexp.match(value) |
| if mat is None: |
| # the value is badly constructed, probably badly quoted, |
| # or an invalid list |
| raise SyntaxError() |
| (list_values, single, empty_list, comment) = mat.groups() |
| if (list_values == '') and (single is None): |
| # change this if you want to accept empty values |
| raise SyntaxError() |
| # NOTE: note there is no error handling from here if the regex |
| # is wrong: then incorrect values will slip through |
| if empty_list is not None: |
| # the single comma - meaning an empty list |
| return ([], comment) |
| if single is not None: |
| # handle empty values |
| if list_values and not single: |
| # FIXME: the '' is a workaround because our regex now matches |
| # '' at the end of a list if it has a trailing comma |
| single = None |
| else: |
| single = single or '""' |
| single = self._unquote(single) |
| if list_values == '': |
| # not a list value |
| return (single, comment) |
| the_list = self._listvalueexp.findall(list_values) |
| the_list = [self._unquote(val) for val in the_list] |
| if single is not None: |
| 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) |
| if mat is not None: |
| retval = list(mat.groups()) |
| retval.append(cur_index) |
| return retval |
| elif newvalue.find(quot) != -1: |
| # somehow the triple quote is missing |
| raise SyntaxError() |
| # |
| while cur_index < maxline: |
| cur_index += 1 |
| newvalue += '\n' |
| line = infile[cur_index] |
| if line.find(quot) == -1: |
| newvalue += line |
| else: |
| # end of multiline, process it |
| break |
| else: |
| # we've got to the end of the config, oops... |
| raise SyntaxError() |
| mat = multi_line.match(line) |
| if mat is None: |
| # a badly formed line |
| raise SyntaxError() |
| (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``) |
| if not isinstance(configspec, ConfigObj): |
| try: |
| configspec = ConfigObj(configspec, |
| raise_errors=True, |
| file_error=True, |
| _inspec=True) |
| except ConfigObjError, e: |
| # FIXME: Should these errors have a reference |
| # to the already parsed ConfigObj ? |
| raise ConfigspecError('Parsing configspec failed: %s' % e) |
| except IOError, e: |
| raise IOError('Reading configspec failed: %s' % e) |
| |
| self.configspec = configspec |
| |
| |
| |
| def _set_configspec(self, section, copy): |
| """ |
| Called by validate. Handles setting the configspec on subsections |
| including sections to be validated by __many__ |
| """ |
| configspec = section.configspec |
| many = configspec.get('__many__') |
| if isinstance(many, dict): |
| for entry in section.sections: |
| if entry not in configspec: |
| section[entry].configspec = many |
| |
| for entry in configspec.sections: |
| if entry == '__many__': |
| continue |
| if entry not in section: |
| section[entry] = {} |
| section[entry]._created = True |
| if copy: |
| # copy comments |
| section.comments[entry] = configspec.comments.get(entry, []) |
| section.inline_comments[entry] = configspec.inline_comments.get(entry, '') |
| |
| # Could be a scalar when we expect a section |
| if isinstance(section[entry], Section): |
| section[entry].configspec = configspec[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. |
| if not self.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.""" |
| if not comment: |
| return '' |
| start = self.indent_type |
| if not comment.startswith('#'): |
| start += self._a_to_u(' # ') |
| return (start + comment) |
| |
| |
| # Public methods |
| |
| def write(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 |
| >>> import os |
| >>> os.remove('test.ini') |
| """ |
| if self.indent_type is None: |
| # this can be true if initialised from a dictionary |
| self.indent_type = DEFAULT_INDENT_TYPE |
| |
| out = [] |
| cs = self._a_to_u('#') |
| csp = self._a_to_u('# ') |
| if section is None: |
| int_val = self.interpolation |
| self.interpolation = False |
| section = self |
| for line in self.initial_comment: |
| line = self._decode_element(line) |
| stripped_line = line.strip() |
| if stripped_line and not stripped_line.startswith(cs): |
| line = csp + line |
| out.append(line) |
| |
| indent_string = self.indent_type * section.depth |
| for entry in (section.scalars + section.sections): |
| if entry in section.defaults: |
| # don't write out default values |
| continue |
| for comment_line in section.comments[entry]: |
| comment_line = self._decode_element(comment_line.lstrip()) |
| if comment_line and not comment_line.startswith(cs): |
| comment_line = csp + comment_line |
| out.append(indent_string + comment_line) |
| this_entry = section[entry] |
| comment = self._handle_comment(section.inline_comments[entry]) |
| |
| if isinstance(this_entry, dict): |
| # a section |
| out.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)) |
| |
| if section is self: |
| for line in self.final_comment: |
| line = self._decode_element(line) |
| stripped_line = line.strip() |
| if stripped_line and not stripped_line.startswith(cs): |
| line = csp + line |
| out.append(line) |
| self.interpolation = int_val |
| |
| if section is not self: |
| return out |
| |
| if (self.filename is None) and (outfile is None): |
| # output a list of lines |
| # might need to encode |
| # NOTE: This will *screw* UTF16, each line will start with the BOM |
| if self.encoding: |
| out = [l.encode(self.encoding) for l in out] |
| if (self.BOM and ((self.encoding is None) or |
| (BOM_LIST.get(self.encoding.lower()) == 'utf_8'))): |
| # Add the UTF8 BOM |
| if not out: |
| out.append('') |
| out[0] = BOM_UTF8 + out[0] |
| return out |
| |
| # Turn the list to a string, joined with correct newlines |
| newline = self.newlines or os.linesep |
| if (getattr(outfile, 'mode', None) is not None and outfile.mode == 'w' |
| and sys.platform == 'win32' and newline == '\r\n'): |
| # Windows specific hack to avoid writing '\r\r\n' |
| newline = '\n' |
| output = self._a_to_u(newline).join(out) |
| if self.encoding: |
| output = output.encode(self.encoding) |
| if self.BOM and ((self.encoding is None) or match_utf8(self.encoding)): |
| # Add the UTF8 BOM |
| output = BOM_UTF8 + output |
| |
| if not output.endswith(newline): |
| output += newline |
| if outfile is not None: |
| outfile.write(output) |
| else: |
| h = open(self.filename, 'wb') |
| h.write(output) |
| h.close() |
| |
| |
| def validate(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`` indicates 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. |
| """ |
| if section is None: |
| if self.configspec is None: |
| raise ValueError('No configspec supplied.') |
| if preserve_errors: |
| # We do this once to remove a top level dependency on the validate module |
| # Which makes importing configobj faster |
| from validate import VdtMissingValue |
| self._vdtMissingValue = VdtMissingValue |
| |
| section = self |
| |
| if copy: |
| section.initial_comment = section.configspec.initial_comment |
| section.final_comment = section.configspec.final_comment |
| section.encoding = section.configspec.encoding |
| section.BOM = section.configspec.BOM |
| section.newlines = section.configspec.newlines |
| section.indent_type = section.configspec.indent_type |
| |
| # |
| # section.default_values.clear() #?? |
| configspec = section.configspec |
| self._set_configspec(section, copy) |
| |
| |
| def validate_entry(entry, spec, val, missing, ret_true, ret_false): |
| section.default_values.pop(entry, None) |
| |
| try: |
| section.default_values[entry] = validator.get_default_value(configspec[entry]) |
| except (KeyError, AttributeError, validator.baseErrorClass): |
| # No default, bad default or validator has no 'get_default_value' |
| # (e.g. SimpleVal) |
| pass |
| |
| try: |
| check = validator.check(spec, |
| val, |
| missing=missing |
| ) |
| except validator.baseErrorClass, e: |
| if not preserve_errors or isinstance(e, self._vdtMissingValue): |
| out[entry] = False |
| else: |
| # preserve the error |
| out[entry] = e |
| ret_false = False |
| ret_true = False |
| else: |
| ret_false = False |
| out[entry] = True |
| if self.stringify or missing: |
| # if we are doing type conversion |
| # or the value is a supplied default |
| if not self.stringify: |
| if isinstance(check, (list, tuple)): |
| # preserve lists |
| check = [self._str(item) for item in check] |
| elif missing and check is None: |
| # convert the None from a default to a '' |
| check = '' |
| else: |
| check = self._str(check) |
| if (check != val) or missing: |
| section[entry] = check |
| if not copy and missing and entry not in section.defaults: |
| section.defaults.append(entry) |
| return ret_true, ret_false |
| |
| # |
| out = {} |
| ret_true = True |
| ret_false = True |
| |
| unvalidated = [k for k in section.scalars if k not in configspec] |
| incorrect_sections = [k for k in configspec.sections if k in section.scalars] |
| incorrect_scalars = [k for k in configspec.scalars if k in section.sections] |
| |
| for entry in configspec.scalars: |
| if entry in ('__many__', '___many___'): |
| # reserved names |
| continue |
| if (not entry in section.scalars) or (entry in section.defaults): |
| # missing entries |
| # or entries from defaults |
| missing = True |
| val = None |
| if copy and entry not in section.scalars: |
| # copy comments |
| section.comments[entry] = ( |
| configspec.comments.get(entry, [])) |
| section.inline_comments[entry] = ( |
| configspec.inline_comments.get(entry, '')) |
| # |
| else: |
| missing = False |
| val = section[entry] |
| |
| ret_true, ret_false = validate_entry(entry, configspec[entry], val, |
| missing, ret_true, ret_false) |
| |
| many = None |
| if '__many__' in configspec.scalars: |
| many = configspec['__many__'] |
| elif '___many___' in configspec.scalars: |
| many = configspec['___many___'] |
| |
| if many is not None: |
| for entry in unvalidated: |
| val = section[entry] |
| ret_true, ret_false = validate_entry(entry, many, val, False, |
| ret_true, ret_false) |
| unvalidated = [] |
| |
| for entry in incorrect_scalars: |
| ret_true = False |
| if not preserve_errors: |
| out[entry] = False |
| else: |
| ret_false = False |
| msg = 'Value %r was provided as a section' % entry |
| out[entry] = validator.baseErrorClass(msg) |
| for entry in incorrect_sections: |
| ret_true = False |
| if not preserve_errors: |
| out[entry] = False |
| else: |
| ret_false = False |
| msg = 'Section %r was provided as a single value' % entry |
| out[entry] = validator.baseErrorClass(msg) |
| |
| # Missing sections will have been created as empty ones when the |
| # configspec was read. |
| for entry in section.sections: |
| # FIXME: this means DEFAULT is not copied in copy mode |
| if section is self and entry == 'DEFAULT': |
| continue |
| if section[entry].configspec is None: |
| unvalidated.append(entry) |
| continue |
| if copy: |
| section.comments[entry] = configspec.comments.get(entry, []) |
| section.inline_comments[entry] = configspec.inline_comments.get(entry, '') |
| check = self.validate(validator, preserve_errors=preserve_errors, copy=copy, section=section[entry]) |
| out[entry] = check |
| if check == False: |
| ret_true = False |
| elif check == True: |
| ret_false = False |
| else: |
| ret_true = False |
| |
| section.extra_values = unvalidated |
| if preserve_errors and not section._created: |
| # If the section wasn't created (i.e. it wasn't missing) |
| # then we can't return False, we need to preserve errors |
| ret_false = False |
| # |
| if ret_false and preserve_errors and out: |
| # If we are preserving errors, but all |
| # the failures are from missing sections / values |
| # then we can return False. Otherwise there is a |
| # real failure that we need to preserve. |
| ret_false = not any(out.values()) |
| if ret_true: |
| return True |
| elif ret_false: |
| return False |
| return out |
| |
| |
| def reset(self): |
| """Clear ConfigObj instance and restore to 'freshly created' state.""" |
| self.clear() |
| self._initialise() |
| # FIXME: Should be done by '_initialise', but ConfigObj constructor (and reload) |
| # requires an empty dictionary |
| self.configspec = None |
| # Just to be sure ;-) |
| self._original_configspec = None |
| |
| |
| def reload(self): |
| """ |
| Reload a ConfigObj from file. |
| |
| This method raises a ``ReloadError`` if the ConfigObj doesn't have |
| a filename attribute pointing to a file. |
| """ |
| if not isinstance(self.filename, basestring): |
| raise ReloadError() |
| |
| filename = self.filename |
| current_options = {} |
| for entry in OPTION_DEFAULTS: |
| if entry == 'configspec': |
| continue |
| current_options[entry] = getattr(self, entry) |
| |
| configspec = self._original_configspec |
| current_options['configspec'] = configspec |
| |
| self.clear() |
| self._initialise(current_options) |
| self._load(filename, configspec) |
| |
| |
| |
| class SimpleVal(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 = ConfigObjError |
| |
| def check(self, check, member, missing=False): |
| """A dummy check method, always returns the value unchanged.""" |
| if missing: |
| raise self.baseErrorClass() |
| return member |
| |
| |
| def flatten_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 (or a section was expected and a scalar provided |
| - or vice-versa) 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*. |
| """ |
| if levels is None: |
| # first time called |
| levels = [] |
| results = [] |
| if res == True: |
| return results |
| if res == False or isinstance(res, Exception): |
| results.append((levels[:], None, res)) |
| if levels: |
| levels.pop() |
| return results |
| for (key, val) in res.items(): |
| if val == True: |
| continue |
| if isinstance(cfg.get(key), dict): |
| # Go down one level |
| levels.append(key) |
| flatten_errors(cfg[key], val, levels, results) |
| continue |
| results.append((levels[:], key, val)) |
| # |
| # Go up one level |
| if levels: |
| levels.pop() |
| # |
| return results |
| |
| |
| def get_extra_values(conf, _prepend=()): |
| """ |
| Find all the values and sections not in the configspec from a validated |
| ConfigObj. |
| |
| ``get_extra_values`` returns a list of tuples where each tuple represents |
| either an extra section, or an extra value. |
| |
| The tuples contain two values, a tuple representing the section the value |
| is in and the name of the extra values. For extra values in the top level |
| section the first member will be an empty tuple. For values in the 'foo' |
| section the first member will be ``('foo',)``. For members in the 'bar' |
| subsection of the 'foo' section the first member will be ``('foo', 'bar')``. |
| |
| NOTE: If you call ``get_extra_values`` on a ConfigObj instance that hasn't |
| been validated it will return an empty list. |
| """ |
| out = [] |
| |
| out.extend([(_prepend, name) for name in conf.extra_values]) |
| for name in conf.sections: |
| if name not in conf.extra_values: |
| out.extend(get_extra_values(conf[name], _prepend + (name,))) |
| return out |
| |
| |
| """*A programming language is a medium of expression.* - Paul Graham""" |