diff options
-rw-r--r-- | configobj.py | 2514 | ||||
-rw-r--r-- | docs/configobj.txt | 2863 | ||||
-rw-r--r-- | docs/validate.txt | 735 | ||||
-rw-r--r-- | extras/ConfigPersist.py | 242 | ||||
-rw-r--r-- | extras/configpersist.txt | 266 | ||||
-rw-r--r-- | setup.py | 48 | ||||
-rw-r--r-- | test_configobj.py | 2085 | ||||
-rw-r--r-- | validate.py | 1423 |
8 files changed, 10176 insertions, 0 deletions
diff --git a/configobj.py b/configobj.py new file mode 100644 index 0000000..f28877d --- /dev/null +++ b/configobj.py @@ -0,0 +1,2514 @@ +# configobj.py +# A config file reader/writer that supports nested sections in config files. +# Copyright (C) 2005-2008 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 sys +INTP_VER = sys.version_info[:2] +if INTP_VER < (2, 2): + raise RuntimeError("Python v.2.2 or later needed") + +import os, re +compiler = None +try: + import compiler +except ImportError: + # for IronPython + pass +from types import StringTypes +from warnings import warn +try: + from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE +except ImportError: + # Python 2.2 does not have these + # UTF-8 + BOM_UTF8 = '\xef\xbb\xbf' + # UTF-16, little endian + BOM_UTF16_LE = '\xff\xfe' + # UTF-16, big endian + BOM_UTF16_BE = '\xfe\xff' + if sys.byteorder == 'little': + # UTF-16, native endianness + BOM_UTF16 = BOM_UTF16_LE + else: + # UTF-16, native endianness + BOM_UTF16 = BOM_UTF16_BE + +# A dictionary mapping BOM to +# the encoding to decode with, and what to set the +# encoding attribute to. +BOMS = { + BOM_UTF8: ('utf_8', None), + BOM_UTF16_BE: ('utf16_be', 'utf_16'), + BOM_UTF16_LE: ('utf16_le', 'utf_16'), + BOM_UTF16: ('utf_16', 'utf_16'), + } +# All legal variants of the BOM codecs. +# TODO: the list of aliases is not meant to be exhaustive, is there a +# better way ? +BOM_LIST = { + 'utf_16': 'utf_16', + 'u16': 'utf_16', + 'utf16': 'utf_16', + 'utf-16': 'utf_16', + 'utf16_be': 'utf16_be', + 'utf_16_be': 'utf16_be', + 'utf-16be': 'utf16_be', + 'utf16_le': 'utf16_le', + 'utf_16_le': 'utf16_le', + 'utf-16le': 'utf16_le', + 'utf_8': 'utf_8', + 'u8': 'utf_8', + 'utf': 'utf_8', + 'utf8': 'utf_8', + 'utf-8': 'utf_8', + } + +# Map of encodings to the BOM to write. +BOM_SET = { + 'utf_8': BOM_UTF8, + 'utf_16': BOM_UTF16, + 'utf16_be': BOM_UTF16_BE, + 'utf16_le': BOM_UTF16_LE, + None: BOM_UTF8 + } + + +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'''" + +try: + enumerate +except NameError: + def enumerate(obj): + """enumerate for Python 2.2.""" + i = -1 + for item in obj: + i += 1 + yield i, item + +try: + True, False +except NameError: + True, False = 1, 0 + + +__version__ = '4.5.3' + +__revision__ = '$Id: configobj.py 156 2006-01-31 14:57:08Z fuzzyman $' + +__docformat__ = "restructuredtext en" + +__all__ = ( + '__version__', + 'DEFAULT_INDENT_TYPE', + 'DEFAULT_INTERPOLATION', + 'ConfigObjError', + 'NestingError', + 'ParseError', + 'DuplicateError', + 'ConfigspecError', + 'ConfigObj', + 'SimpleVal', + 'InterpolationError', + 'InterpolationLoopError', + 'MissingInterpolationOption', + 'RepeatSectionError', + 'ReloadError', + 'UnreprError', + 'UnknownType', + '__docformat__', + 'flatten_errors', +) + +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): + s = "a=" + s + if compiler is None: + raise ImportError('compiler module not available') + 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): + InterpolationError.__init__( + self, + 'missing option "%s" in interpolation.' % option) + + +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") + + def __init__(self, section): + # the Section instance that "owns" this engine + self.section = section + + + def interpolate(self, key, 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 backtrail.has_key((key, section.name)): + # 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: + break + # try "DEFAULT" next + val = current_section.get('DEFAULT', {}).get(key) + if val is not None: + 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.""" + _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.""" + _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 = {} + + + 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 and isinstance(val, StringTypes): + return self._interpolate(key, val) + 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, StringTypes): + raise ValueError('The key "%s" is not a string.' % key) + + # add the comment + if not self.comments.has_key(key): + 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 not self.has_key(key): + 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 not self.has_key(key): + 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 not self.has_key(key): + self.scalars.append(key) + if not self.main.stringify: + if isinstance(value, StringTypes): + pass + elif isinstance(value, (list, tuple)): + for entry in value: + if not isinstance(entry, StringTypes): + 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, *args): + """ + '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' + """ + val = dict.pop(self, key, *args) + if key in self.scalars: + del self.comments[key] + del self.inline_comments[key] + self.scalars.remove(key) + elif key in self.sections: + del self.comments[key] + del self.inline_comments[key] + self.sections.remove(key) + if self.main.interpolation and isinstance(val, StringTypes): + return self._interpolate(key, val) + 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 + + + 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)""" + return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(self[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 + {'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 + {'XXXXsection': {'XXXXkey': 'XXXXvalue'}} + >>> def transform(section, key): + ... val = section[key] + ... newkey = key.replace('XXXX', 'CLIENT1') + ... section.rename(key, newkey) + ... if isinstance(val, (tuple, list, dict)): + ... pass + ... else: + ... val = val.replace('XXXX', 'CLIENT1') + ... section[newkey] = val + >>> cfg.walk(transform, call_on_sections=True) + {'CLIENT1section': {'CLIENT1key': None}} + >>> cfg + {'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}} + """ + out = {} + # scalars 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 decode(self, encoding): + """ + Decode all strings and values to unicode, using the specified encoding. + + Works with subsections and list values. + + Uses the ``walk`` method. + + Testing ``encode`` and ``decode``. + >>> m = ConfigObj(a) + >>> m.decode('ascii') + >>> def testuni(val): + ... for entry in val: + ... if not isinstance(entry, unicode): + ... print >> sys.stderr, type(entry) + ... raise AssertionError, 'decode failed.' + ... if isinstance(val[entry], dict): + ... testuni(val[entry]) + ... elif not isinstance(val[entry], unicode): + ... raise AssertionError, 'decode failed.' + >>> testuni(m) + >>> m.encode('ascii') + >>> a == m + 1 + """ + warn('use of ``decode`` is deprecated.', DeprecationWarning) + def decode(section, key, encoding=encoding, warn=True): + """ """ + val = section[key] + if isinstance(val, (list, tuple)): + newval = [] + for entry in val: + newval.append(entry.decode(encoding)) + elif isinstance(val, dict): + newval = val + else: + newval = val.decode(encoding) + newkey = key.decode(encoding) + section.rename(key, newkey) + section[newkey] = newval + # using ``call_on_sections`` allows us to modify section names + self.walk(decode, call_on_sections=True) + + + def encode(self, encoding): + """ + Encode all strings and values from unicode, + using the specified encoding. + + Works with subsections and list values. + Uses the ``walk`` method. + """ + warn('use of ``encode`` is deprecated.', DeprecationWarning) + def encode(section, key, encoding=encoding): + """ """ + val = section[key] + if isinstance(val, (list, tuple)): + newval = [] + for entry in val: + newval.append(entry.encode(encoding)) + elif isinstance(val, dict): + newval = val + else: + newval = val.encode(encoding) + newkey = key.encode(encoding) + section.rename(key, newkey) + section[newkey] = newval + self.walk(encode, call_on_sections=True) + + + def istrue(self, key): + """A deprecated version of ``as_bool``.""" + warn('use of ``istrue`` is deprecated. Use ``as_bool`` method ' + 'instead.', DeprecationWarning) + return self.as_bool(key) + + + 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, StringTypes): + # 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(): fish + >>> a['b'] = '1' + >>> a.as_int('b') + 1 + >>> a['b'] = '3.2' + >>> a.as_int('b') + Traceback (most recent call last): + ValueError: invalid literal for int(): 3.2 + """ + 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 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, _inspec=False, **kwargs): + """ + Parse a config file or create a config file object. + + ``ConfigObj(infile=None, options=None, **kwargs)`` + """ + self._inspec = _inspec + # init the superclass + Section.__init__(self, self, 0, self) + + infile = infile or [] + options = dict(options or {}) + + # keyword arguments take precedence over an options dictionary + options.update(kwargs) + if _inspec: + options['list_values'] = False + + defaults = OPTION_DEFAULTS.copy() + # TODO: check the values too. + for entry in options: + if entry not in defaults: + raise TypeError('Unrecognised option "%s".' % entry) + + # Add any explicit options to the defaults + defaults.update(options) + self._initialise(defaults) + configspec = defaults['configspec'] + self._original_configspec = configspec + self._load(infile, configspec) + + + def _load(self, infile, configspec): + if isinstance(infile, StringTypes): + 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 + infile = infile.dict() + + 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 hasattr(infile, 'read'): + # 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): + return ('ConfigObj({%s})' % + ', '.join([('%s: %s' % (repr(key), repr(self[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, StringTypes): + 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, StringTypes): + # 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, StringTypes): + # 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, StringTypes): + 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 parent.has_key(sect_name): + 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 this_section.has_key(key): + self._handle_error( + 'Duplicate keyword name at line %s.', + DuplicateError, infile, cur_index) + continue + # add the key. + # we set unrepr because if we have got this far we will never + # be creating a new 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 (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, StringTypes): + 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] = {} + 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 + """ + 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 + 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 + + # + configspec = section.configspec + self._set_configspec(section, copy) + + def validate_entry(entry, spec, val, missing, ret_true, ret_false): + 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: + try: + section.default_values.pop(entry, None) + except AttributeError: + # For Python 2.2 compatibility + try: + del section.default_values[entry] + except KeyError: + pass + + try: + section.default_values[entry] = validator.get_default_value(configspec[entry]) + except (KeyError, AttributeError): + # No default or validator has no 'get_default_value' (e.g. SimpleVal) + pass + + 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 not entry 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) + + 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: + 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 + ret_false = False + # + 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, StringTypes): + 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 + + +# Check / processing functions for options +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*. + + >>> import validate + >>> vtor = validate.Validator() + >>> my_ini = ''' + ... option1 = True + ... [section1] + ... option1 = True + ... [section2] + ... another_option = Probably + ... [section3] + ... another_option = True + ... [[section3b]] + ... value = 3 + ... value2 = a + ... value3 = 11 + ... ''' + >>> my_cfg = ''' + ... option1 = boolean() + ... option2 = boolean() + ... option3 = boolean(default=Bad_value) + ... [section1] + ... option1 = boolean() + ... option2 = boolean() + ... option3 = boolean(default=Bad_value) + ... [section2] + ... another_option = boolean() + ... [section3] + ... another_option = boolean() + ... [[section3b]] + ... value = integer + ... value2 = integer + ... value3 = integer(0, 10) + ... [[[section3b-sub]]] + ... value = string + ... [section4] + ... another_option = boolean() + ... ''' + >>> cs = my_cfg.split('\\n') + >>> ini = my_ini.split('\\n') + >>> cfg = ConfigObj(ini, configspec=cs) + >>> res = cfg.validate(vtor, preserve_errors=True) + >>> errors = [] + >>> for entry in flatten_errors(cfg, res): + ... section_list, key, error = entry + ... section_list.insert(0, '[root]') + ... if key is not None: + ... section_list.append(key) + ... else: + ... section_list.append('[missing]') + ... section_string = ', '.join(section_list) + ... errors.append((section_string, ' = ', error)) + >>> errors.sort() + >>> for entry in errors: + ... print entry[0], entry[1], (entry[2] or 0) + [root], option2 = 0 + [root], option3 = the value "Bad_value" is of the wrong type. + [root], section1, option2 = 0 + [root], section1, option3 = the value "Bad_value" is of the wrong type. + [root], section2, another_option = the value "Probably" is of the wrong type. + [root], section3, section3b, section3b-sub, [missing] = 0 + [root], section3, section3b, value2 = the value "a" is of the wrong type. + [root], section3, section3b, value3 = the value "11" is too big. + [root], section4, [missing] = 0 + """ + if levels is None: + # first time called + levels = [] + results = [] + if res is True: + return results + if res is 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 + + +"""*A programming language is a medium of expression.* - Paul Graham""" diff --git a/docs/configobj.txt b/docs/configobj.txt new file mode 100644 index 0000000..cb0c9a8 --- /dev/null +++ b/docs/configobj.txt @@ -0,0 +1,2863 @@ +================================== + Reading and Writing Config Files +================================== + +---------------------------------------- + ConfigObj 4 Introduction and Reference +---------------------------------------- + +:Authors: Michael Foord, Nicola Larosa +:Version: ConfigObj 4.5.3 +:Date: 2008/06/27 +:Homepage: `ConfigObj Homepage`_ +:Sourceforge: Sourceforge_ +:Development: `SVN Repository`_ +:License: `BSD License`_ +:Support: `Mailing List`_ + +.. _Mailing List: http://lists.sourceforge.net/lists/listinfo/configobj-develop +.. _SVN Repository: http://svn.pythonutils.python-hosting.com + +.. meta:: + :description: ConfigObj - a Python module for easy reading and writing of + config files. + :keywords: python, script, module, config, configuration, data, persistence, + developer, configparser + + +.. contents:: ConfigObj Manual +.. sectnum:: + + +Introduction +============ + +**ConfigObj** is a simple but powerful config file reader and writer: an *ini +file round tripper*. Its main feature is that it is very easy to use, with a +straightforward programmer's interface and a simple syntax for config files. +It has lots of other features though : + +* Nested sections (subsections), to any level +* List values +* Multiple line values +* String interpolation (substitution) +* Integrated with a powerful validation system + + - including automatic type checking/conversion + - repeated sections + - and allowing default values + +* When writing out config files, ConfigObj preserves all comments and the order of members and sections +* Many useful methods and options for working with configuration files (like the 'reload' method) +* Full Unicode support + + +For support and bug reports please use the ConfigObj `Mailing List`_. + + +Downloading +=========== + +The current version is **4.5.3**, dated 27th June 2008. ConfigObj 4 is +stable and mature. We still expect to pick up a few bugs along the way though [#]_. +{sm;:-)} + +You can get ConfigObj in the following ways : + +Files +----- + +* configobj.py_ from Voidspace + + ConfigObj has no external dependencies. This file is sufficient to access + all the functionality except Validation_. + +* configobj.zip_ from Voidspace + + This also contains validate.py_ and `this document`_. + +* The latest development version can be obtained from the `Subversion + Repository`_. + +* validate.py_ from Voidspace + +* You can also download *configobj.zip* from Sourceforge_ + +Documentation +------------- + +*configobj.zip* also contains `this document`_. + +* You can view `this document`_ online at the `ConfigObj Homepage`_. + + +Pythonutils +----------- + +ConfigObj is also part of the Pythonutils_ set of modules. This contains +various other useful modules, and is required by many of the `Voidspace Python +Projects`_. + + +Development Version +------------------- + +It is sometimes possible to get the latest *development version* of ConfigObj +from the `Subversion Repository <http://svn.pythonutils.python-hosting.com/trunk/pythonutils/>`_. + +.. _configobj.py: http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=configobj.py +.. _configobj.zip: http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=configobj-4.5.3.zip +.. _validate.py: http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=validate.py +.. _this document: +.. _configobj homepage: http://www.voidspace.org.uk/python/configobj.html +.. _Sourceforge: http://sourceforge.net/projects/configobj +.. _pythonutils: http://www.voidspace.org.uk/python/pythonutils.html +.. _Voidspace Python Projects: http://www.voidspace.org.uk/python/index.shtml + + + +ConfigObj in the Real World +=========================== + +**ConfigObj** is widely used. Projects using it include: + +* `Bazaar <http://bazaar-ng.org>`_. + + Bazaar is a Python distributed {acro;VCS;Version Control System}. + ConfigObj is used to read ``bazaar.conf`` and ``branches.conf``. + +* `Turbogears <http://www.turbogears.org/>`_ + + Turbogears is a web application framework. + +* `Chandler <http://chandler.osafoundation.org/>`_ + + A Python and `wxPython <http://www.wxpython.org>`_ + {acro;PIM;Personal Information Manager}, being developed by the + `OSAFoundation <http://www.osafoundation.org/>`_. + +* `matplotlib <http://matplotlib.sourceforge.net/>`_ + + A 2D plotting library. + +* `IPython <http://ipython.scipy.org/moin/>`_ + + IPython is an enhanced interactive Python shell. IPython uses ConfigObj in a module called 'TConfig' that combines it with enthought `Traits <http://code.enthought.com/traits/>`_: `tconfig <http://ipython.scipy.org/ipython/ipython/browser/ipython/branches/saw/sandbox/tconfig>`_. + +* `Elisa - the Fluendo Mediacenter <http://elisa.fluendo.com/>`_ + + Elisa is an open source cross-platform media center solution designed to be simple for people not particularly familiar with computers. + + +Getting Started +=============== + +The outstanding feature of using ConfigObj is simplicity. Most functions can be +performed with single line commands. + + +Reading a Config File +--------------------- + +The normal way to read a config file, is to give ConfigObj the filename : + +.. raw:: html + + {+coloring} + + from configobj import ConfigObj + config = ConfigObj(filename) + + {-coloring} + +You can also pass the config file in as a list of lines, or a ``StringIO`` +instance, so it doesn't matter where your config data comes from. + +You can then access members of your config file as a dictionary. Subsections +will also be dictionaries. + +.. raw:: html + + {+coloring} + + from configobj import ConfigObj + config = ConfigObj(filename) + # + value1 = config['keyword1'] + value2 = config['keyword2'] + # + section1 = config['section1'] + value3 = section1['keyword3'] + value4 = section1['keyword4'] + # + # you could also write + value3 = config['section1']['keyword3'] + value4 = config['section1']['keyword4'] + + {-coloring} + + +Writing a Config File +--------------------- + +Creating a new config file is just as easy as reading one. You can specify a +filename when you create the ConfigObj, or do it later [#]_. + +If you *don't* set a filename, then the ``write`` method will return a list of +lines instead of writing to file. See the write_ method for more details. + +Here we show creating an empty ConfigObj, setting a filename and some values, +and then writing to file : + +.. raw:: html + + {+coloring} + + from configobj import ConfigObj + config = ConfigObj() + config.filename = filename + # + config['keyword1'] = value1 + config['keyword2'] = value2 + # + config['section1'] = {} + config['section1']['keyword3'] = value3 + config['section1']['keyword4'] = value4 + # + section2 = { + 'keyword5': value5, + 'keyword6': value6, + 'sub-section': { + 'keyword7': value7 + } + } + config['section2'] = section2 + # + config['section3'] = {} + config['section3']['keyword 8'] = [value8, value9, value10] + config['section3']['keyword 9'] = [value11, value12, value13] + # + config.write() + + {-coloring} + +.. caution:: + + Keywords and section names can only be strings [#]_. Attempting to set + anything else will raise a ``ValueError``. + + +Config Files +------------ + +The config files that ConfigObj will read and write are based on the 'INI' +format. This means it will read and write files created for ``ConfigParser`` +[#]_. + +Keywords and values are separated by an ``'='``, and section markers are +between square brackets. Keywords, values, and section names can be surrounded +by single or double quotes. Indentation is not significant, but can be +preserved. + +Subsections are indicated by repeating the square brackets in the section +marker. You nest levels by using more brackets. + +You can have list values by separating items with a comma, and values spanning +multiple lines by using triple quotes (single or double). + +For full details on all these see `the config file format`_. Here's an example +to illustrate : :: + + # This is the 'initial_comment' + # Which may be several lines + keyword1 = value1 + 'keyword 2' = 'value 2' + + [ "section 1" ] + # This comment goes with keyword 3 + keyword 3 = value 3 + 'keyword 4' = value4, value 5, 'value 6' + + [[ sub-section ]] # an inline comment + # sub-section is inside "section 1" + 'keyword 5' = 'value 7' + 'keyword 6' = '''A multiline value, + that spans more than one line :-) + The line breaks are included in the value.''' + + [[[ sub-sub-section ]]] + # sub-sub-section is *in* 'sub-section' + # which is in 'section 1' + 'keyword 7' = 'value 8' + + [section 2] # an inline comment + keyword8 = "value 9" + keyword9 = value10 # an inline comment + # The 'final_comment' + # Which also may be several lines + + +ConfigObj specifications +======================== + +.. raw:: html + + {+coloring} + + config = ConfigObj(infile=None, options=None, **keywargs) + + {-coloring} + + +infile +------ + +You don't need to specify an infile. If you omit it, an empty ConfigObj will be +created. ``infile`` *can* be : + +* Nothing. In which case the ``filename`` attribute of your ConfigObj will be + ``None``. You can set a filename at any time. + +* A filename. What happens if the file doesn't already exist is determined by + the options_ ``file_error`` and ``create_empty``. The filename will be + preserved as the ``filename`` attribute. This can be changed at any time. + +* A list of lines. Any trailing newlines will be removed from the lines. The + ``filename`` attribute of your ConfigObj will be ``None``. + +* A ``StringIO`` instance or file object, or any object with a ``read`` method. + The ``filename`` attribute of your ConfigObj will be ``None`` [#]_. + +* A dictionary. You can initialise a ConfigObj from a dictionary [#]_. The + ``filename`` attribute of your ConfigObj will be ``None``. All keys must be + strings. In this case, the order of values and sections is arbitrary. + + +options +------- + +There are various options that control the way ConfigObj behaves. They can be +passed in as a dictionary of options, or as keyword arguments. Explicit keyword +arguments override the dictionary. + +All of the options are available as attributes after the config file has been +parsed. + +ConfigObj has the following options (with the default values shown) : + +* 'raise_errors': ``False`` + + When parsing, it is possible that the config file will be badly formed. The + default is to parse the whole file and raise a single error at the end. You + can set ``raise_errors = True`` to have errors raised immediately. See the + exceptions_ section for more details. + + Altering this value after initial parsing has no effect. + +* 'list_values': ``True`` + + If ``True`` (the default) then list values are possible. If ``False``, the + values are not parsed for lists. + + If ``list_values = False`` then single line values are not quoted or + unquoted when reading and writing. + + Changing this value affects whether single line values will be quoted or + not when writing. + +* 'create_empty': ``False`` + + If this value is ``True`` and the file specified by ``infile`` doesn't + exist, ConfigObj will create an empty file. This can be a useful test that + the filename makes sense: an impossible filename will cause an error. + + Altering this value after initial parsing has no effect. + +* 'file_error': ``False`` + + If this value is ``True`` and the file specified by ``infile`` doesn't + exist, ConfigObj will raise an ``IOError``. + + Altering this value after initial parsing has no effect. + +* 'interpolation': ``True`` + + Whether string interpolation is switched on or not. It is on (``True``) by + default. + + You can set this attribute to change whether string interpolation is done + when values are fetched. See the `String Interpolation`_ section for more details. + +* 'configspec': ``None`` + + If you want to use the validation system, you supply a configspec. This is + effectively a type of config file that specifies a check for each member. + This check can be used to do type conversion as well as check that the + value is within your required parameters. + + You provide a configspec in the same way as you do the initial file: a + filename, or list of lines, etc. See the validation_ section for full + details on how to use the system. + + When parsed, every section has a ``configspec`` with a dictionary of + configspec checks for *that section*. + +* 'stringify': ``True`` + + If you use the validation scheme, it can do type checking *and* conversion + for you. This means you may want to set members to integers, or other + non-string values. + + If 'stringify' is set to ``True`` (default) then non-string values will + be converted to strings when you write the config file. The validation_ + process converts values from strings to the required type. + + If 'stringify' is set to ``False``, attempting to set a member to a + non-string value [#]_ will raise a ``TypeError`` (no type conversion is + done by validation). + +* 'indent_type': ``' '`` + + Indentation is not significant; it can however be present in the input and + output config. Any combination of tabs and spaces may be used: the string + will be repeated for each level of indentation. Typical values are: ``''`` + (no indentation), ``' '`` (indentation with four spaces, the default), + ``'\t'`` (indentation with one tab). + + If this option is not specified, and the ConfigObj is initialised with a + dictionary, the indentation used in the output is the default one, that is, + four spaces. + + If this option is not specified, and the ConfigObj is initialised with a + list of lines or a file, the indentation used in the first indented line is + selected and used in all output lines. If no input line is indented, no + output line will be either. + + If this option *is* specified, the option value is used in the output + config, overriding the type of indentation in the input config (if any). + +* 'encoding': ``None`` + + By default **ConfigObj** does not decode the file/strings you pass it into + Unicode [#]_. If you want your config file as Unicode (keys and members) + you need to provide an encoding to decode the file with. This encoding will + also be used to encode the config file when writing. + + You can change the encoding attribute at any time. + + Any characters in your strings that can't be encoded with the specified + encoding will raise a ``UnicodeEncodeError``. + + .. note:: + + ``UTF16`` encoded files will automatically be detected and decoded, + even if ``encoding`` is ``None``. + + This is because it is a 16-bit encoding, and ConfigObj will mangle it + (split characters on byte boundaries) if it parses it without decoding. + +* 'default_encoding': ``None`` + + When using the ``write`` method, **ConfigObj** uses the ``encoding`` + attribute to encode the Unicode strings. If any members (or keys) have + been set as byte strings instead of Unicode, these must first be decoded + to Unicode before outputting in the specified encoding. + + ``default_encoding``, if specified, is the encoding used to decode byte + strings in the **ConfigObj** before writing. If this is ``None``, then + the Python default encoding (``sys.defaultencoding`` - usually ASCII) is + used. + + For most Western European users, a value of ``latin-1`` is sensible. + + ``default_encoding`` is *only* used if an ``encoding`` is specified. + + Any characters in byte-strings that can't be decoded using the + ``default_encoding`` will raise a ``UnicodeDecodeError``. + +* 'unrepr': ``False`` + + The ``unrepr`` option reads and writes files in a different mode. This + allows you to store and retrieve the basic Python data-types using config + files. + + This uses Python syntax for lists and quoting. See `unrepr mode`_ for the + full details. + +* 'write_empty_values': ``False`` + + If ``write_empty_values`` is ``True``, empty strings are written as + empty values. See `Empty Values`_ for more details. + + +Methods +------- + +The ConfigObj is a subclass of an object called ``Section``, which is itself a +subclass of ``dict``, the builtin dictionary type. This means it also has +**all** the normal dictionary methods. + +In addition, the following `Section Methods`_ may be useful : + +* 'restore_default' +* 'restore_defaults' +* 'walk' +* 'merge' +* 'dict' +* 'as_bool' +* 'as_float' +* 'as_int' + +Read about Sections_ for details of all the methods. + +.. hint:: + + The *merge* method of sections is a recursive update. + + You can use this to merge sections, or even whole ConfigObjs, into each + other. + + You would typically use this to create a default ConfigObj and then merge + in user settings. This way users only need to specify values that are + different from the default. You can use configspecs and validation to + achieve the same thing of course. + + +The public methods available on ConfigObj are : + +* 'write' +* 'validate' +* 'reset' +* 'reload' + + +write +~~~~~ + +:: + + write(file_object=None) + +This method writes the current ConfigObj and takes a single, optional argument +[#]_. + +If you pass in a file like object to the ``write`` method, the config file will +be written to this. (The only method of this object that is used is its +``write`` method, so a ``StringIO`` instance, or any other file like object +will work.) + +Otherwise, the behaviour of this method depends on the ``filename`` attribute +of the ConfigObj. + +``filename`` + ConfigObj will write the configuration to the file specified. + +``None`` + ``write`` returns a list of lines. (Not ``'\n'`` terminated) + +First the 'initial_comment' is written, then the config file, followed by the +'final_comment'. Comment lines and inline comments are written with each +key/value. + + +validate +~~~~~~~~ + +:: + + validate(validator, preserve_errors=False, copy=False) + +.. raw:: html + + {+coloring} + + # filename is the config file + # filename2 is the configspec + # (which could also be hardcoded into your program) + config = ConfigObj(filename, configspec=filename2) + # + from validate import Validator + val = Validator() + test = config.validate(val) + if test == True: + print 'Succeeded.' + {-coloring} + +The validate method uses the `validate +<http://www.voidspace.org.uk/python/validate.html>`__ module to do the +validation. + +This method validates the ConfigObj against the configspec. By doing type +conversion as well it can abstract away the config file altogether and present +the config *data* to your application (in the types it expects it to be). + +If the ``configspec`` attribute of the ConfigObj is ``None``, it raises a +``ValueError``. + +If the stringify_ attribute is set, this process will convert values to the +type defined in the configspec. + +The validate method uses checks specified in the configspec and defined in the +``Validator`` object. It is very easy to extend. + +The configspec looks like the config file, but instead of the value, you +specify the check (and any default value). See the validation_ section for +details. + +.. hint:: + + The system of configspecs can seem confusing at first, but is actually + quite simple and powerful. For a concrete example of how to use it, you may + find this blog entry helpful : + `Transforming Values with ConfigObj <http://www.voidspace.org.uk/python/weblog/arch_d7_2006_03_04.shtml#e257>`_. + + +The ``copy`` parameter fills in missing values from the configspec (default +values), *without* marking the values as defaults. It also causes comments to +be copied from the configspec into the config file. This allows you to use a +configspec to create default config files. (Normally default values aren't +written out by the ``write`` method.) + +As of ConfigObj 4.3.0 you can also pass in a ConfigObj instance as your +configspec. This is especially useful if you need to specify the encoding of +your configspec file. When you read your configspec file, you *must* specify +``list_values=False``. + +.. raw:: html + + {+coloring} + from configobj import ConfigObj + configspec = ConfigObj(configspecfilename, encoding='UTF8', + list_values=False) + config = ConfigObj(filename, configspec=configspec) + {-coloring} + + +Return Value +############ + +By default, the validate method either returns ``True`` (everything passed) +or a dictionary of ``True``/``False`` representing pass/fail. The dictionary +follows the structure of the ConfigObj. + +If a whole section passes then it is replaced with the value ``True``. If a +whole section fails, then it is replaced with the value ``False``. + +If a value is missing, and there is no default in the check, then the check +automatically fails. + +The ``validate`` method takes an optional keyword argument ``preserve_errors``. +If you set this to ``True``, instead of getting ``False`` for failed checks you +get the actual error object from the **validate** module. This usually contains +useful information about why the check failed. + +See the `flatten_errors`_ function for how to turn your results dictionary into +a useful list of error messages. + +Even if ``preserve_errors`` is ``True``, missing keys or sections will still be +represented by a ``False`` in the results dictionary. + + +Mentioning Default Values +######################### + +In the check in your configspec, you can specify a default to be used - by +using the ``default`` keyword. E.g. :: + + key1 = integer(0, 30, default=15) + key2 = integer(default=15) + key3 = boolean(default=True) + key4 = option('Hello', 'Goodbye', 'Not Today', default='Not Today') + +If the configspec check supplies a default and the value is missing in the +config, then the default will be set in your ConfigObj. (It is still passed to +the ``Validator`` so that type conversion can be done: this means the default +value must still pass the check.) + +ConfigObj keeps a record of which values come from defaults, using the +``defaults`` attribute of sections_. Any key in this list isn't written out by +the ``write`` method. If a key is set from outside (even to the same value) +then it is removed from the ``defaults`` list. + +.. note: + + Even if all the keys in a section are in the defaults list, the section + marker is still written out. + +There is additionally a special case default value of ``None``. If you set the +default value to ``None`` and the value is missing, the value will always be +set to ``None``. As the other checks don't return ``None`` (unless you +implement your own that do), you can tell that this value came from a default +value (and was missing from the config file). It allows an easy way of +implementing optional values. Simply check (and ignore) members that are set +to ``None``. + +.. note:: + + If stringify_ is ``False`` then ``default=None`` returns ``''`` instead of + ``None``. This is because setting a value to a non-string raises an error + if stringify is unset. + +The default value can be a list. See `List Values`_ for the way to do this. + +Writing invalid default values is a *guaranteed* way of confusing your users. +Default values **must** pass the check. + + +Mentioning Repeated Sections +############################ + +In the configspec it is possible to cause *every* sub-section in a section to +be validated using the same configspec. You do this with a section in the +configspec called ``__many__``. Every sub-section in that section has the +``__many__`` configspec applied to it (without you having to explicitly name +them in advance). + +If you define a ``__many__`` type section it must the only sub-section in that +section. Having a ``__many__`` *and* other sub-sections defined in the same +section will raise a ``RepeatSectionError``. + +Your ``__many__`` section can have nested subsections, which can also include +``__many__`` type sections. + +See `Repeated Sections`_ for examples. + + +Mentioning SimpleVal +#################### + +If you just want to check if all members are present, then you can use the +``SimpleVal`` object that comes with ConfigObj. It only fails members if they +are missing. + +Write a configspec that has all the members you want to check for, but set +every section to ``''``. + +.. raw:: html + + {+coloring} + + val = SimpleVal() + test = config.validate(val) + if test is True: + print 'Succeeded.' + + {-coloring} + + +Mentioning copy Mode +#################### + +As discussed in `Mentioning Default Values`_, you can use a configspec to +supply default values. These are marked in the ConfigObj instance as defaults, +and *not* written out by the ``write`` mode. This means that your users only +need to supply values that are different from the defaults. + +This can be inconvenient if you *do* want to write out the default values, +for example to write out a default config file. + +If you set ``copy=True`` when you call validate, then no values are marked as +defaults. In addition, all comments from the configspec are copied into +your ConfigObj instance. You can then call ``write`` to create your config +file. + +There is a limitation with this. In order to allow `String Interpolation`_ to work +within configspecs, ``DEFAULT`` sections are not processed by +validation; even in copy mode. + + +reload +~~~~~~ + +If a ConfigObj instance was loaded from the filesystem, then this method will reload it. It +will also reuse any configspec you supplied at instantiation (including reloading it from +the filesystem if you passed it in as a filename). + +If the ConfigObj does not have a filename attribute pointing to a file, then a ``ReloadError`` +will be raised. + + +reset +~~~~~ + +This method takes no arguments and doesn't return anything. It restores a ConfigObj +instance to a freshly created state. + + +Attributes +---------- + +A ConfigObj has the following attributes : + +* indent_type +* interpolate +* stringify +* BOM +* initial_comment +* final_comment +* list_values +* encoding +* default_encoding +* unrepr +* write_empty_values +* newlines + +.. note:: + + This doesn't include *comments*, *inline_comments*, *defaults*, or + *configspec*. These are actually attributes of Sections_. + +It also has the following attributes as a result of parsing. They correspond to +options_ when the ConfigObj was created, but changing them has no effect. + +* raise_errors +* create_empty +* file_error + + +interpolation +~~~~~~~~~~~~~ + +ConfigObj can perform string interpolation in a *similar* way to +``ConfigParser``. See the `String Interpolation`_ section for full details. + +If ``interpolation`` is set to ``False``, then interpolation is *not* done when +you fetch values. + + +stringify +~~~~~~~~~ + +If this attribute is set (``True``) then the validate_ method changes the +values in the ConfigObj. These are turned back into strings when write_ is +called. + +If stringify is unset (``False``) then attempting to set a value to a non +string (or a list of strings) will raise a ``TypeError``. + + +BOM +~~~ + +If the initial config file *started* with the UTF8 Unicode signature (known +slightly incorrectly as the {acro;BOM;Byte Order Mark}), or the UTF16 BOM, then +this attribute is set to ``True``. Otherwise it is ``False``. + +If it is set to ``True`` when ``write`` is called then, if ``encoding`` is set +to ``None`` *or* to ``utf_8`` (and variants) a UTF BOM will be written. + +For UTF16 encodings, a BOM is *always* written. + + +initial_comment +~~~~~~~~~~~~~~~ + +This is a list of lines. If the ConfigObj is created from an existing file, it +will contain any lines of comments before the start of the members. + +If you create a new ConfigObj, this will be an empty list. + +The write method puts these lines before it starts writing out the members. + + +final_comment +~~~~~~~~~~~~~ + +This is a list of lines. If the ConfigObj is created from an existing file, it +will contain any lines of comments after the last member. + +If you create a new ConfigObj, this will be an empty list. + +The ``write`` method puts these lines after it finishes writing out the +members. + + +list_values +~~~~~~~~~~~ + +This attribute is ``True`` or ``False``. If set to ``False`` then values are +not parsed for list values. In addition single line values are not unquoted. + +This allows you to do your own parsing of values. It exists primarily to +support the reading of the configspec_ - but has other use cases. + +For example you could use the ``LineParser`` from the +`listquote module <http://www.voidspace.org.uk/python/listquote.html#lineparser>`_ +to read values for nested lists. + +Single line values aren't quoted when writing - but multiline values are +handled as normal. + +.. caution:: + + Because values aren't quoted, leading or trailing whitespace can be + lost. + + This behaviour was changed in version 4.0.1. + + Prior to this, single line values might have been quoted; even with + ``list_values=False``. This means that files written by **ConfigObj** + *could* now be incompatible - and need the quotes removing by hand. + + +encoding +~~~~~~~~ + +This is the encoding used to encode the output, when you call ``write``. It +must be a valid encoding `recognised by Python <http://docs.python.org/lib/standard-encodings.html>`_. + +If this value is ``None`` then no encoding is done when ``write`` is called. + + +default_encoding +~~~~~~~~~~~~~~~~ + +If encoding is set, any byte-strings in your ConfigObj instance (keys or +members) will first be decoded to Unicode using the encoding specified by the +``default_encoding`` attribute. This ensures that the output is in the encoding +specified. + +If this value is ``None`` then ``sys.defaultencoding`` is used instead. + + +unrepr +~~~~~~ + +Another boolean value. If this is set, then ``repr(value)`` is used to write +values. This writes values in a slightly different way to the normal ConfigObj +file syntax. + +This preserves basic Python data-types when read back in. See `unrepr mode`_ +for more details. + + +write_empty_values +~~~~~~~~~~~~~~~~~~ + +Also boolean. If set, values that are an empty string (``''``) are written as +empty values. See `Empty Values`_ for more details. + + +newlines +~~~~~~~~ + +When a config file is read, ConfigObj records the type of newline separators in the +file and uses this separator when writing. It defaults to ``None``, and ConfigObj +uses the system default (``os.sep``) if write is called without newlines having +been set. + + +The Config File Format +====================== + +You saw an example config file in the `Config Files`_ section. Here is a fuller +specification of the config files used and created by ConfigObj. + +The basic pattern for keywords is : :: + + # comment line + # comment line + keyword = value # inline comment + +Both keyword and value can optionally be surrounded in quotes. The equals sign +is the only valid divider. + +Values can have comments on the lines above them, and an inline comment after +them. This, of course, is optional. See the comments_ section for details. + +If a keyword or value starts or ends with whitespace, or contains a quote mark +or comma, then it should be surrounded by quotes. Quotes are not necessary if +whitespace is surrounded by non-whitespace. + +Values can also be lists. Lists are comma separated. You indicate a single +member list by a trailing comma. An empty list is shown by a single comma : :: + + keyword1 = value1, value2, value3 + keyword2 = value1, # a single member list + keyword3 = , # an empty list + +Values that contain line breaks (multi-line values) can be surrounded by triple +quotes. These can also be used if a value contains both types of quotes. List +members cannot be surrounded by triple quotes : :: + + keyword1 = ''' A multi line value + on several + lines''' # with a comment + keyword2 = '''I won't be "afraid".''' + # + keyword3 = """ A multi line value + on several + lines""" # with a comment + keyword4 = """I won't be "afraid".""" + +.. warning:: + + There is no way of safely quoting values that contain both types of triple + quotes. + +A line that starts with a '#', possibly preceded by whitespace, is a comment. + +New sections are indicated by a section marker line. That is the section name +in square brackets. Whitespace around the section name is ignored. The name can +be quoted with single or double quotes. The marker can have comments before it +and an inline comment after it : :: + + # The First Section + [ section name 1 ] # first section + keyword1 = value1 + + # The Second Section + [ "section name 2" ] # second section + keyword2 = value2 + +Any subsections (sections that are *inside* the current section) are +designated by repeating the square brackets before and after the section name. +The number of square brackets represents the nesting level of the sub-section. +Square brackets may be separated by whitespace; such whitespace, however, will +not be present in the output config written by the ``write`` method. + +Indentation is not significant, but can be preserved. See the description of +the ``indent_type`` option, in the `ConfigObj specifications`_ chapter, for the +details. + +A *NestingError* will be raised if the number of the opening and the closing +brackets in a section marker is not the same, or if a sub-section's nesting +level is greater than the nesting level of it parent plus one. + +In the outer section, single values can only appear before any sub-section. +Otherwise they will belong to the sub-section immediately before them. :: + + # initial comment + keyword1 = value1 + keyword2 = value2 + + [section 1] + keyword1 = value1 + keyword2 = value2 + + [[sub-section]] + # this is in section 1 + keyword1 = value1 + keyword2 = value2 + + [[[nested section]]] + # this is in sub section + keyword1 = value1 + keyword2 = value2 + + [[sub-section2]] + # this is in section 1 again + keyword1 = value1 + keyword2 = value2 + + [[sub-section3]] + # this is also in section 1, indentation is misleading here + keyword1 = value1 + keyword2 = value2 + + # final comment + +When parsed, the above config file produces the following data structure : + +.. raw:: html + + {+coloring} + + ConfigObj({ + 'keyword1': 'value1', + 'keyword2': 'value2', + 'section 1': { + 'keyword1': 'value1', + 'keyword2': 'value2', + 'sub-section': { + 'keyword1': 'value1', + 'keyword2': 'value2', + 'nested section': { + 'keyword1': 'value1', + 'keyword2': 'value2', + }, + }, + 'sub-section2': { + 'keyword1': 'value1', + 'keyword2': 'value2', + }, + 'sub-section3': { + 'keyword1': 'value1', + 'keyword2': 'value2', + }, + }, + }) + + {-coloring} + +Sections are ordered: note how the structure of the resulting ConfigObj is in +the same order as the original file. + +.. note:: + + In ConfigObj 4.3.0 *empty values* became valid syntax. They are read as the + empty string. There is also an option/attribute (``write_empty_values``) to + allow the writing of these. + + This is mainly to support 'legacy' config files, written from other + applications. This is documented under `Empty Values`_. + + `unrepr mode`_ introduces *another* syntax variation, used for storing + basic Python datatypes in config files. {sm;:-)} + + +Sections +======== + +Every section in a ConfigObj has certain properties. The ConfigObj itself also +has these properties, because it too is a section (sometimes called the *root +section*). + +``Section`` is a subclass of the standard new-class dictionary, therefore it +has **all** the methods of a normal dictionary. This means you can ``update`` +and ``clear`` sections. + +.. note:: + + You create a new section by assigning a member to be a dictionary. + + The new ``Section`` is created *from* the dictionary, but isn't the same + thing as the dictionary. (So references to the dictionary you use to create + the section *aren't* references to the new section). + + Note the following. + + .. raw:: html + + {+coloring} + + config = ConfigObj() + vals = {'key1': 'value 1', + 'key2': 'value 2' + } + config['vals'] = vals + config['vals'] == vals + True + config['vals'] is vals + False + + {-coloring} + + If you now change ``vals``, the changes won't be reflected in ``config['vals']``. + +A section is ordered, following its ``scalars`` and ``sections`` +attributes documented below. This means that the following dictionary +attributes return their results in order. + +* '__iter__' + + More commonly known as ``for member in section:``. + +* '__repr__' and '__str__' + + Any time you print or display the ConfigObj. + +* 'items' + +* 'iteritems' + +* 'iterkeys' + +* 'itervalues' + +* 'keys' + +* 'popitem' + +* 'values' + + +Section Attributes +------------------ + +* main + + A reference to the main ConfigObj. + +* parent + + A reference to the 'parent' section, the section that this section is a + member of. + + On the ConfigObj this attribute is a reference to itself. You can use this + to walk up the sections, stopping when ``section.parent is section``. + +* depth + + The nesting level of the current section. + + If you create a new ConfigObj and add sections, 1 will be added to the + depth level between sections. + +* defaults + + This attribute is a list of scalars that came from default values. Values + that came from defaults aren't written out by the ``write`` method. + Setting any of these values in the section removes them from the defaults + list. + +* default_values + + This attribute is a dictionary mapping keys to the default values for the + keys. By default it is an empty dictionary and is populated when you + validate the ConfigObj. + +* scalars, sections + + These attributes are normal lists, representing the order that members, + single values and subsections appear in the section. The order will either + be the order of the original config file, *or* the order that you added + members. + + The order of members in this lists is the order that ``write`` creates in + the config file. The ``scalars`` list is output before the ``sections`` + list. + + Adding or removing members also alters these lists. You can manipulate the + lists directly to alter the order of members. + + .. warning:: + + If you alter the ``scalars``, ``sections``, or ``defaults`` attributes + so that they no longer reflect the contents of the section, you will + break your ConfigObj. + + See also the ``rename`` method. + +* comments + + This is a dictionary of comments associated with each member. Each entry is + a list of lines. These lines are written out before the member. + +* inline_comments + + This is *another* dictionary of comments associated with each member. Each + entry is a string that is put inline with the member. + +* configspec + + The configspec attribute is a dictionary mapping scalars to *checks*. A + check defines the expected type and possibly the allowed values for a + member. + + The configspec has the same format as a config file, but instead of values + it has a specification for the value (which may include a default value). + The validate_ method uses it to check the config file makes sense. If a + configspec is passed in when the ConfigObj is created, then it is parsed + and broken up to become the ``configspec`` attribute of each section. + + If you didn't pass in a configspec, this attribute will be ``None`` on the + root section (the main ConfigObj). + + You can set the configspec attribute directly on a section. + + See the validation_ section for full details of how to write configspecs. + + +Section Methods +--------------- + +* **dict** + + This method takes no arguments. It returns a deep copy of the section as a + dictionary. All subsections will also be dictionaries, and list values will + be copies, rather than references to the original [#]_. + +* **rename** + + ``rename(oldkey, newkey)`` + + This method renames a key, without affecting its position in the sequence. + + It is mainly implemented for the ``encode`` and ``decode`` methods, which + provide some Unicode support. + +* **merge** + + ``merge(indict)`` + + This method is a *recursive update* method. It allows you to merge two + config files together. + + You would typically use this to create a default ConfigObj and then merge + in user settings. This way users only need to specify values that are + different from the default. + + For example : + + .. raw:: html + + {+coloring} + + # def_cfg contains your default config settings + # user_cfg contains the user settings + cfg = ConfigObj(def_cfg) + usr = ConfigObj(user_cfg) + # + cfg.merge(usr) + + """ + cfg now contains a combination of the default settings and the user + settings. + + The user settings will have overwritten any of the default ones. + """ + + {-coloring} + +* **walk** + + This method can be used to transform values and names. See `walking a + section`_ for examples and explanation. + +* **decode** + + ``decode(encoding)`` + + This method decodes names and values into Unicode objects, using the + supplied encoding. + +* **encode** + + ``encode(encoding)`` + + This method is the opposite of ``decode`` {sm;:!:}. + + It encodes names and values using the supplied encoding. If any of your + names/values are strings rather than Unicode, Python will have to do an + implicit decode first. (This method uses ``sys.defaultencoding`` for + implicit decodes.) + +* **as_bool** + + ``as_bool(key)`` + + Returns ``True`` if the key contains a string that represents ``True``, or + is the ``True`` object. + + Returns ``False`` if the key contains a string that represents ``False``, + or is the ``False`` object. + + Raises a ``ValueError`` if the key contains anything else. + + Strings that represent ``True`` are (not case sensitive) : :: + + true, yes, on, 1 + + Strings that represent ``False`` are : :: + + false, no, off, 0 + + .. note:: + + In ConfigObj 4.1.0, this method was called ``istrue``. That method is + now deprecated and will issue a warning when used. It will go away + in a future release. + +* **as_int** + + ``as_int(key)`` + + This returns the value contained in the specified key as an integer. + + It raises a ``ValueError`` if the conversion can't be done. + +* **as_float** + + ``as_float(key)`` + + This returns the value contained in the specified key as a float. + + It raises a ``ValueError`` if the conversion can't be done. + +* **restore_default** + + ``restore_default(key)`` + + Restore (and return) the 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. + +* **restore_defaults** + + ``restore_defaults()`` + + 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. + + +Walking a Section +----------------- + +.. note:: + + The walk method allows you to call a function on every member/name. + +.. raw:: html + + {+coloring} + + walk(function, raise_errors=True, + call_on_sections=False, **keywargs): + + {-coloring} + +``walk`` is a method of the ``Section`` object. This means it is also a method +of ConfigObj. + +It walks through every member and calls a function on the keyword and value. It +walks recursively through subsections. + +It returns a dictionary of all the computed values. + +If the function raises an exception, the default is to propagate the error, and +stop. If ``raise_errors=False`` then it sets the return value for that keyword +to ``False`` instead, and continues. This is similar to the way validation_ +works. + +Your function receives the arguments ``(section, key)``. The current value is +then ``section[key]`` [#]_. Any unrecognised keyword arguments you pass to +walk, are passed on to the function. + +Normally ``walk`` just recurses into subsections. If you are transforming (or +checking) names as well as values, then you want to be able to change the names +of sections. In this case set ``call_on_sections`` to ``True``. Now, on +encountering a sub-section, *first* the function is called for the *whole* +sub-section, and *then* it recurses into it's members. This means your function +must be able to handle receiving dictionaries as well as strings and lists. + +If you are using the return value from ``walk`` *and* ``call_on_sections``, +note that walk discards the return value when it calls your function. + +.. caution:: + + You can use ``walk`` to transform the names of members of a section + but you mustn't add or delete members. + + +Examples +-------- + +Examples that use the walk method are the ``encode`` and ``decode`` methods. +They both define a function and pass it to walk. Because these functions +transform names as well as values (from byte strings to Unicode) they set +``call_on_sections=True``. + +To see how they do it, *read the source Luke* {sm;:cool:}. + +You can use this for transforming all values in your ConfigObj. For example +you might like the nested lists from ConfigObj 3. This was provided by the +listquote_ module. You could switch off the parsing for list values +(``list_values=False``) and use listquote to parse every value. + +Another thing you might want to do is use the Python escape codes in your +values. You might be *used* to using ``\n`` for line feed and ``\t`` for tab. +Obviously we'd need to decode strings that come from the config file (using the +escape codes). Before writing out we'll need to put the escape codes back in +encode. + +As an example we'll write a function to use with walk, that encodes or decodes +values using the ``string-escape`` codec. + +The function has to take each value and set the new value. As a bonus we'll +create one function that will do decode *or* encode depending on a keyword +argument. + +We don't want to work with section names, we're only transforming values, so +we can leave ``call_on_sections`` as ``False``. This means the two datatypes we +have to handle are strings and lists, we can ignore everything else. (We'll +treat tuples as lists as well). + +We're not using the return values, so it doesn't need to return anything, just +change the values if appropriate. + +.. raw:: html + + {+coloring} + + def string_escape(section, key, encode=False): + """ + A function to encode or decode using the 'string-escape' codec. + To be passed to the walk method of a ConfigObj. + By default it decodes. + To encode, pass in the keyword argument ``encode=True``. + """ + val = section[key] + # is it a type we can work with + # NOTE: for platforms where Python > 2.2 + # you can use basestring instead of (str, unicode) + if not isinstance(val, (str, unicode, list, tuple)): + # no ! + return + elif isinstance(val, (str, unicode)): + # it's a string ! + if not encode: + section[key] = val.decode('string-escape') + else: + section[key] = val.encode('string-escape') + else: + # it must be a list or tuple! + # we'll be lazy and create a new list + newval = [] + # we'll check every member of the list + for entry in val: + if isinstance(entry, (str, unicode)): + if not encode: + newval.append(entry.decode('string-escape')) + else: + newval.append(entry.encode('string-escape')) + else: + newval.append(entry) + # done ! + section[key] = newval + + # assume we have a ConfigObj called ``config`` + # + # To decode + config.walk(string_escape) + # + # To encode. + # Because ``walk`` doesn't recognise the ``encode`` argument + # it passes it to our function. + config.walk(string_escape, encode=True) + + {-coloring} + +Here's a simple example of using ``walk`` to transform names and values. One +usecase of this would be to create a *standard* config file with placeholders +for section and keynames. You can then use walk to create new config files +and change values and member names : + +.. raw:: html + + {+coloring} + + # We use 'XXXX' as a placeholder + config = ''' + XXXXkey1 = XXXXvalue1 + XXXXkey2 = XXXXvalue2 + XXXXkey3 = XXXXvalue3 + [XXXXsection1] + XXXXkey1 = XXXXvalue1 + XXXXkey2 = XXXXvalue2 + XXXXkey3 = XXXXvalue3 + [XXXXsection2] + XXXXkey1 = XXXXvalue1 + XXXXkey2 = XXXXvalue2 + XXXXkey3 = XXXXvalue3 + [[XXXXsection1]] + XXXXkey1 = XXXXvalue1 + XXXXkey2 = XXXXvalue2 + XXXXkey3 = XXXXvalue3 + '''.splitlines() + cfg = ConfigObj(config) + # + 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) + print cfg + ConfigObj({'CLIENT1key1': 'CLIENT1value1', 'CLIENT1key2': 'CLIENT1value2', + 'CLIENT1key3': 'CLIENT1value3', + 'CLIENT1section1': {'CLIENT1key1': 'CLIENT1value1', + 'CLIENT1key2': 'CLIENT1value2', 'CLIENT1key3': 'CLIENT1value3'}, + 'CLIENT1section2': {'CLIENT1key1': 'CLIENT1value1', + 'CLIENT1key2': 'CLIENT1value2', 'CLIENT1key3': 'CLIENT1value3', + 'CLIENT1section1': {'CLIENT1key1': 'CLIENT1value1', + 'CLIENT1key2': 'CLIENT1value2', 'CLIENT1key3': 'CLIENT1value3'}}}) + + {-coloring} + + +Exceptions +========== + +There are several places where ConfigObj may raise exceptions (other than +because of bugs). + +1) If a configspec filename you pass in doesn't exist, or a config file + filename doesn't exist *and* ``file_error=True``, an ``IOError`` will be + raised. + +2) If you try to set a non-string key, or a non string value when + ``stringify=False``, a ``TypeError`` will be raised. + +3) A badly built config file will cause parsing errors. + +4) A parsing error can also occur when reading a configspec. + +5) In string interpolation you can specify a value that doesn't exist, or + create circular references (recursion). + +6) If you have a ``__many__`` repeated section with other section definitions + (in a configspec), a ``RepeatSectionError`` will be raised. + +Number 5 (which is actually two different types of exceptions) is documented + in `String Interpolation`_. + +Number 6 is explained in the validation_ section. + +*This* section is about errors raised during parsing. + +The base error class is ``ConfigObjError``. This is a subclass of +``SyntaxError``, so you can trap for ``SyntaxError`` without needing to +directly import any of the ConfigObj exceptions. + +The following other exceptions are defined (all deriving from +``ConfigObjError``) : + +* ``NestingError`` + + This error indicates either a mismatch in the brackets in a section marker, + or an excessive level of nesting. + +* ``ParseError`` + + This error indicates that a line is badly written. It is neither a valid + ``key = value`` line, nor a valid section marker line, nor a comment line. + +* ``DuplicateError`` + + The keyword or section specified already exists. + +* ``ConfigspecError`` + + An error occurred whilst parsing a configspec. + +* ``UnreprError`` + + An error occurred when parsing a value in `unrepr mode`_. + +* ``ReloadError`` + + ``reload`` was called on a ConfigObj instance that doesn't have a valid + filename attribute. + +When parsing a configspec, ConfigObj will stop on the first error it +encounters. It will raise a ``ConfigspecError``. This will have an ``error`` +attribute, which is the actual error that was raised. + +Behaviour when parsing a config file depends on the option ``raise_errors``. +If ConfigObj encounters an error while parsing a config file: + + If ``raise_errors=True`` then ConfigObj will raise the appropriate error + and parsing will stop. + + If ``raise_errors=False`` (the default) then parsing will continue to the + end and *all* errors will be collected. + +If ``raise_errors`` is False and multiple errors are found a ``ConfigObjError`` +is raised. The error raised has a ``config`` attribute, which is the parts of +the ConfigObj that parsed successfully. It also has an attribute ``errors``, +which is a list of *all* the errors raised. Each entry in the list is an +instance of the appropriate error type. Each one has the following attributes +(useful for delivering a sensible error message to your user) : + +* ``line``: the original line that caused the error. + +* ``line_number``: its number in the config file. + +* ``message``: the error message that accompanied the error. + +If only one error is found, then that error is re-raised. The error still has +the ``config`` and ``errors`` attributes. This means that your error handling +code can be the same whether one error is raised in parsing , or several. + +It also means that in the most common case (a single error) a useful error +message will be raised. + +.. note:: + + One wrongly written line could break the basic structure of your config + file. This could cause every line after it to flag an error, so having a + list of all the lines that caused errors may not be as useful as it sounds. + {sm;:-(}. + + +Validation +========== + +.. hint:: + + The system of configspecs can seem confusing at first, but is actually + quite simple and powerful. For a concrete example of how to use it, you may + find this blog entry helpful : + `Transforming Values with ConfigObj <http://www.voidspace.org.uk/python/weblog/arch_d7_2006_03_04.shtml#e257>`_. + +Validation is done through a combination of the configspec_ and a ``Validator`` +object. For this you need *validate.py* [#]_. See downloading_ if you don't +have a copy. + +Validation can perform two different operations : + +1) Check that a value meets a specification. For example, check that a value + is an integer between one and six, or is a choice from a specific set of + options. + +2) It can convert the value into the type required. For example, if one of + your values is a port number, validation will turn it into an integer for + you. + +So validation can act as a transparent layer between the datatypes of your +application configuration (boolean, integers, floats, etc) and the text format +of your config file. + + +configspec +---------- + +The ``validate`` method checks members against an entry in the configspec. Your +configspec therefore resembles your config file, with a check for every member. + +In order to perform validation you need a ``Validator`` object. This has +several useful built-in check functions. You can also create your own custom +functions and register them with your Validator object. + +Each check is the name of one of these functions, including any parameters and +keyword arguments. The configspecs look like function calls, and they map to +function calls. + +The basic datatypes that an un-extended Validator can test for are : + +* boolean values (True and False) +* integers (including minimum and maximum values) +* floats (including min and max) +* strings (including min and max length) +* IP addresses (v4 only) + +It can also handle lists of these types and restrict a value to being one from +a set of options. + +An example configspec is going to look something like : :: + + port = integer(0, 100) + user = string(max=25) + mode = option('quiet', 'loud', 'silent') + +You can specify default values, and also have the same configspec applied to +several sections. This is called `repeated sections`_. + +For full details on writing configspecs, please refer to the `validate.py +documentation`_. + +.. important:: + + Your configspec is read by ConfigObj in the same way as a config file. + + That means you can do interpolation *within* your configspec. + + In order to allow this, checks in the 'DEFAULT' section (of the root level + of your configspec) are *not* used. + +If you need to specify the encoding of your configspec, then you can pass in a +ConfigObj instance as your configspec. When you read your configspec file, you +*must* specify ``list_values=False``. + +.. raw:: html + + {+coloring} + from configobj import ConfigObj + configspec = ConfigObj(configspecfilename, encoding='UTF8', + list_values=False) + config = ConfigObj(filename, configspec=configspec) + {-coloring} + +.. _validate.py documentation: http://www.voidspace.org.uk/python/validate.html + + +Type Conversion +--------------- + +By default, validation does type conversion. This means that if you specify +``integer`` as the check, then calling validate_ will actually change the value +to an integer (so long as the check succeeds). + +It also means that when you call the write_ method, the value will be converted +back into a string using the ``str`` function. + +To switch this off, and leave values as strings after validation, you need to +set the stringify_ attribute to ``False``. If this is the case, attempting to +set a value to a non-string will raise an error. + + +Default Values +-------------- + +You can set a default value in your check. If the value is missing from the +config file then this value will be used instead. This means that your user +only has to supply values that differ from the defaults. + +If you *don't* supply a default then for a value to be missing is an error, +and this will show in the `return value`_ from validate. + +Additionally you can set the default to be ``None``. This means the value will +be set to ``None`` (the object) *whichever check is used*. (It will be set to +``''`` rather than ``None`` if stringify_ is ``False``). You can use this +to easily implement optional values in your config files. :: + + port = integer(0, 100, default=80) + user = string(max=25, default=0) + mode = option('quiet', 'loud', 'silent', default='loud') + nick = string(default=None) + +.. note:: + + Because the default goes through type conversion, it also has to pass the + check. + + Note that ``default=None`` is case sensitive. + + +List Values +~~~~~~~~~~~ + +It's possible that you will want to specify a list as a default value. To avoid +confusing syntax with commas and quotes you use a list constructor to specify +that keyword arguments are lists. This includes the ``default`` value. This +makes checks look something like : :: + + checkname(default=list('val1', 'val2', 'val3')) + +This works with all keyword arguments, but is most useful for default values. + + +Repeated Sections +----------------- + +Repeated sections are a way of specifying a configspec for a section that +should be applied to *all* subsections in the same section. + +The easiest way of explaining this is to give an example. Suppose you have a +config file that describes a dog. That dog has various attributes, but it can +also have many fleas. You don't know in advance how many fleas there will be, +or what they will be called, but you want each flea validated against the same +configspec. + +We can define a section called *fleas*. We want every flea in that section +(every sub-section) to have the same configspec applied to it. We do this by +defining a single section called ``__many__``. :: + + [dog] + name = string(default=Rover) + age = float(0, 99, default=0) + + [[fleas]] + + [[[__many__]]] + bloodsucker = boolean(default=True) + children = integer(default=10000) + size = option(small, tiny, micro, default=tiny) + +Every flea on our dog will now be validated using the ``__many__`` configspec. + +If you define another sub-section in a section *as well as* a ``__many__`` then +you will get an error. + +``__many__`` sections can have sub-sections, including their own ``__many__`` +sub-sections. Defaults work in the normal way in repeated sections. + + +Copy Mode +--------- + +Because you can specify default values in your configspec, you can use +ConfigObj to write out default config files for your application. + +However, normally values supplied from a default in a configspec are *not* +written out by the ``write`` method. + +To do this, you need to specify ``copy=True`` when you call validate. As well +as not marking values as default, all the comments in the configspec file +will be copied into your ConfigObj instance. + +.. raw:: html + + {+coloring} + from configobj import ConfigObj + from validate import Validator + vdt = Validator() + config = ConfigObj(configspec='default.ini') + config.filename = 'new_default.ini' + config.validate(vdt, copy=True) + config.write() + {-coloring} + + +Validation and Interpolation +---------------------------- + +String interpolation and validation don't play well together. When validation +changes type it sets the value. If the value uses interpolation, then the +interpolation reference would normally be overwritten. Calling ``write`` would +then use the absolute value and the interpolation reference would be lost. + +As a compromise - if the value is unchanged by validation then it is not reset. +This means strings that pass through validation unmodified will not be +overwritten. If validation changes type - the value has to be overwritten, and +any interpolation references are lost {sm;:-(}. + + +SimpleVal +--------- + +You may not need a full validation process, but still want to check if all the +expected values are present. + +Provided as part of the ConfigObj module is the ``SimpleVal`` object. This has +a dummy ``test`` method that always passes. + +The only reason a test will fail is if the value is missing. The return value +from ``validate`` will either be ``True``, meaning all present, or a dictionary +with ``False`` for all missing values/sections. + +To use it, you still need to pass in a valid configspec when you create the +ConfigObj, but just set all the values to ``''``. Then create an instance of +``SimpleVal`` and pass it to the ``validate`` method. + +As a trivial example if you had the following config file : :: + + # config file for an application + port = 80 + protocol = http + domain = voidspace + top_level_domain = org.uk + +You would write the following configspec : :: + + port = '' + protocol = '' + domain = '' + top_level_domain = '' + +.. raw:: html + + {+coloring} + + config = Configobj(filename, configspec=configspec) + val = SimpleVal() + test = config.validate(val) + if test == True: + print 'All values present.' + elif test == False: + print 'No values present!' + else: + for entry in test: + if test[entry] == False: + print '"%s" missing.' % entry + + {-coloring} + + +Empty values +============ + +Many config files from other applications allow empty values. As of version +4.3.0, ConfigObj will read these as an empty string. + +A new option/attribute has been added (``write_empty_values``) to allow +ConfigObj to write empty strings as empty values. + +.. raw:: html + + {+coloring} + from configobj import ConfigObj + cfg = ''' + key = + key2 = # a comment + '''.splitlines() + config = ConfigObj(cfg) + print config + ConfigObj({'key': '', 'key2': ''}) + + config.write_empty_values = True + for line in config.write(): + print line + + key = + key2 = # a comment + {-coloring} + + +unrepr mode +=========== + +The ``unrepr`` option allows you to store and retrieve the basic Python +data-types using config files. It has to use a slightly different syntax to +normal ConfigObj files. Unsurprisingly it uses Python syntax. + +This means that lists are different (they are surrounded by square brackets), +and strings *must* be quoted. + +The types that ``unrepr`` can work with are : + + | strings, lists tuples + | None, True, False + | dictionaries, integers, floats + | longs and complex numbers + +You can't store classes, types or instances. + +``unrepr`` uses ``repr(object)`` to write out values, so it currently *doesn't* +check that you are writing valid objects. If you attempt to read an unsupported +value, ConfigObj will raise a ``configobj.UnknownType`` exception. + +Values that are triple quoted cased. The triple quotes are removed *before* +converting. This means that you can use triple quotes to write dictionaries +over several lines in your config files. They won't be written like this +though. + +If you are writing config files by hand, for use with ``unrepr``, you should +be aware of the following differences from normal ConfigObj syntax : + + | List : ``['A List', 'With', 'Strings']`` + | Strings : ``"Must be quoted."`` + | Backslash : ``"The backslash must be escaped \\"`` + +These all follow normal Python syntax. + +In unrepr mode *inline comments* are not saved. This is because lines are +parsed using the `compiler package <http://docs.python.org/lib/compiler.html>`_ +which discards comments. + + +String Interpolation +==================== + +ConfigObj allows string interpolation *similar* to the way ``ConfigParser`` +or ``string.Template`` work. The value of the ``interpolation`` attribute +determines which style of interpolation you want to use. Valid values are +"ConfigParser" or "Template" (case-insensitive, so "configparser" and +"template" will also work). For backwards compatibility reasons, the value +``True`` is also a valid value for the ``interpolation`` attribute, and +will select ``ConfigParser``-style interpolation. At some undetermined point +in the future, that default *may* change to ``Template``-style interpolation. + +For ``ConfigParser``-style interpolation, you specify a value to be +substituted by including ``%(name)s`` in the value. + +For ``Template``-style interpolation, you specify a value to be substituted +by including ``${cl}name{cr}`` in the value. Alternately, if 'name' is a valid +Python identifier (i.e., is composed of nothing but alphanumeric characters, +plus the underscore character), then the braces are optional and the value +can be written as ``$name``. + +Note that ``ConfigParser``-style interpolation and ``Template``-style +interpolation are mutually exclusive; you cannot have a configuration file +that's a mix of one or the other. Pick one and stick to it. ``Template``-style +interpolation is simpler to read and write by hand, and is recommended if +you don't have a particular reason to use ``ConfigParser``-style. + +Interpolation checks first the current section to see if ``name`` is the key +to a value. ('name' is case sensitive). + +If it doesn't find it, next it checks the 'DEFAULT' sub-section of the current +section. + +If it still doesn't find it, it moves on to check the parent section and the +parent section's 'DEFAULT' subsection, and so on all the way up to the main +section. + +If the value specified isn't found in any of these locations, then a +``MissingInterpolationOption`` error is raised (a subclass of +``ConfigObjError``). + +If it is found then the returned value is also checked for substitutions. This +allows you to make up compound values (for example directory paths) that use +more than one default value. It also means it's possible to create circular +references. If there are any circular references which would cause an infinite +interpolation loop, an ``InterpolationLoopError`` is raised. + +Both of these errors are subclasses of ``InterpolationError``, which is a +subclass of ``ConfigObjError``. + +String interpolation and validation don't play well together. This is because +validation overwrites values - and so may erase the interpolation references. +See `Validation and Interpolation`_. (This can only happen if validation +has to *change* the value). + + +Comments +======== + +Any line that starts with a '#', possibly preceded by whitespace, is a comment. + +If a config file starts with comments then these are preserved as the +initial_comment_. + +If a config file ends with comments then these are preserved as the +final_comment_. + +Every key or section marker may have lines of comments immediately above it. +These are saved as the ``comments`` attribute of the section. Each member is a +list of lines. + +You can also have a comment inline with a value. These are saved as the +``inline_comments`` attribute of the section, with one entry per member of the +section. + +Subsections (section markers in the config file) can also have comments. + +See `Section Attributes`_ for more on these attributes. + +These comments are all written back out by the ``write`` method. + + +flatten_errors +============== + +:: + + flatten_errors(cfg, res) + +Validation_ is a powerful way of checking that the values supplied by the user +make sense. + +The validate_ method returns a results dictionary that represents pass or fail +for each value. This doesn't give you any information about *why* the check +failed. + +``flatten_errors`` is an example function that turns a results dictionary into +a flat list, that only contains values that *failed*. + +``cfg`` is the ConfigObj instance being checked, ``res`` is the results +dictionary returned by ``validate``. + +It returns a list of keys that failed. Each member of the list is a tuple : :: + + ([list of sections...], key, result) + +If ``validate`` was called with ``preserve_errors=False`` (the default) +then ``result`` will always be ``False``. + +*list of sections* is a flattened list of sections that the key was found +in. + +If the section was missing then key will be ``None``. + +If the value (or section) was missing then ``result`` will be ``False``. + +If ``validate`` was called with ``preserve_errors=True`` and a value +was present, but failed the check, then ``result`` will be the exception +object returned. You can use this as a string that describes the failure. + +For example : + + *The value "3" is of the wrong type*. + + +Example Usage +------------- + +The output from ``flatten_errors`` is a list of tuples. + +Here is an example of how you could present this information to the user. + +.. raw:: html + + {+coloring} + + vtor = validate.Validator() + # ini is your config file - cs is the configspec + cfg = ConfigObj(ini, configspec=cs) + res = cfg.validate(vtor, preserve_errors=True) + for entry in flatten_errors(cfg, res): + # each entry is a tuple + section_list, key, error = entry + if key is not None: + section_list.append(key) + else: + section_list.append('[missing section]') + section_string = ', '.join(section_list) + if error == False: + error = 'Missing value or section.' + print section_string, ' = ', error + + {-coloring} + + +ConfigObj 3 +=========== + +ConfigObj 3 is now deprecated in favour of ConfigObj 4. I can fix bugs in +ConfigObj 3 if needed, though. + +For anyone who still needs it, you can download it here: `ConfigObj 3.3.1`_ + +You can read the old docs at : `ConfigObj 3 Docs`_ + +.. _ConfigObj 3.3.1: http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=configobj3.zip +.. _ConfigObj 3 Docs: http://www.voidspace.org.uk/python/configobj3.html + + +CREDITS +======= + +ConfigObj 4 is written by (and copyright) `Michael Foord`_ and +`Nicola Larosa`_. + +Particularly thanks to Nicola Larosa for help on the config file spec, the +validation system and the doctests. + +*validate.py* was originally written by Michael Foord and Mark Andrews. + +Thanks to others for input and bugfixes. + + +LICENSE +======= + +ConfigObj, and related files, are licensed under the BSD license. This is a +very unrestrictive license, but it comes with the usual disclaimer. This is +free software: test it, break it, just don't blame us if it eats your data ! +Of course if it does, let us know and we'll fix the problem so it doesn't +happen to anyone else {sm;:-)}. :: + + Copyright (c) 2004 - 2008, Michael Foord & Nicola Larosa + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Michael Foord nor Nicola Larosa + may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You should also be able to find a copy of this license at : `BSD License`_ + +.. _BSD License: http://www.voidspace.org.uk/python/license.shtml + + +TODO +==== + +Better support for configuration from multiple files, including tracking +*where* the original file came from and writing changes to the correct +file. + +Make ``newline`` an option (as well as an attribute) ? + +``UTF16`` encoded files, when returned as a list of lines, will have the +BOM at the start of every line. Should this be removed from all but the +first line ? + +Option to set warning type for unicode decode ? (Defaults to strict). + +A method to optionally remove uniform indentation from multiline values. +(do as an example of using ``walk`` - along with string-escape) + +Should the results dictionary from validate be an ordered dictionary if +`odict <http://www.voidspace.org.uk/python/odict.html>`_ is available ? + +Implement some of the sequence methods (which include slicing) from the +newer ``odict`` ? + +Preserve line numbers of values (and possibly the original text of each value). + + +ISSUES +====== + +.. note:: + + Please file any bug reports to `Michael Foord`_ or the **ConfigObj** + `Mailing List`_. + +There is currently no way to specify the encoding of a configspec file. + +When using ``copy`` mode for validation, it won't copy ``DEFAULT`` +sections. This is so that you *can* use interpolation in configspec +files. + +``validate`` doesn't report *extra* values or sections. + +You can't have a keyword with the same name as a section (in the same +section). They are both dictionary keys - so they would overlap. + +ConfigObj doesn't quote and unquote values if ``list_values=False``. +This means that leading or trailing whitespace in values will be lost when +writing. (Unless you manually quote). + +Interpolation checks first the current section, then the 'DEFAULT' subsection +of the current section, before moving on to the current section's parent and +so on up the tree. + +Does it matter that we don't support the ':' divider, which is supported +by ``ConfigParser`` ? + +String interpolation and validation don't play well together. When +validation changes type it sets the value. This will correctly fetch the +value using interpolation - but then overwrite the interpolation reference. +If the value is unchanged by validation (it's a string) - but other types +will be. + + +CHANGELOG +========= + +This is an abbreviated changelog showing the major releases up to version 4. +From version 4 it lists all releases and changes. + + +2008/06/27 - Version 4.5.3 +-------------------------- + +BUGFIX: fixed a problem with ``copy=True`` when validating with configspecs that use +``__many__`` sections. + + +2008/02/05 - Version 4.5.2 +-------------------------- + +Distribution updated to include version 0.3.2 of validate_. This means that +``None`` as a default value win configspecs works. + + +2008/02/05 - Version 4.5.1 +-------------------------- + +Distribution updated to include version 0.3.1 of validate_. This means that +Unicode configspecs now work. + + +2008/02/05 - Version 4.5.0 +-------------------------- + +ConfigObj will now guarantee that files will be written terminated with a +newline. + +ConfigObj will no longer attempt to import the ``validate`` module, until/unless +you call ``ConfigObj.validate`` with ``preserve_errors=True``. This makes it +faster to import. + +New methods ``restore_default`` and ``restore_defaults``. ``restore_default`` +resets an entry to its default value (and returns that value). ``restore_defaults`` +resets all entries to their default value. It doesn't modify entries without a +default value. You must have validated a ConfigObj (which populates the +``default_values`` dictionary) before calling these methods. + +BUGFIX: Proper quoting of keys, values and list values that contain hashes +(when writing). When ``list_values=False``, values containing hashes are +triple quoted. + +Added the ``reload`` method. This reloads a ConfigObj from file. If the filename +attribute is not set then a ``ReloadError`` (a new exception inheriting from +``IOError``) is raised. + +BUGFIX: Files are read in with 'rb' mode, so that native/non-native line endings work! + +Minor efficiency improvement in ``unrepr`` mode. + +Added missing docstrings for some overidden dictionary methods. + +Added the ``reset`` method. This restores a ConfigObj to a freshly created state. + +Removed old CHANGELOG file. + + +2007/02/04 - Version 4.4.0 +-------------------------- + +Official release of 4.4.0 + + +2006/12/17 - Version 4.3.3-alpha4 +--------------------------------- + +By Nicola Larosa + +Allowed arbitrary indentation in the ``indent_type`` parameter, removed the +``NUM_INDENT_SPACES`` and ``MAX_INTERPOL_DEPTH`` (a leftover) constants, +added indentation tests (including another docutils workaround, sigh), updated +the documentation. + +By Michael Foord + +Made the import of ``compiler`` conditional so that ``ConfigObj`` can be used +with `IronPython <http://www.codeplex.com/IronPython>`_. + + +2006/12/17 - Version 4.3.3-alpha3 +--------------------------------- + +By Nicola Larosa + +Added a missing ``self.`` in the _handle_comment method and a related test, +per Sourceforge bug #1523975. + + +2006/12/09 - Version 4.3.3-alpha2 +--------------------------------- + +By Nicola Larosa + +Changed interpolation search strategy, based on this patch by Robin Munn: +http://sourceforge.net/mailarchive/message.php?msg_id=17125993 + + +2006/12/09 - Version 4.3.3-alpha1 +--------------------------------- + +By Nicola Larosa + +Added Template-style interpolation, with tests, based on this patch by +Robin Munn: http://sourceforge.net/mailarchive/message.php?msg_id=17125991 +(awful archives, bad Sourceforge, bad). + + +2006/06/04 - Version 4.3.2 +-------------------------- + +Changed error handling, if parsing finds a single error then that error will +be re-raised. That error will still have an ``errors`` and a ``config`` +attribute. + +Fixed bug where '\\n' terminated files could be truncated. + +Bugfix in ``unrepr`` mode, it couldn't handle '#' in values. (Thanks to +Philippe Normand for the report.) + +As a consequence of this fix, ConfigObj doesn't now keep inline comments in +``unrepr`` mode. This is because the parser in the `compiler package`_ +doesn't keep comments. {sm;:-)} + +Error messages are now more useful. They tell you the number of parsing errors +and the line number of the first error. (In the case of multiple errors.) + +Line numbers in exceptions now start at 1, not 0. + +Errors in ``unrepr`` mode are now handled the same way as in the normal mode. +The errors stored will be an ``UnreprError``. + + +2006/04/29 - Version 4.3.1 +-------------------------- + +Added ``validate.py`` back into ``configobj.zip``. (Thanks to Stewart +Midwinter) + +Updated to `validate.py`_ 0.2.2. + +Preserve tuples when calling the ``dict`` method. (Thanks to Gustavo Niemeyer.) + +Changed ``__repr__`` to return a string that contains ``ConfigObj({ ... })``. + +Change so that an options dictionary isn't modified by passing it to ConfigObj. +(Thanks to Artarious.) + +Added ability to handle negative integers in ``unrepr``. (Thanks to Kevin +Dangoor.) + + +2006/03/24 - Version 4.3.0 +-------------------------- + +Moved the tests and the CHANGELOG (etc) into a separate file. This has reduced +the size of ``configobj.py`` by about 40%. + +Added the ``unrepr`` mode to reading and writing config files. Thanks to Kevin +Dangoor for this suggestion. + +Empty values are now valid syntax. They are read as an empty string ``''``. +(``key =``, or ``key = # comment``.) + +``validate`` now honours the order of the configspec. + +Added the ``copy`` mode to validate. Thanks to Louis Cordier for this +suggestion. + +Fixed bug where files written on windows could be given ``'\r\r\n'`` line +terminators. + +Fixed bug where last occurring comment line could be interpreted as the +final comment if the last line isn't terminated. + +Fixed bug where nested list values would be flattened when ``write`` is +called. Now sub-lists have a string representation written instead. + +Deprecated ``encode`` and ``decode`` methods instead. + +You can now pass in a ConfigObj instance as a configspec (remember to read +the configspec file using ``list_values=False``). + +Sorted footnotes in the docs. + + +2006/02/16 - Version 4.2.0 +-------------------------- + +Removed ``BOM_UTF8`` from ``__all__``. + +The ``BOM`` attribute has become a boolean. (Defaults to ``False``.) It is +*only* ``True`` for the ``UTF16/UTF8`` encodings. + +File like objects no longer need a ``seek`` attribute. + +Full unicode support added. New options/attributes ``encoding``, +``default_encoding``. + +ConfigObj no longer keeps a reference to file like objects. Instead the +``write`` method takes a file like object as an optional argument. (Which +will be used in preference of the ``filename`` attribute if that exists as +well.) + +utf16 files decoded to unicode. + +If ``BOM`` is ``True``, but no encoding specified, then the utf8 BOM is +written out at the start of the file. (It will normally only be ``True`` if +the utf8 BOM was found when the file was read.) + +Thanks to Aaron Bentley for help and testing on the unicode issues. + +File paths are *not* converted to absolute paths, relative paths will +remain relative as the ``filename`` attribute. + +Fixed bug where ``final_comment`` wasn't returned if ``write`` is returning +a list of lines. + +Deprecated ``istrue``, replaced it with ``as_bool``. + +Added ``as_int`` and ``as_float``. + + +2005/12/14 - Version 4.1.0 +-------------------------- + +Added ``merge``, a recursive update. + +Added ``preserve_errors`` to ``validate`` and the ``flatten_errors`` +example function. + +Thanks to Matthew Brett for suggestions and helping me iron out bugs. + +Fixed bug where a config file is *all* comment, the comment will now be +``initial_comment`` rather than ``final_comment``. + +Validation no longer done on the 'DEFAULT' section (only in the root level). +This allows interpolation in configspecs. + +Also use the new list syntax in validate_ 0.2.1. (For configspecs). + + +2005/12/02 - Version 4.0.2 +-------------------------- + +Fixed bug in ``create_empty``. Thanks to Paul Jimenez for the report. + + +2005/11/05 - Version 4.0.1 +-------------------------- + +Fixed bug in ``Section.walk`` when transforming names as well as values. + +Added the ``istrue`` method. (Fetches the boolean equivalent of a string +value). + +Fixed ``list_values=False`` - they are now only quoted/unquoted if they +are multiline values. + +List values are written as ``item, item`` rather than ``item,item``. + + +2005/10/17 - Version 4.0.0 +-------------------------- + +**ConfigObj 4.0.0 Final** + +Fixed bug in ``setdefault``. When creating a new section with setdefault the +reference returned would be to the dictionary passed in *not* to the new +section. Bug fixed and behaviour documented. + +Obscure typo/bug fixed in ``write``. Wouldn't have affected anyone though. + + +2005/09/09 - Version 4.0.0 beta 5 +--------------------------------- + +Removed ``PositionError``. + +Allowed quotes around keys as documented. + +Fixed bug with commas in comments. (matched as a list value) + + +2005/09/07 - Version 4.0.0 beta 4 +--------------------------------- + +Fixed bug in ``__delitem__``. Deleting an item no longer deletes the +``inline_comments`` attribute. + +Fixed bug in initialising ConfigObj from a ConfigObj. + +Changed the mailing list address. + + +2005/08/28 - Version 4.0.0 beta 3 +--------------------------------- + +Interpolation is switched off before writing out files. + +Fixed bug in handling ``StringIO`` instances. (Thanks to report from +Gustavo Niemeyer.) + +Moved the doctests from the ``__init__`` method to a separate function. +(For the sake of IDE calltips). + + +2005/08/25 - Version 4.0.0 beta 2 +--------------------------------- + +Amendments to *validate.py*. + +First public release. + + +2005/08/21 - Version 4.0.0 beta 1 +--------------------------------- + +Reads nested subsections to any depth. + +Multiline values. + +Simplified options and methods. + +New list syntax. + +Faster, smaller, and better parser. + +Validation greatly improved. Includes: + + * type conversion + * default values + * repeated sections + +Improved error handling. + +Plus lots of other improvements. {sm;:grin:} + + +2004/05/24 - Version 3.0.0 +-------------------------- + +Several incompatible changes: another major overhaul and change. (Lots of +improvements though). + +Added support for standard config files with sections. This has an entirely +new interface: each section is a dictionary of values. + +Changed the update method to be called writein: update clashes with a dict +method. + +Made various attributes keyword arguments, added several. + +Configspecs and orderlists have changed a great deal. + +Removed support for adding dictionaries: use update instead. + +Now subclasses a new class called caselessDict. This should add various +dictionary methods that could have caused errors before. + +It also preserves the original casing of keywords when writing them back out. + +Comments are also saved using a ``caselessDict``. + +Using a non-string key will now raise a ``TypeError`` rather than converting +the key. + +Added an exceptions keyword for *much* better handling of errors. + +Made ``creatempty=False`` the default. + +Now checks indict *and* any keyword args. Keyword args take precedence over +indict. + +``' ', ':', '=', ','`` and ``'\t'`` are now all valid dividers where the +keyword is unquoted. + +ConfigObj now does no type checking against configspec when you set items. + +delete and add methods removed (they were unnecessary). + +Docs rewritten to include all this gumph and more; actually ConfigObj is +*really* easy to use. + +Support for stdout was removed. + +A few new methods added. + +Charmap is now incorporated into ConfigObj. + + +2004/03/14 - Version 2.0.0 beta +------------------------------- + +Re-written it to subclass dict. My first forays into inheritance and operator +overloading. + +The config object now behaves like a dictionary. + +I've completely broken the interface, but I don't think anyone was really +using it anyway. + +This new version is much more 'classy'. {sm;:wink:} + +It will also read straight from/to a filename and completely parse a config +file without you *having* to supply a config spec. + +Uses listparse, so can handle nested list items as values. + +No longer has getval and setval methods: use normal dictionary methods, or add +and delete. + + +2004/01/29 - Version 1.0.5 +-------------------------- + +Version 1.0.5 has a couple of bugfixes as well as a couple of useful additions +over previous versions. + +Since 1.0.0 the buildconfig function has been moved into this distribution, +and the methods reset, verify, getval and setval have been added. + +A couple of bugs have been fixed. + + +Origins +------- + +ConfigObj originated in a set of functions for reading config files in the +`atlantibots <http://www.voidspace.org.uk/atlantibots/>`_ project. The original +functions were written by Rob McNeur. + + +---------- + + +Footnotes +========= + +.. [#] And if you discover any bugs, let us know. We'll fix them quickly. + +.. [#] If you specify a filename that doesn't exist, ConfigObj will assume you + are creating a new one. See the *create_empty* and *file_error* options_. + +.. [#] They can be byte strings (*ordinary* strings) or Unicode. + +.. [#] Except we don't support the RFC822 style line continuations, nor ':' as + a divider. + +.. [#] This is a change in ConfigObj 4.2.0. Note that ConfigObj doesn't call + the seek method of any file like object you pass in. You may want to call + ``file_object.seek(0)`` yourself, first. + +.. [#] A side effect of this is that it enables you to copy a ConfigObj : + + .. raw:: html + + {+coloring} + + # only copies members + # not attributes/comments + config2 = ConfigObj(config1) + + {-coloring} + + The order of values and sections will not be preserved, though. + +.. [#] Other than lists of strings. + +.. [#] The exception is if it detects a ``UTF16`` encoded file which it + must decode before parsing. + +.. [#] The method signature shows that this method takes + two arguments. The second is the section to be written. This is because the + ``write`` method is called recursively. + +.. [#] The dict method doesn't actually use the deepcopy mechanism. This means + if you add nested lists (etc) to your ConfigObj, then the dictionary + returned by dict may contain some references. For all *normal* ConfigObjs + it will return a deepcopy. + +.. [#] Passing ``(section, key)`` rather than ``(value, key)`` allows you to + change the value by setting ``section[key] = newval``. It also gives you + access to the *rename* method of the section. + +.. [#] Minimum required version of *validate.py* 0.2.0 . + + +.. note:: + + Rendering this document with docutils also needs the + textmacros module and the PySrc CSS stuff. See + http://www.voidspace.org.uk/python/firedrop2/textmacros.shtml + + +.. raw:: html + + <div align="center"> + <p> + <a href="http://www.python.org"> + <img src="images/new_python.gif" width="100" height="103" border="0" + alt="Powered by Python" /> + </a> + <a href="http://sourceforge.net"> + <img src="http://sourceforge.net/sflogo.php?group_id=123265&type=1" width="88" height="31" border="0" alt="SourceForge.net Logo" /> + </a> + <a href="http://www.opensource.org"> + <img src="images/osi-certified-120x100.gif" width="120" height="100" border="0" + alt="Certified Open Source"/> + </a> + </p> + <p> + <a href="http://www.voidspace.org.uk/python/index.shtml"> + <img src="images/pythonbanner.gif" width="468" height="60" + alt="Python on Voidspace" border="0" /> + </a> + </p> + + </div> + +.. _listquote: http://www.voidspace.org.uk/python/modules.shtml#listquote +.. _Michael Foord: http://www.voidspace.org.uk/python/weblog/index.shtml +.. _Nicola Larosa: http://www.teknico.net diff --git a/docs/validate.txt b/docs/validate.txt new file mode 100644 index 0000000..cbb3ce2 --- /dev/null +++ b/docs/validate.txt @@ -0,0 +1,735 @@ +=================================== + Validation Schema with validate.py +=================================== + +-------------------------- + Using the Validator class +-------------------------- + + +:Authors: `Michael Foord`_, `Nicola Larosa`_, `Mark Andrews`_ +:Version: Validate 0.3.2 +:Date: 2008/02/24 +:Homepage: `Validate Homepage`_ +:License: `BSD License`_ +:Support: `Mailing List`_ + +.. _Mailing List: http://lists.sourceforge.net/lists/listinfo/configobj-develop +.. _Michael Foord: fuzzyman@voidspace.org.uk +.. _Nicola Larosa: nico@teknico.net +.. _This Document: +.. _Validate Homepage: http://www.voidspace.org.uk/python/validate.html +.. _BSD License: http://www.voidspace.org.uk/python/license.shtml + + +.. contents:: Validate Manual +.. sectnum:: + + +Introduction +============ + +Validation is used to check that supplied values conform to a specification. + +The value can be supplied as a string, e.g. from a config file. In this case +the check will also *convert* the value to the required type. This allows you +to add validation as a transparent layer to access data stored as strings. The +validation checks that the data is correct *and* converts it to the expected +type. + +Checks are also strings, and are easy to write. One generic system can be used +to validate information from different sources via a single consistent +mechanism. + +Checks look like function calls, and map to function calls. They can include +parameters and keyword arguments. These arguments are passed to the relevant +function by the ``Validator`` instance, along with the value being checked. + +The syntax for checks also allows for specifying a default value. This default +value can be ``None``, no matter what the type of the check. This can be used +to indicate that a value was missing, and so holds no useful value. + +Functions either return a new value, or raise an exception. See `Validator +Exceptions`_ for the low down on the exception classes that ``validate.py`` +defines. + +Some standard functions are provided, for basic data types; these come built +into every validator. Additional checks are easy to write: they can be provided +when the ``Validator`` is instantiated, or added afterwards. + +Validate was primarily written to support ConfigObj_, but is designed to be +applicable to many other situations. + +For support and bug reports please use the ConfigObj `Mailing List`_. + +.. _ConfigObj: http://www.voidspace.org.uk/python/configobj.html + + +Downloading +=========== + +The current version is **0.3.2**, dated 24th February 2008. + +You can get obtain validate in the following ways : + + +Files +----- + +* validate.py_ from Voidspace + +* configobj.zip from Voidspace - See the homepage of ConfigObj_ for the latest + version and download links. + + This contains validate.py and `this document`_. (As well as ConfigObj_ and + the ConfigObj documentation). + +* The latest development version can be obtained from the `Subversion Repository`_. + + +Documentation +------------- + +*configobj.zip* contains `this document`_. + +* You can view `this document`_ online as the `Validate Homepage`_. + + +Pythonutils +----------- + +Validate_ is also part of the Pythonutils_ set of modules. This contains +various other useful helper modules, and is required by many of the `Voidspace +Python Projects`_. + +.. _configobj.py: http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=configobj.py +.. _configobj.zip: http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=configobj-4.5.3.zip +.. _validate.py: http://www.voidspace.org.uk/cgi-bin/voidspace/downman.py?file=validate.py +.. _Subversion Repository: http://svn.pythonutils.python-hosting.com/trunk/pythonutils/ +.. _Sourceforge: http://sourceforge.net/projects/configobj +.. _pythonutils: http://www.voidspace.org.uk/python/pythonutils.html +.. _Voidspace Python Projects: http://www.voidspace.org.uk/python +.. _validate: http://www.voidspace.org.uk/python/validate.html + + +The standard functions +====================== + +The standard functions come built-in to every ``Validator`` instance. They work +with the following basic data types : + +* integer +* float +* boolean +* string +* ip_addr + +plus lists of these datatypes. + +Adding additional checks is done through coding simple functions. + +The full set of standard checks are : + +:'integer': matches integer values (including negative). Takes optional 'min' + and 'max' arguments : :: + + integer() + integer(3, 9) # any value from 3 to 9 + integer(min=0) # any positive value + integer(max=9) + +:'float': matches float values + Has the same parameters as the integer check. + +:'boolean': matches boolean values: ``True`` or ``False``. + Acceptable string values for True are : :: + + true, on, yes, 1 + + Acceptable string values for False are : :: + + false, off, no, 0 + + Any other value raises an error. + +:'string': matches any string. Takes optional keyword args 'min' and 'max' to + specify min and max length of string. + +:'ip_addr': matches an Internet Protocol address, v.4, represented by a + dotted-quad string, i.e. '1.2.3.4'. + +:'list': matches any list. Takes optional keyword args 'min', and 'max' to + specify min and max sizes of the list. The list checks always + return a list. + +:'tuple': matches any list. This check returns a tuple rather than a list. + +:'int_list': Matches a list of integers. Takes the same arguments as list. + +:'float_list': Matches a list of floats. Takes the same arguments as list. + +:'bool_list': Matches a list of boolean values. Takes the same arguments as + list. + +:'string_list': Matches a list of strings. Takes the same arguments as list. + +:'ip_addr_list': Matches a list of IP addresses. Takes the same arguments as + list. + +:'mixed_list': Matches a list with different types in specific positions. + List size must match the number of arguments. + + Each position can be one of : :: + + int, str, boolean, float, ip_addr + + So to specify a list with two strings followed by two integers, + you write the check as : :: + + mixed_list(str, str, int, int) + +:'pass': matches everything: it never fails and the value is unchanged. It is + also the default if no check is specified. + +:'option': matches any from a list of options. + You specify this test with : :: + + option('option 1', 'option 2', 'option 3') + +The following code will work without you having to specifically add the +functions yourself. + +.. raw:: html + + {+coloring} + + from validate import Validator + # + vtor = Validator() + newval1 = vtor.check('integer', value1) + newval2 = vtor.check('boolean', value2) + # etc ... + + {-coloring} + +.. note:: + + Of course, if these checks fail they raise exceptions. So you should wrap + them in ``try...except`` blocks. Better still, use ConfigObj for a higher + level interface. + + +Using Validator +=============== + +Using ``Validator`` is very easy. It has one public attribute and one public +method. + +Shown below are the different steps in using ``Validator``. + +The only additional thing you need to know, is about `Writing check +functions`_. + +Instantiate +----------- + +.. raw:: html + + {+coloring} + + from validate import Validator + vtor = Validator() + + {-coloring} + +or even : + +.. raw:: html + + {+coloring} + + from validate import Validator + # + fdict = { + 'check_name1': function1, + 'check_name2': function2, + 'check_name3': function3, + } + # + vtor = Validator(fdict) + + {-coloring} + +The second method adds a set of your functions as soon as your validator is +created. They are stored in the ``vtor.functions`` dictionary. The 'key' you +give them in this dictionary is the name you use in your checks (not the +original function name). + +Dictionary keys/functions you pass in can override the built-in ones if you +want. + + +Adding functions +---------------- + +The code shown above, for adding functions on instantiation, has exactly the +same effect as the following code : + +.. raw:: html + + {+coloring} + + from validate import Validator + # + vtor = Validator() + vtor.functions['check_name1'] = function1 + vtor.functions['check_name2'] = function2 + vtor.functions['check_name3'] = function3 + + {-coloring} + +``vtor.functions`` is just a dictionary that maps names to functions, so we +could also have called ``vtor.functions.update(fdict)``. + + +Writing the check +----------------- + +As we've heard, the checks map to the names in the ``functions`` dictionary. +You've got a full list of `The standard functions`_ and the arguments they +take. + +If you're using ``Validator`` from ConfigObj, then your checks will look like +: :: + + keyword = int_list(max=6) + +but the check part will be identical . + + +The check method +---------------- + +If you're not using ``Validator`` from ConfigObj, then you'll need to call the +``check`` method yourself. + +If the check fails then it will raise an exception, so you'll want to trap +that. Here's the basic example : + +.. raw:: html + + {+coloring} + + from validate import Validator, ValidateError + # + vtor = Validator() + check = "integer(0, 9)" + value = 3 + try: + newvalue = vtor.check(check, value) + except ValidateError: + print 'Check Failed.' + else: + print 'Check passed.' + + {-coloring} + +.. caution:: + + Although the value can be a string, if it represents a list it should + already have been turned into a list of strings. + + +Default Values +~~~~~~~~~~~~~~ + +Some values may not be available, and you may want to be able to specify a +default as part of the check. + +You do this by passing the keyword ``missing=True`` to the ``check`` method, as +well as a ``default=value`` in the check. (Constructing these checks is done +automatically by ConfigObj: you only need to know about the ``default=value`` +part) : + +.. raw:: html + + {+coloring} + + check1 = 'integer(default=50)' + check2 = 'option("val 1", "val 2", "val 3", default="val 1")' + + assert vtor.check(check1, '', missing=True) == 50 + assert vtor.check(check2, '', missing=True) == "val 1" + + {-coloring} + +If you pass in ``missing=True`` to the check method, then the actual value is +ignored. If no default is specified in the check, a ``ValidateMissingValue`` +exception is raised. If a default is specified then that is passed to the +check instead. + +If the check has ``default=None`` (case sensitive) then ``vtor.check`` will +*always* return ``None`` (the object). This makes it easy to tell your program +that this check contains no useful value when missing, i.e. the value is +optional, and may be omitted without harm. + + +.. note:: + + As of version 0.3.0, if you specify ``default='None'`` (note the quote marks + around ``None``) then it will be interpreted as the string ``'None'``. + + +List Values +~~~~~~~~~~~ + +It's possible that you would like your default value to be a list. It's even +possible that you will write your own check functions - and would like to pass +them keyword arguments as lists from within the check. + +To avoid confusing syntax with commas and quotes you use a list constructor to +specify that keyword arguments are lists. This includes the ``default`` value. +This makes checks look something like : :: + + checkname(default=list('val1', 'val2', 'val3')) + + +get_default_value +----------------- + +``Validator`` instances have a ``get_default_value`` method. It takes a ``check`` string +(the same string you would pass to the ``check`` method) and returns the default value, +converted to the right type. If the check doesn't define a default value then this method +raises a ``KeyError``. + +If the ``check`` has been seen before then it will have been parsed and cached already, +so this method is not expensive to call (however the conversion is done each time). + + + +Validator Exceptions +==================== + +.. note:: + + If you only use Validator through ConfigObj, it traps these Exceptions for + you. You will still need to know about them for writing your own check + functions. + +``vtor.check`` indicates that the check has failed by raising an exception. +The appropriate error should be raised in the check function. + +The base error class is ``ValidateError``. All errors (except for ``VdtParamError``) +raised are sub-classes of this. + +If an unrecognised check is specified then ``VdtUnknownCheckError`` is +raised. + +There are also ``VdtTypeError`` and ``VdtValueError``. + +If incorrect parameters are passed to a check function then it will (or should) +raise ``VdtParamError``. As this indicates *programmer* error, rather than an error +in the value, it is a subclass of ``SyntaxError`` instead of ``ValidateError``. + +.. note:: + + This means it *won't* be caught by ConfigObj - but propagated instead. + +If the value supplied is the wrong type, then the check should raise +``VdtTypeError``. e.g. the check requires the value to be an integer (or +representation of an integer) and something else was supplied. + +If the value supplied is the right type, but an unacceptable value, then the +check should raise ``VdtValueError``. e.g. the check requires the value to +be an integer (or representation of an integer) less than ten and a higher +value was supplied. + +Both ``VdtTypeError`` and ``VdtValueError`` are initialised with the +incorrect value. In other words you raise them like this : + +.. raw:: html + + {+coloring} + + raise VdtTypeError(value) + # + raise VdtValueError(value) + + {-coloring} + +``VdtValueError`` has the following subclasses, which should be raised if +they are more appropriate. + +* ``VdtValueTooSmallError`` +* ``VdtValueTooBigError`` +* ``VdtValueTooShortError`` +* ``VdtValueTooLongError`` + + +Writing check functions +======================= + +Writing check functions is easy. + +The check function will receive the value as its first argument, followed by +any other parameters and keyword arguments. + +If the check fails, it should raise a ``VdtTypeError`` or a +``VdtValueError`` (or an appropriate subclass). + +All parameters and keyword arguments are *always* passed as strings. (Parsed +from the check string). + +The value might be a string (or list of strings) and need +converting to the right type - alternatively it might already be a list of +integers. Our function needs to be able to handle either. + +If the check passes then it should return the value (possibly converted to the +right type). + +And that's it ! + + +Example +------- + +Here is an example function that requires a list of integers. Each integer +must be between 0 and 99. + +It takes a single argument specifying the length of the list. (Which allows us +to use the same check in more than one place). If the length can't be converted +to an integer then we need to raise ``VdtParamError``. + +Next we check that the value is a list. Anything else should raise a +``VdtTypeError``. The list should also have 'length' entries. If the list +has more or less entries then we will need to raise a +``VdtValueTooShortError`` or a ``VdtValueTooLongError``. + +Then we need to check every entry in the list. Each entry should be an integer +between 0 and 99, or a string representation of an integer between 0 and 99. +Any other type is a ``VdtTypeError``, any other value is a +``VdtValueError`` (either too big, or too small). + +.. raw:: html + + {+coloring} + + def special_list(value, length): + """ + Check that the supplied value is a list of integers, + with 'length' entries, and each entry between 0 and 99. + """ + # length is supplied as a string + # we need to convert it to an integer + try: + length = int(length) + except ValueError: + raise VdtParamError('length', length) + # + # Check the supplied value is a list + if not isinstance(value, list): + raise VdtTypeError(value) + # + # check the length of the list is correct + if len(value) > length: + raise VdtValueTooLongError(value) + elif len(value) < length: + raise VdtValueTooShortError(value) + # + # Next, check every member in the list + # converting strings as necessary + out = [] + for entry in value: + if not isinstance(entry, (str, unicode, int)): + # a value in the list + # is neither an integer nor a string + raise VdtTypeError(value) + elif isinstance(entry, (str, unicode)): + if not entry.isdigit(): + raise VdtTypeError(value) + else: + entry = int(entry) + if entry < 0: + raise VdtValueTooSmallError(value) + elif entry > 99: + raise VdtValueTooBigError(value) + out.append(entry) + # + # if we got this far, all is well + # return the new list + return out + + {-coloring} + +If you are only using validate from ConfigObj then the error type (*TooBig*, +*TooSmall*, etc) is lost - so you may only want to raise ``VdtValueError``. + +.. caution:: + + If your function raises an exception that isn't a subclass of + ``ValidateError``, then ConfigObj won't trap it. This means validation will + fail. + + This is why our function starts by checking the type of the value. If we + are passed the wrong type (e.g. an integer rather than a list) we get a + ``VdtTypeError`` rather than bombing out when we try to iterate over + the value. + +If you are using validate in another circumstance you may want to create your +own subclasses of ``ValidateError``, that convey more specific information. + + +Known Issues +============ + +The following parses and then blows up. The resulting error message +is confusing: + + ``checkname(default=list(1, 2, 3, 4)`` + +This is because it parses as: ``checkname(default="list(1", 2, 3, 4)``. +That isn't actually unreasonable, but the error message won't help you +work out what has happened. + + +TODO +==== + +* A regex check function ? +* A timestamp check function ? (Using the ``parse`` function from ``DateUtil`` perhaps). + + +ISSUES +====== + +.. note:: + + Please file any bug reports to `Michael Foord`_ or the ConfigObj + `Mailing List`_. + +If we could pull tuples out of arguments, it would be easier +to specify arguments for 'mixed_lists'. + + +CHANGELOG +========= + +2008/02/24 - Version 0.3.2 +-------------------------- + +BUGFIX: Handling of None as default value fixed. + + +2008/02/05 - Version 0.3.1 +-------------------------- + +BUGFIX: Unicode checks no longer broken. + + +2008/02/05 - Version 0.3.0 +-------------------------- + +Improved performance with a parse cache. + +New ``get_default_value`` method. Given a check it returns the default +value (converted to the correct type) or raises a ``KeyError`` if the +check doesn't specify a default. + +Added 'tuple' check and corresponding 'is_tuple' function (which always returns a tuple). + +BUGFIX: A quoted 'None' as a default value is no longer treated as None, +but as the string 'None'. + +BUGFIX: We weren't unquoting keyword arguments of length two, so an +empty string didn't work as a default. + +BUGFIX: Strings no longer pass the 'is_list' check. Additionally, the +list checks always return lists. + +A couple of documentation bug fixes. + +Removed CHANGELOG from module. + + +2007/02/04 Version 0.2.3 +----------------------------- + +Release of 0.2.3 + + +2006/12/17 Version 0.2.3-alpha1 +------------------------------------ + +By Nicola Larosa + +Fixed validate doc to talk of ``boolean`` instead of ``bool``; changed the +``is_bool`` function to ``is_boolean`` (Sourceforge bug #1531525). + + +2006/04/29 Version 0.2.2 +----------------------------- + +Addressed bug where a string would pass the ``is_list`` test. (Thanks to +Konrad Wojas.) + + +2005/12/16 Version 0.2.1 +----------------------------- + +Fixed bug so we can handle keyword argument values with commas. + +We now use a list constructor for passing list values to keyword arguments +(including ``default``) : :: + + default=list("val", "val", "val") + +Added the ``_test`` test. {sm;:-)} + +Moved a function call outside a try...except block. + + +2005/08/18 Version 0.2.0 +----------------------------- + +Updated by `Michael Foord`_ and `Nicola Larosa`_ + +Does type conversion as well. + + +2005/02/01 Version 0.1.0 +----------------------------- + +Initial version developed by `Michael Foord`_ +and Mark Andrews. + +.. note:: + + Rendering this document with docutils also needs the + textmacros module and the PySrc CSS stuff. See + http://www.voidspace.org.uk/python/firedrop2/textmacros.shtml + + +.. raw:: html + + <div align="center"> + <p> + <a href="http://www.python.org"> + <img src="images/new_python.gif" width="100" height="103" border="0" + alt="Powered by Python" /> + </a> + <a href="http://sourceforge.net"> + <img src="http://sourceforge.net/sflogo.php?group_id=123265&type=1" width="88" height="31" border="0" alt="SourceForge.net Logo" /> + </a> + <a href="http://www.opensource.org"> + <img src="images/osi-certified-120x100.gif" width="120" height="100" border="0" + alt="Certified Open Source"/> + </a> + </p> + <p> + <a href="http://www.voidspace.org.uk/python/index.shtml"> + <img src="images/pythonbanner.gif" width="468" height="60" + alt="Python on Voidspace" border="0" /> + </a> + </p> + </div> + diff --git a/extras/ConfigPersist.py b/extras/ConfigPersist.py new file mode 100644 index 0000000..42dca37 --- /dev/null +++ b/extras/ConfigPersist.py @@ -0,0 +1,242 @@ +# ConfigPersist.py +# Functions for using ConfigObj for data persistence +# Copyright (C) 2005 Michael Foord +# E-mail: fuzzyman AT voidspace DOT org DOT uk + +# 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. + +""" +Functions for doing data persistence with ConfigObj. + +It requires access to the validate module and ConfigObj. +""" + +__version__ = '0.1.0' + +__all__ = ( + 'add_configspec', + 'write_configspec', + 'add_typeinfo', + 'typeinfo_to_configspec', + 'vtor', + 'store', + 'restore', + 'save_configspec', + '__version__' + ) + +from configobj import ConfigObj + +try: + from validate import Validator +except ImportError: + vtor = None +else: + vtor = Validator() + +def add_configspec(config): + """ + A function that adds a configspec to a ConfigObj. + + Will only work for ConfigObj instances using basic datatypes : + + * floats + * strings + * ints + * booleans + * Lists of the above + """ + config.configspec = {} + for entry in config: + val = config[entry] + if isinstance(val, dict): + # a subsection + add_configspec(val) + elif isinstance(val, bool): + config.configspec[entry] = 'boolean' + elif isinstance(val, int): + config.configspec[entry] = 'integer' + elif isinstance(val, float): + config.configspec[entry] = 'float' + elif isinstance(val, str): + config.configspec[entry] = 'string' + elif isinstance(val, (list, tuple)): + list_type = None + out_list = [] + for mem in val: + if isinstance(mem, str): + this = 'string' + elif isinstance(mem, bool): + this = 'boolean' + elif isinstance(mem, int): + this = 'integer' + elif isinstance(mem, float): + this = 'float' + else: + raise TypeError('List member "%s" is an innapropriate type.' % mem) + if list_type and this != list_type: + list_type = 'mixed' + elif list_type is None: + list_type = this + out_list.append(this) + if list_type is None: + l = 'list(%s)' + else: + list_type = {'integer': 'int', 'boolean': 'bool', + 'mixed': 'mixed', 'float': 'float', + 'string': 'string' }[list_type] + l = '%s_list(%%s)' % list_type + config.configspec[entry] = l % str(out_list)[1:-1] + # + else: + raise TypeError('Value "%s" is an innapropriate type.' % val) + +def write_configspec(config): + """Return the configspec (of a ConfigObj) as a list of lines.""" + out = [] + for entry in config: + val = config[entry] + if isinstance(val, dict): + # a subsection + m = config.main._write_marker('', val.depth, entry, '') + out.append(m) + out += write_configspec(val) + else: + name = config.main._quote(entry, multiline=False) + out.append("%s = %s" % (name, config.configspec[entry])) + # + return out + +def add_typeinfo(config): + """ + Turns the configspec attribute of each section into a member of the + section. (Called ``__types__``). + + You must have already called ``add_configspec`` on the ConfigObj. + """ + for entry in config.sections: + add_typeinfo(config[entry]) + config['__types__'] = config.configspec + +def typeinfo_to_configspec(config): + """Turns the '__types__' member of each section into a configspec.""" + for entry in config.sections: + if entry == '__types__': + continue + typeinfo_to_configspec(config[entry]) + config.configspec = config['__types__'] + del config['__types__'] + +def store(config): + """" + Passed a ConfigObj instance add type info and save. + + Returns the result of calling ``config.write()``. + """ + add_configspec(config) + add_typeinfo(config) + return config.write() + +def restore(stored): + """ + Restore a ConfigObj saved using the ``store`` function. + + Takes a filename or list of lines, returns the ConfigObj instance. + + Uses the built-in Validator instance of this module (vtor). + + Raises an ImportError if the validate module isn't available + """ + if vtor is None: + raise ImportError('Failed to import the validate module.') + config = ConfigObj(stored) + typeinfo_to_configspec(config) + config.validate(vtor) + return config + +def save_configspec(config): + """Creates a configspec and returns it as a list of lines.""" + add_configspec(config) + return write_configspec(config) + +def _test(): + """ + A dummy function for the sake of doctest. + + First test add_configspec + >>> from configobj import ConfigObj + >>> from validate import Validator + >>> vtor = Validator() + >>> config = ConfigObj() + >>> config['member 1'] = 3 + >>> config['member 2'] = 3.0 + >>> config['member 3'] = True + >>> config['member 4'] = [3, 3.0, True] + >>> add_configspec(config) + >>> assert config.configspec == { 'member 2': 'float', + ... 'member 3': 'boolean', 'member 1': 'integer', + ... 'member 4': "mixed_list('integer', 'float', 'boolean')"} + >>> assert config.validate(vtor) == True + + Next test write_configspec - including a nested section + >>> config['section 1'] = config.copy() + >>> add_configspec(config) + >>> a = config.write() + >>> configspec = write_configspec(config) + >>> b = ConfigObj(a, configspec=configspec) + >>> assert b.validate(vtor) == True + >>> assert b == config + + Next test add_typeinfo and typeinfo_to_configspec + >>> orig = ConfigObj(config) + >>> add_typeinfo(config) + >>> a = ConfigObj(config.write()) + >>> typeinfo_to_configspec(a) + >>> assert a.validate(vtor) == True + >>> assert a == orig + >>> typeinfo_to_configspec(config) + >>> assert config.validate(vtor) == True + >>> assert config == orig + + Test store and restore + >>> a = store(config) + >>> b = restore(a) + >>> assert b == orig + + Test save_configspec + >>> a = save_configspec(orig) + >>> b = ConfigObj(b, configspec=a) + >>> b.validate(vtor) + 1 + """ + +if __name__ == '__main__': + # run the code tests in doctest format + # + import doctest + doctest.testmod() + +""" +ISSUES +====== + +TODO +==== + + +CHANGELOG +========= + +2005/09/07 +---------- + +Module created. + +"""
\ No newline at end of file diff --git a/extras/configpersist.txt b/extras/configpersist.txt new file mode 100644 index 0000000..196b209 --- /dev/null +++ b/extras/configpersist.txt @@ -0,0 +1,266 @@ +================================= + Data Persistence with ConfigObj +================================= +-------------------------- + The ConfigPersist Module +-------------------------- + +:Author: Michael Foord +:Contact: fuzzyman@voidspace.org.uk +:Version: 0.1.0 +:Date: 2005/09/07 +:License: `BSD License`_ [#]_ +:Online Version: `ConfigPersist online`_ + +.. _`configpersist online`: http://www.voidspace.org.uk/python/configpersist.html +.. _BSD License: BSD-LICENSE.txt + +.. contents:: Data Persistence + +Introduction +============ + +This module contains various functions for data persistence with ConfigObj_. + +ConfigObj_ is a pure python module for the easy reading and writing of +application configuration data. It uses an *ini* file like syntax - similar to +the ConfigParser_ module - but with much greater power. + +ConfigObj in conjunction with validate_ can store nested sections (like +dictionaries) - with values as integers, floats, strings, booleans, and lists +of these values. This makes it ideal for certain types of human readable (and +writeable) data persistence. + +For a discussion of this idea (out of which this module was born) - see +`ConfigObj for Data Persistence`_. + +You can find ConfigObj, and other useful modules, over at the +`Voidspace Modules Page`_. + +.. _Voidspace Modules Page: /python/modules.shtml +.. _validate: /python/validate.html +.. _ConfigParser: http://docs.python.org/lib/module-configparser.html +.. _ConfigObj for Data Persistence: /python/articles/configobj_for_data_persistence.shtml +.. _ConfigObj: /python/configobj.html + +Downloading +=========== + +You can download the ConfigPersist module from : ConfigPersist.py_ + +.. _ConfigPersist.py: /cgi-bin/voidspace/downman.py?file=ConfigPersist.py + +Limitations +=========== + +.. hint:: + + This is an extract from the `ConfigObj for Data Persistence`_ article. + +ConfigObj can't just be used to represent arbitrary data structures - even if +all the members are allowed types. + + * Although dictionaries can be nested, they can't be inside lists. + * Lists also can't be nested inside each other [#]_. + * Values other than strings need a schema (a ``configspec``) to convert + them back into the right type. + * Dictionary keys must be strings. + * It is actually impossible to store a string containing single triple + quotes (``'''``) *and* double triple quotes (``"""``). + * List members cannot contain carriage returns. (Single line values only). [#]_ + +ConfigObj *isn't* a data persistence module - this list of restrictions tells +you that much. However if you examine the typical data structures used in your +programs you may find that these restrictions aren't a problem for many of them. + +So Why Do It ? +-------------- + +Why would we want to do this ? Well, the usual method for preserving data +structures is the Python pickle_ module. This can store and retrieve a much +wider range of objects - with *none* of the restrictions above. + +However : + + * Pickles aren't human readable or writeable. This makes ConfigObj ideal + for debugging or where you want to manually modify the data. + * Pickles are unsafe - a maliciously crafted pickle can cause arbitrary + code execution. + * ConfigObj is slightly easier to use - ``data = ConfigObj(filename)`` and + ``data.write()``. + +Of these three reasons the first is overwhelmingly the most compelling. + +.. _pickle: http://docs.python.org/lib/module-pickle.html + +The Functions +============= + +The first three functions provide the highest level interface to this module. +You can use these without needing to the other functions. + +save_configspec_ could be useful to anyone using the ConfigObj module - not +just for data persistence. + + +store +----- + +:: + + store(config) + + +Passed a ConfigObj instance add type info and save. + +Returns the result of calling ``config.write()``. + +.. caution:: + + This function modifies the ConfigObj instance by adding the ``__types__`` + data. + + You can call typeinfo_to_configspec_ to reverse this. + + +restore +------- + +:: + + restore(stored) + +Restore a ConfigObj saved using the ``store`` function. + +Takes a filename or list of lines, returns the ConfigObj instance. + +Uses the built-in ``Validator`` instance of this module (vtor). + +Raises an ``ImportError`` if the validate module isn't available. + + +save_configspec +--------------- + +:: + + save_configspec(config) + +Creates a configspec for a ConfigObj (which must be comprised of the basic +datatypes) and returns it as a list of lines. + +Lower Level Functions +--------------------- + +These functions provide a slightly lower level interface to adding type info to +a ConfigObj. They are still very easy to use though. + +add_configspec +~~~~~~~~~~~~~~ + +:: + + add_configspec(config) + +A function that adds a configspec to a ConfigObj. + +Will only work for ConfigObj instances using basic datatypes : + + * floats + * strings + * ints + * booleans + * Lists of the above + +write_configspec +~~~~~~~~~~~~~~~~ + +:: + + write_configspec(config) + +Return the configspec (of a ConfigObj) as a list of lines. + +You must first call ``add_configspec``. You can use save_configspec_ which does +both in one step. + +add_typeinfo +~~~~~~~~~~~~ + +:: + + add_typeinfo(config) + +Turns the configspec attribute of each section into a member of the +section. (Called ``__types__``). + +You must have already called ``add_configspec`` on the ConfigObj. + +typeinfo_to_configspec +~~~~~~~~~~~~~~~~~~~~~~ + +:: + + typeinfo_to_configspec(config) + +Turns the ``__types__`` member of each section into a configspec. + +(The opposite of ``add_typeinfo``). + +vtor +~~~~ + +This object isn't actually a function - it's the ``Validator`` instance used +by this module. + +If the validate module isn't available - this object will be ``None``. + + +CHANGELOG +========= + +See the source code for CHANGELOG (and TODO/ISSUES). + +Footnotes +========= + +.. [#] Online at http://www.voidspace.org.uk/python/license.shtml +.. [#] We could remove this restriction by using the listquote_ module to parse + ConfigObj values. Unfortunately a bug in ConfigObj means that this is + currently not possible. +.. [#] List members can also not contain both types of quote. We could remove + these last two restrictions using the ``quote_unescape`` function from + listquote - it's a bit ungainly though. Note however that the ``walk`` + method of ConfigObj is ideal for transforming values in this way. + It will recursively walk the values and apply a function to them all. + +.. _listquote: /python/listquote.html + +.. raw:: html + + <center> + <a href="http://sourceforge.net/donate/index.php?group_id=123265"> + <img src="http://images.sourceforge.net/images/project-support.jpg" width="88" height="32" border="0" alt="Support This Project" /> + </a> + <a href="http://sourceforge.net"> + <img src="http://sourceforge.net/sflogo.php?group_id=123265&type=1" width="88" height="31" border="0" alt="SourceForge.net Logo" /> + </a> + <br /> + <a href="http://www.python.org"> + <img src="images/powered_by_python.jpg" width="602" height="186" border="0" /> + </a> + <a href="http://www.opensource.org"> + <img src="images/osi-certified-120x100.gif" width="120" height="100" border="0" /> + <br /><strong>Certified Open Source</strong> + </a> + <br /><br /> + <script type="text/javascript" language="JavaScript">var site="s16atlantibots"</script> + <script type="text/javascript" language="JavaScript1.2" src="http://s16.sitemeter.com/js/counter.js?site=s16atlantibots"></script> + <noscript> + <a href="http://s16.sitemeter.com/stats.asp?site=s16atlantibots"> + <img src="http://s16.sitemeter.com/meter.asp?site=s16atlantibots" alt="Site Meter" border=0 /> + </a> + </noscript> + <br /> + </center> + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7170656 --- /dev/null +++ b/setup.py @@ -0,0 +1,48 @@ +# setup.py +# Install script for ConfigObj +# Copyright (C) 2005 Michael Foord, Mark Andrews, Nicola Larosa +# E-mail: fuzzyman AT voidspace DOT org DOT uk +# mark AT la-la DOT com +# nico AT tekNico DOT net + +# This software is licensed under the terms of the BSD license. +# http://www.voidspace.org.uk/python/license.shtml +# Basically you're free to copy, modify, distribute and relicense it, +# So long as you keep a copy of the license with it. + +# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml +# For information about bugfixes, updates and support, please join the +# Rest2Web mailing list: +# http://lists.sourceforge.net/lists/listinfo/rest2web-develop +# Comments, suggestions and bug reports welcome. +""" +**setup.py** for ``configobj`` and ``validate`` modules. +""" + +if __name__ == '__main__': + import sys + from distutils.core import setup + from configobj import __version__ as VERSION + + NAME = 'configobj' + MODULES = 'configobj', 'validate' + DESCRIPTION = 'Config file reading, writing, and validation.' + URL = 'http://www.voidspace.org.uk/python/configobj.html' + LICENSE = 'BSD' + PLATFORMS = ["Platform Independent"] + + if sys.version < '2.2.3': + from distutils.dist import DistributionMetadata + DistributionMetadata.classifiers = None + DistributionMetadata.download_url = None + + setup(name= NAME, + version= VERSION, + description= DESCRIPTION, + license = LICENSE, + platforms = PLATFORMS, + author= 'Michael Foord & Nicola Larosa', + author_email= 'fuzzyman@voidspace.org.uk', + url= URL, + py_modules = MODULES, + ) diff --git a/test_configobj.py b/test_configobj.py new file mode 100644 index 0000000..e5b1687 --- /dev/null +++ b/test_configobj.py @@ -0,0 +1,2085 @@ +# configobj_test.py +# doctests for ConfigObj +# A config file reader/writer that supports nested sections in config files. +# Copyright (C) 2005-2008 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 +from StringIO import StringIO + +import os +import sys +INTP_VER = sys.version_info[:2] +if INTP_VER < (2, 2): + raise RuntimeError("Python v.2.2 or later needed") + +try: + from codecs import BOM_UTF8 +except ImportError: + # Python 2.2 does not have this + # UTF-8 + BOM_UTF8 = '\xef\xbb\xbf' + +from configobj import * +from validate import Validator, VdtValueTooSmallError + + +""" + >>> z = ConfigObj() + >>> z['a'] = 'a' + >>> z['sect'] = { + ... 'subsect': { + ... 'a': 'fish', + ... 'b': 'wobble', + ... }, + ... 'member': 'value', + ... } + >>> x = ConfigObj(z.write()) + >>> z == x + 1 +""" + + +def _error_test(): + """ + Testing the error classes. + + >>> raise ConfigObjError + Traceback (most recent call last): + ConfigObjError + + >>> raise NestingError + Traceback (most recent call last): + NestingError + + >>> raise ParseError + Traceback (most recent call last): + ParseError + + >>> raise DuplicateError + Traceback (most recent call last): + DuplicateError + + >>> raise ConfigspecError + Traceback (most recent call last): + ConfigspecError + + >>> raise InterpolationLoopError('yoda') + Traceback (most recent call last): + InterpolationLoopError: interpolation loop detected in value "yoda". + + >>> raise RepeatSectionError + Traceback (most recent call last): + RepeatSectionError + + >>> raise MissingInterpolationOption('yoda') + Traceback (most recent call last): + MissingInterpolationOption: missing option "yoda" in interpolation. + + + >>> raise ReloadError() + Traceback (most recent call last): + ReloadError: reload failed, filename is not set. + >>> try: + ... raise ReloadError() + ... except IOError: + ... pass + ... else: + ... raise Exception('We should catch a ReloadError as an IOError') + >>> + + """ + + +def _section_test(): + """ + Tests from Section methods. + + >>> n = a.dict() + >>> n == a + 1 + >>> n is a + 0 + + >>> 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'}}}) + + >>> 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'}}) + """ + + +def _test_reset(): + """ + >>> something = object() + >>> c = ConfigObj() + >>> c['something'] = something + >>> c['section'] = {'something': something} + >>> c.filename = 'fish' + >>> c.raise_errors = something + >>> c.list_values = something + >>> c.create_empty = something + >>> c.file_error = something + >>> c.stringify = something + >>> c.indent_type = something + >>> c.encoding = something + >>> c.default_encoding = something + >>> c.BOM = something + >>> c.newlines = something + >>> c.write_empty_values = something + >>> c.unrepr = something + >>> c.initial_comment = something + >>> c.final_comment = something + >>> c.configspec = something + >>> c.inline_comments = something + >>> c.comments = something + >>> c.defaults = something + >>> c.default_values = something + >>> c.reset() + >>> + >>> c.filename + >>> c.raise_errors + False + >>> c.list_values + True + >>> c.create_empty + False + >>> c.file_error + False + >>> c.interpolation + True + >>> c.configspec + >>> c.stringify + True + >>> c.indent_type + >>> c.encoding + >>> c.default_encoding + >>> c.unrepr + False + >>> c.write_empty_values + False + >>> c.inline_comments + {} + >>> c.comments + {} + >>> c.defaults + [] + >>> c.default_values + {} + >>> c == ConfigObj() + True + >>> c + ConfigObj({}) + """ + + +def _test_reload(): + """ + >>> c = ConfigObj(StringIO()) + >>> c.reload() + Traceback (most recent call last): + ReloadError: reload failed, filename is not set. + >>> c = ConfigObj() + >>> c.reload() + Traceback (most recent call last): + ReloadError: reload failed, filename is not set. + >>> c = ConfigObj([]) + >>> c.reload() + Traceback (most recent call last): + ReloadError: reload failed, filename is not set. + + We need to use a real file as reload is only for files loaded from + the filesystem. + >>> h = open('temp', 'w') + >>> h.write(''' + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... [section] + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... [[sub section]] + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... [section2] + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... ''') + >>> h.close() + >>> configspec = ''' + ... test1= integer(30,50) + ... test2= string + ... test3=integer + ... test4=float(4.5) + ... [section] + ... test1=integer(30,50) + ... test2=string + ... test3=integer + ... test4=float(4.5) + ... [[sub section]] + ... test1=integer(30,50) + ... test2=string + ... test3=integer + ... test4=float(4.5) + ... [section2] + ... test1=integer(30,50) + ... test2=string + ... test3=integer + ... test4=float(4.5) + ... '''.split('\\n') + >>> c = ConfigObj('temp', configspec=configspec) + >>> c.configspec['test1'] = 'integer(50,60)' + >>> backup = ConfigObj('temp') + >>> del c['section'] + >>> del c['test1'] + >>> c['extra'] = '3' + >>> c['section2']['extra'] = '3' + >>> c.reload() + >>> c == backup + True + >>> c.validate(Validator()) + True + >>> os.remove('temp') + """ + + +def _doctest(): + """ + Dummy function to hold some of the doctests. + + >>> a.depth + 0 + >>> a == { + ... 'key2': 'val', + ... 'key1': 'val', + ... 'lev1c': { + ... 'lev2c': { + ... 'lev3c': { + ... 'key1': 'val', + ... }, + ... }, + ... }, + ... 'lev1b': { + ... 'key2': 'val', + ... 'key1': 'val', + ... 'lev2ba': { + ... 'key1': 'val', + ... }, + ... 'lev2bb': { + ... 'key1': 'val', + ... }, + ... }, + ... 'lev1a': { + ... 'key2': 'val', + ... 'key1': 'val', + ... }, + ... } + 1 + >>> b.depth + 0 + >>> b == { + ... 'key3': 'val3', + ... 'key2': 'val2', + ... 'key1': 'val1', + ... 'section 1': { + ... 'keys11': 'val1', + ... 'keys13': 'val3', + ... 'keys12': 'val2', + ... }, + ... 'section 2': { + ... 'section 2 sub 1': { + ... 'fish': '3', + ... }, + ... 'keys21': 'val1', + ... 'keys22': 'val2', + ... 'keys23': 'val3', + ... }, + ... } + 1 + >>> t = ''' + ... 'a' = b # !"$%^&*(),::;'@~#= 33 + ... "b" = b #= 6, 33 + ... ''' .split('\\n') + >>> t2 = ConfigObj(t) + >>> assert t2 == {'a': 'b', 'b': 'b'} + >>> t2.inline_comments['b'] = '' + >>> del t2['a'] + >>> assert t2.write() == ['','b = b', ''] + + # Test ``list_values=False`` stuff + >>> c = ''' + ... key1 = no quotes + ... key2 = 'single quotes' + ... key3 = "double quotes" + ... key4 = "list", 'with', several, "quotes" + ... ''' + >>> cfg = ConfigObj(c.splitlines(), list_values=False) + >>> cfg == {'key1': 'no quotes', 'key2': "'single quotes'", + ... 'key3': '"double quotes"', + ... 'key4': '"list", \\'with\\', several, "quotes"' + ... } + 1 + >>> cfg = ConfigObj(list_values=False) + >>> cfg['key1'] = 'Multiline\\nValue' + >>> cfg['key2'] = '''"Value" with 'quotes' !''' + >>> cfg.write() + ["key1 = '''Multiline\\nValue'''", 'key2 = "Value" with \\'quotes\\' !'] + >>> cfg.list_values = True + >>> cfg.write() == ["key1 = '''Multiline\\nValue'''", + ... 'key2 = \\'\\'\\'"Value" with \\'quotes\\' !\\'\\'\\''] + 1 + + Test flatten_errors: + + >>> config = ''' + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... [section] + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... [[sub section]] + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... '''.split('\\n') + >>> configspec = ''' + ... test1= integer(30,50) + ... test2= string + ... test3=integer + ... test4=float(6.0) + ... [section] + ... test1=integer(30,50) + ... test2=string + ... test3=integer + ... test4=float(6.0) + ... [[sub section]] + ... test1=integer(30,50) + ... test2=string + ... test3=integer + ... test4=float(6.0) + ... '''.split('\\n') + >>> val = Validator() + >>> c1 = ConfigObj(config, configspec=configspec) + >>> res = c1.validate(val) + >>> flatten_errors(c1, res) == [([], 'test4', False), (['section', + ... 'sub section'], 'test4', False), (['section'], 'test4', False)] + True + >>> res = c1.validate(val, preserve_errors=True) + >>> check = flatten_errors(c1, res) + >>> check[0][:2] + ([], 'test4') + >>> check[1][:2] + (['section', 'sub section'], 'test4') + >>> check[2][:2] + (['section'], 'test4') + >>> for entry in check: + ... isinstance(entry[2], VdtValueTooSmallError) + ... print str(entry[2]) + True + the value "5.0" is too small. + True + the value "5.0" is too small. + True + the value "5.0" is too small. + + Test unicode handling, BOM, write witha file like object and line endings : + >>> u_base = ''' + ... # initial comment + ... # inital comment 2 + ... + ... test1 = some value + ... # comment + ... test2 = another value # inline comment + ... # section comment + ... [section] # inline comment + ... test = test # another inline comment + ... test2 = test2 + ... + ... # final comment + ... # final comment2 + ... ''' + >>> u = u_base.encode('utf_8').splitlines(True) + >>> u[0] = BOM_UTF8 + u[0] + >>> uc = ConfigObj(u) + >>> uc.encoding = None + >>> uc.BOM == True + 1 + >>> uc == {'test1': 'some value', 'test2': 'another value', + ... 'section': {'test': 'test', 'test2': 'test2'}} + 1 + >>> uc = ConfigObj(u, encoding='utf_8', default_encoding='latin-1') + >>> uc.BOM + 1 + >>> isinstance(uc['test1'], unicode) + 1 + >>> uc.encoding + 'utf_8' + >>> uc.newlines + '\\n' + >>> uc['latin1'] = "This costs lot's of " + >>> a_list = uc.write() + >>> len(a_list) + 15 + >>> isinstance(a_list[0], str) + 1 + >>> a_list[0].startswith(BOM_UTF8) + 1 + >>> u = u_base.replace('\\n', '\\r\\n').encode('utf_8').splitlines(True) + >>> uc = ConfigObj(u) + >>> uc.newlines + '\\r\\n' + >>> uc.newlines = '\\r' + >>> file_like = StringIO() + >>> uc.write(file_like) + >>> file_like.seek(0) + >>> uc2 = ConfigObj(file_like) + >>> uc2 == uc + 1 + >>> uc2.filename == None + 1 + >>> uc2.newlines == '\\r' + 1 + + Test validate in copy mode + >>> a = ''' + ... # Initial Comment + ... + ... key1 = string(default=Hello) + ... + ... # section comment + ... [section] # inline comment + ... # key1 comment + ... key1 = integer(default=6) + ... # key2 comment + ... key2 = boolean(default=True) + ... + ... # subsection comment + ... [[sub-section]] # inline comment + ... # another key1 comment + ... key1 = float(default=3.0)'''.splitlines() + >>> b = ConfigObj(configspec=a) + >>> b.validate(val, copy=True) + 1 + >>> b.write() == ['', + ... '# Initial Comment', + ... '', + ... 'key1 = Hello', + ... '', + ... '# section comment', + ... '[section] # inline comment', + ... ' # key1 comment', + ... ' key1 = 6', + ... ' # key2 comment', + ... ' key2 = True', + ... ' ', + ... ' # subsection comment', + ... ' [[sub-section]] # inline comment', + ... ' # another key1 comment', + ... ' key1 = 3.0'] + 1 + + Test Writing Empty Values + >>> a = ''' + ... key1 = + ... key2 =# a comment''' + >>> b = ConfigObj(a.splitlines()) + >>> b.write() + ['', 'key1 = ""', 'key2 = "" # a comment'] + >>> b.write_empty_values = True + >>> b.write() + ['', 'key1 = ', 'key2 = # a comment'] + + Test unrepr when reading + >>> a = ''' + ... key1 = (1, 2, 3) # comment + ... key2 = True + ... key3 = 'a string' + ... key4 = [1, 2, 3, 'a mixed list'] + ... '''.splitlines() + >>> b = ConfigObj(a, unrepr=True) + >>> b == {'key1': (1, 2, 3), + ... 'key2': True, + ... 'key3': 'a string', + ... 'key4': [1, 2, 3, 'a mixed list']} + 1 + + Test unrepr when writing + >>> c = ConfigObj(b.write(), unrepr=True) + >>> c == b + 1 + + Test unrepr with multiline values + >>> a = '''k = \"""{ + ... 'k1': 3, + ... 'k2': 6.0}\""" + ... '''.splitlines() + >>> c = ConfigObj(a, unrepr=True) + >>> c == {'k': {'k1': 3, 'k2': 6.0}} + 1 + + Test unrepr with a dictionary + >>> a = 'k = {"a": 1}'.splitlines() + >>> c = ConfigObj(a, unrepr=True) + >>> type(c['k']) == dict + 1 + + >>> 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 + + >>> a = ConfigObj() + >>> a['a'] = 'fish' + >>> try: + ... a.as_int('a') #doctest: +ELLIPSIS + ... except ValueError, e: + ... err_mess = str(e) + >>> err_mess.startswith('invalid literal for int()') + 1 + >>> a['b'] = '1' + >>> a.as_int('b') + 1 + >>> a['b'] = '3.2' + >>> try: + ... a.as_int('b') #doctest: +ELLIPSIS + ... except ValueError, e: + ... err_mess = str(e) + >>> err_mess.startswith('invalid literal for int()') + 1 + + >>> 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 + + Test # with unrepr + >>> a = ''' + ... key1 = (1, 2, 3) # comment + ... key2 = True + ... key3 = 'a string' + ... key4 = [1, 2, 3, 'a mixed list#'] + ... '''.splitlines() + >>> b = ConfigObj(a, unrepr=True) + >>> b == {'key1': (1, 2, 3), + ... 'key2': True, + ... 'key3': 'a string', + ... 'key4': [1, 2, 3, 'a mixed list#']} + 1 + """ + + # Comments are no longer parsed from values in configspecs + # so the following test fails and is disabled + untested = """ + Test validate in copy mode + >>> a = ''' + ... # Initial Comment + ... + ... key1 = string(default=Hello) # comment 1 + ... + ... # section comment + ... [section] # inline comment + ... # key1 comment + ... key1 = integer(default=6) # an integer value + ... # key2 comment + ... key2 = boolean(default=True) # a boolean + ... + ... # subsection comment + ... [[sub-section]] # inline comment + ... # another key1 comment + ... key1 = float(default=3.0) # a float'''.splitlines() + >>> b = ConfigObj(configspec=a) + >>> b.validate(val, copy=True) + 1 + >>> b.write() + >>> b.write() == ['', + ... '# Initial Comment', + ... '', + ... 'key1 = Hello # comment 1', + ... '', + ... '# section comment', + ... '[section] # inline comment', + ... ' # key1 comment', + ... ' key1 = 6 # an integer value', + ... ' # key2 comment', + ... ' key2 = True # a boolean', + ... ' ', + ... ' # subsection comment', + ... ' [[sub-section]] # inline comment', + ... ' # another key1 comment', + ... ' key1 = 3.0 # a float'] + 1 + """ + + +def _test_configobj(): + """ + Testing ConfigObj + Testing with duplicate keys and sections. + + >>> c = ''' + ... [hello] + ... member = value + ... [hello again] + ... member = value + ... [ "hello" ] + ... member = value + ... ''' + >>> ConfigObj(c.split('\\n'), raise_errors = True) + Traceback (most recent call last): + DuplicateError: Duplicate section name at line 6. + + >>> d = ''' + ... [hello] + ... member = value + ... [hello again] + ... member1 = value + ... member2 = value + ... 'member1' = value + ... [ "and again" ] + ... member = value + ... ''' + >>> ConfigObj(d.split('\\n'), raise_errors = True) + Traceback (most recent call last): + DuplicateError: Duplicate keyword name at line 7. + + Testing ConfigParser-style interpolation + + >>> c = ConfigObj() + >>> c['DEFAULT'] = { + ... 'b': 'goodbye', + ... 'userdir': 'c:\\\\home', + ... 'c': '%(d)s', + ... 'd': '%(c)s' + ... } + >>> c['section'] = { + ... 'a': '%(datadir)s\\\\some path\\\\file.py', + ... 'b': '%(userdir)s\\\\some path\\\\file.py', + ... 'c': 'Yo %(a)s', + ... 'd': '%(not_here)s', + ... 'e': '%(e)s', + ... } + >>> c['section']['DEFAULT'] = { + ... 'datadir': 'c:\\\\silly_test', + ... 'a': 'hello - %(b)s', + ... } + >>> c['section']['a'] == 'c:\\\\silly_test\\\\some path\\\\file.py' + 1 + >>> c['section']['b'] == 'c:\\\\home\\\\some path\\\\file.py' + 1 + >>> c['section']['c'] == 'Yo c:\\\\silly_test\\\\some path\\\\file.py' + 1 + + Switching Interpolation Off + + >>> c.interpolation = False + >>> c['section']['a'] == '%(datadir)s\\\\some path\\\\file.py' + 1 + >>> c['section']['b'] == '%(userdir)s\\\\some path\\\\file.py' + 1 + >>> c['section']['c'] == 'Yo %(a)s' + 1 + + Testing the interpolation errors. + + >>> c.interpolation = True + >>> c['section']['d'] + Traceback (most recent call last): + MissingInterpolationOption: missing option "not_here" in interpolation. + >>> c['section']['e'] + Traceback (most recent call last): + InterpolationLoopError: interpolation loop detected in value "e". + + Testing Template-style interpolation + + >>> interp_cfg = ''' + ... [DEFAULT] + ... keyword1 = value1 + ... 'keyword 2' = 'value 2' + ... reference = ${keyword1} + ... foo = 123 + ... + ... [ section ] + ... templatebare = $keyword1/foo + ... bar = $$foo + ... dollar = $$300.00 + ... stophere = $$notinterpolated + ... with_braces = ${keyword1}s (plural) + ... with_spaces = ${keyword 2}!!! + ... with_several = $keyword1/$reference/$keyword1 + ... configparsersample = %(keyword 2)sconfig + ... deep = ${reference} + ... + ... [[DEFAULT]] + ... baz = $foo + ... + ... [[ sub-section ]] + ... quux = '$baz + $bar + $foo' + ... + ... [[[ sub-sub-section ]]] + ... convoluted = "$bar + $baz + $quux + $bar" + ... ''' + >>> c = ConfigObj(interp_cfg.split('\\n'), interpolation='Template') + >>> c['section']['templatebare'] + 'value1/foo' + >>> c['section']['dollar'] + '$300.00' + >>> c['section']['stophere'] + '$notinterpolated' + >>> c['section']['with_braces'] + 'value1s (plural)' + >>> c['section']['with_spaces'] + 'value 2!!!' + >>> c['section']['with_several'] + 'value1/value1/value1' + >>> c['section']['configparsersample'] + '%(keyword 2)sconfig' + >>> c['section']['deep'] + 'value1' + >>> c['section']['sub-section']['quux'] + '123 + $foo + 123' + >>> c['section']['sub-section']['sub-sub-section']['convoluted'] + '$foo + 123 + 123 + $foo + 123 + $foo' + + Testing our quoting. + + >>> i._quote('\"""\\'\\'\\'') + Traceback (most recent call last): + ConfigObjError: Value \"\"""'''" cannot be safely quoted. + >>> try: + ... i._quote('\\n', multiline=False) + ... except ConfigObjError, e: + ... e.msg + 'Value "\\n" cannot be safely quoted.' + >>> i._quote(' "\\' ', multiline=False) + Traceback (most recent call last): + ConfigObjError: Value " "' " cannot be safely quoted. + + Testing with "stringify" off. + >>> c.stringify = False + >>> c['test'] = 1 + Traceback (most recent call last): + TypeError: Value is not a string "1". + + Testing Empty values. + >>> cfg_with_empty = ''' + ... k = + ... k2 =# comment test + ... val = test + ... val2 = , + ... val3 = 1, + ... val4 = 1, 2 + ... val5 = 1, 2, \'''.splitlines() + >>> cwe = ConfigObj(cfg_with_empty) + >>> cwe == {'k': '', 'k2': '', 'val': 'test', 'val2': [], + ... 'val3': ['1'], 'val4': ['1', '2'], 'val5': ['1', '2']} + 1 + >>> cwe = ConfigObj(cfg_with_empty, list_values=False) + >>> cwe == {'k': '', 'k2': '', 'val': 'test', 'val2': ',', + ... 'val3': '1,', 'val4': '1, 2', 'val5': '1, 2,'} + 1 + + Testing list values. + >>> testconfig3 = \''' + ... a = , + ... b = test, + ... c = test1, test2 , test3 + ... d = test1, test2, test3, + ... \''' + >>> d = ConfigObj(testconfig3.split('\\n'), raise_errors=True) + >>> d['a'] == [] + 1 + >>> d['b'] == ['test'] + 1 + >>> d['c'] == ['test1', 'test2', 'test3'] + 1 + >>> d['d'] == ['test1', 'test2', 'test3'] + 1 + + Testing with list values off. + + >>> e = ConfigObj( + ... testconfig3.split('\\n'), + ... raise_errors=True, + ... list_values=False) + >>> e['a'] == ',' + 1 + >>> e['b'] == 'test,' + 1 + >>> e['c'] == 'test1, test2 , test3' + 1 + >>> e['d'] == 'test1, test2, test3,' + 1 + + Testing creating from a dictionary. + + >>> f = { + ... 'key1': 'val1', + ... 'key2': 'val2', + ... 'section 1': { + ... 'key1': 'val1', + ... 'key2': 'val2', + ... 'section 1b': { + ... 'key1': 'val1', + ... 'key2': 'val2', + ... }, + ... }, + ... 'section 2': { + ... 'key1': 'val1', + ... 'key2': 'val2', + ... 'section 2b': { + ... 'key1': 'val1', + ... 'key2': 'val2', + ... }, + ... }, + ... 'key3': 'val3', + ... } + >>> g = ConfigObj(f) + >>> f == g + 1 + + Testing we correctly detect badly built list values (4 of them). + + >>> testconfig4 = ''' + ... config = 3,4,, + ... test = 3,,4 + ... fish = ,, + ... dummy = ,,hello, goodbye + ... ''' + >>> try: + ... ConfigObj(testconfig4.split('\\n')) + ... except ConfigObjError, e: + ... len(e.errors) + 4 + + Testing we correctly detect badly quoted values (4 of them). + + >>> testconfig5 = ''' + ... config = "hello # comment + ... test = 'goodbye + ... fish = 'goodbye # comment + ... dummy = "hello again + ... ''' + >>> try: + ... ConfigObj(testconfig5.split('\\n')) + ... except ConfigObjError, e: + ... len(e.errors) + 4 + + Test Multiline Comments + >>> i == { + ... 'name4': ' another single line value ', + ... 'multi section': { + ... 'name4': '\\n Well, this is a\\n multiline ' + ... 'value\\n ', + ... 'name2': '\\n Well, this is a\\n multiline ' + ... 'value\\n ', + ... 'name3': '\\n Well, this is a\\n multiline ' + ... 'value\\n ', + ... 'name1': '\\n Well, this is a\\n multiline ' + ... 'value\\n ', + ... }, + ... 'name2': ' another single line value ', + ... 'name3': ' a single line value ', + ... 'name1': ' a single line value ', + ... } + 1 + + >>> filename = a.filename + >>> a.filename = None + >>> values = a.write() + >>> index = 0 + >>> while index < 23: + ... index += 1 + ... line = values[index-1] + ... assert line.endswith('# comment ' + str(index)) + >>> a.filename = filename + + >>> start_comment = ['# Initial Comment', '', '#'] + >>> end_comment = ['', '#', '# Final Comment'] + >>> newconfig = start_comment + testconfig1.split('\\n') + end_comment + >>> nc = ConfigObj(newconfig) + >>> nc.initial_comment + ['# Initial Comment', '', '#'] + >>> nc.final_comment + ['', '#', '# Final Comment'] + >>> nc.initial_comment == start_comment + 1 + >>> nc.final_comment == end_comment + 1 + + Test the _handle_comment method + + >>> c = ConfigObj() + >>> c['foo'] = 'bar' + >>> c.inline_comments['foo'] = 'Nice bar' + >>> c.write() + ['foo = bar # Nice bar'] + + 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 + >>> os.remove('test.ini') + >>> b.filename = 'test.ini' + >>> b.write() + >>> b == ConfigObj('test.ini', raise_errors=True) + 1 + >>> os.remove('test.ini') + >>> i.filename = 'test.ini' + >>> i.write() + >>> i == ConfigObj('test.ini', raise_errors=True) + 1 + >>> os.remove('test.ini') + >>> a = ConfigObj() + >>> a['DEFAULT'] = {'a' : 'fish'} + >>> a['a'] = '%(a)s' + >>> a.write() + ['a = %(a)s', '[DEFAULT]', 'a = fish'] + + Test indentation handling + + >>> ConfigObj({'sect': {'sect': {'foo': 'bar'}}}).write() + ['[sect]', ' [[sect]]', ' foo = bar'] + >>> cfg = ['[sect]', '[[sect]]', 'foo = bar'] + >>> ConfigObj(cfg).write() == cfg + 1 + >>> cfg = ['[sect]', ' [[sect]]', ' foo = bar'] + >>> ConfigObj(cfg).write() == cfg + 1 + >>> cfg = ['[sect]', ' [[sect]]', ' foo = bar'] + >>> ConfigObj(cfg).write() == cfg + 1 + >>> ConfigObj(oneTabCfg).write() == oneTabCfg + 1 + >>> ConfigObj(twoTabsCfg).write() == twoTabsCfg + 1 + >>> ConfigObj(tabsAndSpacesCfg).write() == tabsAndSpacesCfg + 1 + >>> ConfigObj(cfg, indent_type=chr(9)).write() == oneTabCfg + 1 + >>> ConfigObj(oneTabCfg, indent_type=' ').write() == cfg + 1 + """ + + +def _test_validate(): + """ + >>> config = ''' + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... [section] + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... [[sub section]] + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... '''.split('\\n') + >>> configspec = ''' + ... test1= integer(30,50) + ... test2= string + ... test3=integer + ... test4=float(6.0) + ... [section ] + ... test1=integer(30,50) + ... test2=string + ... test3=integer + ... test4=float(6.0) + ... [[sub section]] + ... test1=integer(30,50) + ... test2=string + ... test3=integer + ... test4=float(6.0) + ... '''.split('\\n') + >>> val = Validator() + >>> c1 = ConfigObj(config, configspec=configspec) + >>> test = c1.validate(val) + >>> test == { + ... 'test1': True, + ... 'test2': True, + ... 'test3': True, + ... 'test4': False, + ... 'section': { + ... 'test1': True, + ... 'test2': True, + ... 'test3': True, + ... 'test4': False, + ... 'sub section': { + ... 'test1': True, + ... 'test2': True, + ... 'test3': True, + ... 'test4': False, + ... }, + ... }, + ... } + 1 + >>> val.check(c1.configspec['test4'], c1['test4']) + Traceback (most recent call last): + VdtValueTooSmallError: the value "5.0" is too small. + + >>> val_test_config = ''' + ... key = 0 + ... key2 = 1.1 + ... [section] + ... key = some text + ... key2 = 1.1, 3.0, 17, 6.8 + ... [[sub-section]] + ... key = option1 + ... key2 = True'''.split('\\n') + >>> val_test_configspec = ''' + ... key = integer + ... key2 = float + ... [section] + ... key = string + ... key2 = float_list(4) + ... [[sub-section]] + ... key = option(option1, option2) + ... key2 = boolean'''.split('\\n') + >>> val_test = ConfigObj(val_test_config, configspec=val_test_configspec) + >>> val_test.validate(val) + 1 + >>> val_test['key'] = 'text not a digit' + >>> val_res = val_test.validate(val) + >>> val_res == {'key2': True, 'section': True, 'key': False} + 1 + >>> configspec = ''' + ... test1=integer(30,50, default=40) + ... test2=string(default="hello") + ... test3=integer(default=3) + ... test4=float(6.0, default=6.0) + ... [section ] + ... test1=integer(30,50, default=40) + ... test2=string(default="hello") + ... test3=integer(default=3) + ... test4=float(6.0, default=6.0) + ... [[sub section]] + ... test1=integer(30,50, default=40) + ... test2=string(default="hello") + ... test3=integer(default=3) + ... test4=float(6.0, default=6.0) + ... '''.split('\\n') + >>> default_test = ConfigObj(['test1=30'], configspec=configspec) + >>> default_test + ConfigObj({'test1': '30'}) + >>> default_test.defaults + [] + >>> default_test.default_values + {} + >>> default_test.validate(val) + 1 + >>> default_test == { + ... 'test1': 30, + ... 'test2': 'hello', + ... 'test3': 3, + ... 'test4': 6.0, + ... 'section': { + ... 'test1': 40, + ... 'test2': 'hello', + ... 'test3': 3, + ... 'test4': 6.0, + ... 'sub section': { + ... 'test1': 40, + ... 'test3': 3, + ... 'test2': 'hello', + ... 'test4': 6.0, + ... }, + ... }, + ... } + 1 + >>> default_test.defaults + ['test2', 'test3', 'test4'] + >>> default_test.default_values == {'test1': 40, 'test2': 'hello', + ... 'test3': 3, 'test4': 6.0} + 1 + >>> default_test.restore_default('test1') + 40 + >>> default_test['test1'] + 40 + >>> 'test1' in default_test.defaults + 1 + >>> def change(section, key): + ... section[key] = 3 + >>> _ = default_test.walk(change) + >>> default_test['section']['sub section']['test4'] + 3 + >>> default_test.restore_defaults() + >>> default_test == { + ... 'test1': 40, + ... 'test2': "hello", + ... 'test3': 3, + ... 'test4': 6.0, + ... 'section': { + ... 'test1': 40, + ... 'test2': "hello", + ... 'test3': 3, + ... 'test4': 6.0, + ... 'sub section': { + ... 'test1': 40, + ... 'test2': "hello", + ... 'test3': 3, + ... 'test4': 6.0 + ... }}} + 1 + + Now testing with repeated sections : BIG TEST + + >>> repeated_1 = ''' + ... [dogs] + ... [[__many__]] # spec for a dog + ... fleas = boolean(default=True) + ... tail = option(long, short, default=long) + ... name = string(default=rover) + ... [[[__many__]]] # spec for a puppy + ... name = string(default="son of rover") + ... age = float(default=0.0) + ... [cats] + ... [[__many__]] # spec for a cat + ... fleas = boolean(default=True) + ... tail = option(long, short, default=short) + ... name = string(default=pussy) + ... [[[__many__]]] # spec for a kitten + ... name = string(default="son of pussy") + ... age = float(default=0.0) + ... '''.split('\\n') + >>> repeated_2 = ''' + ... [dogs] + ... + ... # blank dogs with puppies + ... # should be filled in by the configspec + ... [[dog1]] + ... [[[puppy1]]] + ... [[[puppy2]]] + ... [[[puppy3]]] + ... [[dog2]] + ... [[[puppy1]]] + ... [[[puppy2]]] + ... [[[puppy3]]] + ... [[dog3]] + ... [[[puppy1]]] + ... [[[puppy2]]] + ... [[[puppy3]]] + ... [cats] + ... + ... # blank cats with kittens + ... # should be filled in by the configspec + ... [[cat1]] + ... [[[kitten1]]] + ... [[[kitten2]]] + ... [[[kitten3]]] + ... [[cat2]] + ... [[[kitten1]]] + ... [[[kitten2]]] + ... [[[kitten3]]] + ... [[cat3]] + ... [[[kitten1]]] + ... [[[kitten2]]] + ... [[[kitten3]]] + ... '''.split('\\n') + >>> repeated_3 = ''' + ... [dogs] + ... + ... [[dog1]] + ... [[dog2]] + ... [[dog3]] + ... [cats] + ... + ... [[cat1]] + ... [[cat2]] + ... [[cat3]] + ... '''.split('\\n') + >>> repeated_4 = ''' + ... [__many__] + ... + ... name = string(default=Michael) + ... age = float(default=0.0) + ... sex = option(m, f, default=m) + ... '''.split('\\n') + >>> repeated_5 = ''' + ... [cats] + ... [[__many__]] + ... fleas = boolean(default=True) + ... tail = option(long, short, default=short) + ... name = string(default=pussy) + ... [[[description]]] + ... height = float(default=3.3) + ... weight = float(default=6) + ... [[[[coat]]]] + ... fur = option(black, grey, brown, "tortoise shell", default=black) + ... condition = integer(0,10, default=5) + ... '''.split('\\n') + >>> val= Validator() + >>> repeater = ConfigObj(repeated_2, configspec=repeated_1) + >>> repeater.validate(val) + 1 + >>> repeater == { + ... 'dogs': { + ... 'dog1': { + ... 'fleas': True, + ... 'tail': 'long', + ... 'name': 'rover', + ... 'puppy1': {'name': 'son of rover', 'age': 0.0}, + ... 'puppy2': {'name': 'son of rover', 'age': 0.0}, + ... 'puppy3': {'name': 'son of rover', 'age': 0.0}, + ... }, + ... 'dog2': { + ... 'fleas': True, + ... 'tail': 'long', + ... 'name': 'rover', + ... 'puppy1': {'name': 'son of rover', 'age': 0.0}, + ... 'puppy2': {'name': 'son of rover', 'age': 0.0}, + ... 'puppy3': {'name': 'son of rover', 'age': 0.0}, + ... }, + ... 'dog3': { + ... 'fleas': True, + ... 'tail': 'long', + ... 'name': 'rover', + ... 'puppy1': {'name': 'son of rover', 'age': 0.0}, + ... 'puppy2': {'name': 'son of rover', 'age': 0.0}, + ... 'puppy3': {'name': 'son of rover', 'age': 0.0}, + ... }, + ... }, + ... 'cats': { + ... 'cat1': { + ... 'fleas': True, + ... 'tail': 'short', + ... 'name': 'pussy', + ... 'kitten1': {'name': 'son of pussy', 'age': 0.0}, + ... 'kitten2': {'name': 'son of pussy', 'age': 0.0}, + ... 'kitten3': {'name': 'son of pussy', 'age': 0.0}, + ... }, + ... 'cat2': { + ... 'fleas': True, + ... 'tail': 'short', + ... 'name': 'pussy', + ... 'kitten1': {'name': 'son of pussy', 'age': 0.0}, + ... 'kitten2': {'name': 'son of pussy', 'age': 0.0}, + ... 'kitten3': {'name': 'son of pussy', 'age': 0.0}, + ... }, + ... 'cat3': { + ... 'fleas': True, + ... 'tail': 'short', + ... 'name': 'pussy', + ... 'kitten1': {'name': 'son of pussy', 'age': 0.0}, + ... 'kitten2': {'name': 'son of pussy', 'age': 0.0}, + ... 'kitten3': {'name': 'son of pussy', 'age': 0.0}, + ... }, + ... }, + ... } + 1 + >>> repeater = ConfigObj(repeated_3, configspec=repeated_1) + >>> repeater.validate(val) + 1 + >>> repeater == { + ... 'cats': { + ... 'cat1': {'fleas': True, 'tail': 'short', 'name': 'pussy'}, + ... 'cat2': {'fleas': True, 'tail': 'short', 'name': 'pussy'}, + ... 'cat3': {'fleas': True, 'tail': 'short', 'name': 'pussy'}, + ... }, + ... 'dogs': { + ... 'dog1': {'fleas': True, 'tail': 'long', 'name': 'rover'}, + ... 'dog2': {'fleas': True, 'tail': 'long', 'name': 'rover'}, + ... 'dog3': {'fleas': True, 'tail': 'long', 'name': 'rover'}, + ... }, + ... } + 1 + >>> repeater = ConfigObj(configspec=repeated_4) + >>> repeater['Michael'] = {} + >>> repeater.validate(val) + 1 + >>> repeater == { + ... 'Michael': {'age': 0.0, 'name': 'Michael', 'sex': 'm'}, + ... } + 1 + >>> repeater = ConfigObj(repeated_3, configspec=repeated_5) + >>> repeater == { + ... 'dogs': {'dog1': {}, 'dog2': {}, 'dog3': {}}, + ... 'cats': {'cat1': {}, 'cat2': {}, 'cat3': {}}, + ... } + 1 + >>> repeater.validate(val) + 1 + >>> repeater == { + ... 'dogs': {'dog1': {}, 'dog2': {}, 'dog3': {}}, + ... 'cats': { + ... 'cat1': { + ... 'fleas': True, + ... 'tail': 'short', + ... 'name': 'pussy', + ... 'description': { + ... 'weight': 6.0, + ... 'height': 3.2999999999999998, + ... 'coat': {'fur': 'black', 'condition': 5}, + ... }, + ... }, + ... 'cat2': { + ... 'fleas': True, + ... 'tail': 'short', + ... 'name': 'pussy', + ... 'description': { + ... 'weight': 6.0, + ... 'height': 3.2999999999999998, + ... 'coat': {'fur': 'black', 'condition': 5}, + ... }, + ... }, + ... 'cat3': { + ... 'fleas': True, + ... 'tail': 'short', + ... 'name': 'pussy', + ... 'description': { + ... 'weight': 6.0, + ... 'height': 3.2999999999999998, + ... 'coat': {'fur': 'black', 'condition': 5}, + ... }, + ... }, + ... }, + ... } + 1 + + Test that interpolation is preserved for validated string values. + Also check that interpolation works in configspecs. + >>> t = ConfigObj(configspec=['test = string']) + >>> t['DEFAULT'] = {} + >>> t['DEFAULT']['def_test'] = 'a' + >>> t['test'] = '%(def_test)s' + >>> t['test'] + 'a' + >>> v = Validator() + >>> t.validate(v) + 1 + >>> t.interpolation = False + >>> t + ConfigObj({'test': '%(def_test)s', 'DEFAULT': {'def_test': 'a'}}) + >>> specs = [ + ... 'interpolated string = string(default="fuzzy-%(man)s")', + ... '[DEFAULT]', + ... 'man = wuzzy', + ... ] + >>> c = ConfigObj(configspec=specs) + >>> c.validate(v) + 1 + >>> c['interpolated string'] + 'fuzzy-wuzzy' + + Test SimpleVal + >>> val = SimpleVal() + >>> config = ''' + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... [section] + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... [[sub section]] + ... test1=40 + ... test2=hello + ... test3=3 + ... test4=5.0 + ... '''.split('\\n') + >>> configspec = ''' + ... test1='' + ... test2='' + ... test3='' + ... test4='' + ... [section] + ... test1='' + ... test2='' + ... test3='' + ... test4='' + ... [[sub section]] + ... test1='' + ... test2='' + ... test3='' + ... test4='' + ... '''.split('\\n') + >>> o = ConfigObj(config, configspec=configspec) + >>> o.validate(val) + 1 + >>> o = ConfigObj(configspec=configspec) + >>> o.validate(val) + 0 + + Test Flatten Errors + >>> vtor = Validator() + >>> my_ini = ''' + ... option1 = True + ... [section1] + ... option1 = True + ... [section2] + ... another_option = Probably + ... [section3] + ... another_option = True + ... [[section3b]] + ... value = 3 + ... value2 = a + ... value3 = 11 + ... ''' + >>> my_cfg = ''' + ... option1 = boolean() + ... option2 = boolean() + ... option3 = boolean(default=Bad_value) + ... [section1] + ... option1 = boolean() + ... option2 = boolean() + ... option3 = boolean(default=Bad_value) + ... [section2] + ... another_option = boolean() + ... [section3] + ... another_option = boolean() + ... [[section3b]] + ... value = integer + ... value2 = integer + ... value3 = integer(0, 10) + ... [[[section3b-sub]]] + ... value = string + ... [section4] + ... another_option = boolean() + ... ''' + >>> cs = my_cfg.split('\\n') + >>> ini = my_ini.split('\\n') + >>> cfg = ConfigObj(ini, configspec=cs) + >>> res = cfg.validate(vtor, preserve_errors=True) + >>> errors = [] + >>> for entry in flatten_errors(cfg, res): + ... section_list, key, error = entry + ... section_list.insert(0, '[root]') + ... if key is not None: + ... section_list.append(key) + ... else: + ... section_list.append('[missing]') + ... section_string = ', '.join(section_list) + ... errors.append((section_string, ' = ', error)) + >>> errors.sort() + >>> for entry in errors: + ... print entry[0], entry[1], (entry[2] or 0) + [root], option2 = 0 + [root], option3 = the value "Bad_value" is of the wrong type. + [root], section1, option2 = 0 + [root], section1, option3 = the value "Bad_value" is of the wrong type. + [root], section2, another_option = the value "Probably" is of the wrong type. + [root], section3, section3b, section3b-sub, [missing] = 0 + [root], section3, section3b, value2 = the value "a" is of the wrong type. + [root], section3, section3b, value3 = the value "11" is too big. + [root], section4, [missing] = 0 + """ + + +def _test_errors(): + """ + Test the error messages and objects, in normal mode and unrepr mode. + >>> bad_syntax = ''' + ... key = "value" + ... key2 = "value + ... '''.splitlines() + >>> c = ConfigObj(bad_syntax) + Traceback (most recent call last): + ParseError: Parse error in value at line 3. + >>> c = ConfigObj(bad_syntax, raise_errors=True) + Traceback (most recent call last): + ParseError: Parse error in value at line 3. + >>> c = ConfigObj(bad_syntax, raise_errors=True, unrepr=True) + Traceback (most recent call last): + UnreprError: Parse error in value at line 3. + >>> try: + ... c = ConfigObj(bad_syntax) + ... except Exception, e: + ... pass + >>> assert(isinstance(e, ConfigObjError)) + >>> print e + Parse error in value at line 3. + >>> len(e.errors) == 1 + 1 + >>> try: + ... c = ConfigObj(bad_syntax, unrepr=True) + ... except Exception, e: + ... pass + >>> assert(isinstance(e, ConfigObjError)) + >>> print e + Parse error in value at line 3. + >>> len(e.errors) == 1 + 1 + >>> the_error = e.errors[0] + >>> assert(isinstance(the_error, UnreprError)) + + >>> multiple_bad_syntax = ''' + ... key = "value" + ... key2 = "value + ... key3 = "value2 + ... '''.splitlines() + >>> try: + ... c = ConfigObj(multiple_bad_syntax) + ... except ConfigObjError, e: + ... str(e) + 'Parsing failed with several errors.\\nFirst error at line 3.' + >>> c = ConfigObj(multiple_bad_syntax, raise_errors=True) + Traceback (most recent call last): + ParseError: Parse error in value at line 3. + >>> c = ConfigObj(multiple_bad_syntax, raise_errors=True, unrepr=True) + Traceback (most recent call last): + UnreprError: Parse error in value at line 3. + >>> try: + ... c = ConfigObj(multiple_bad_syntax) + ... except Exception, e: + ... pass + >>> assert(isinstance(e, ConfigObjError)) + >>> print e + Parsing failed with several errors. + First error at line 3. + >>> len(e.errors) == 2 + 1 + >>> try: + ... c = ConfigObj(multiple_bad_syntax, unrepr=True) + ... except Exception, e: + ... pass + >>> assert(isinstance(e, ConfigObjError)) + >>> print e + Parsing failed with several errors. + First error at line 3. + >>> len(e.errors) == 2 + 1 + >>> the_error = e.errors[1] + >>> assert(isinstance(the_error, UnreprError)) + + >>> unknown_name = ''' + ... key = "value" + ... key2 = value + ... '''.splitlines() + >>> c = ConfigObj(unknown_name) + >>> c = ConfigObj(unknown_name, unrepr=True) + Traceback (most recent call last): + UnreprError: Unknown name or type in value at line 3. + >>> c = ConfigObj(unknown_name, raise_errors=True, unrepr=True) + Traceback (most recent call last): + UnreprError: Unknown name or type in value at line 3. + """ + + +def _test_unrepr_comments(): + """ + >>> config = ''' + ... # initial comments + ... # with two lines + ... key = "value" + ... # section comment + ... [section] # inline section comment + ... # key comment + ... key = "value" + ... # final comment + ... # with two lines + ... '''.splitlines() + >>> c = ConfigObj(config, unrepr=True) + >>> c == { 'key': 'value', + ... 'section': { 'key': 'value'}} + 1 + >>> c.initial_comment == ['', '# initial comments', '# with two lines'] + 1 + >>> c.comments == {'section': ['# section comment'], 'key': []} + 1 + >>> c.inline_comments == {'section': '# inline section comment', 'key': ''} + 1 + >>> c['section'].comments == { 'key': ['# key comment']} + 1 + >>> c.final_comment == ['# final comment', '# with two lines'] + 1 + """ + + +def _test_newline_terminated(): + """ + >>> c = ConfigObj() + >>> c.newlines = '\\n' + >>> c['a'] = 'b' + >>> collector = StringIO() + >>> c.write(collector) + >>> collector.getvalue() + 'a = b\\n' + """ + + +def _test_hash_escaping(): + """ + >>> c = ConfigObj() + >>> c.newlines = '\\n' + >>> c['#a'] = 'b # something' + >>> collector = StringIO() + >>> c.write(collector) + >>> collector.getvalue() + '"#a" = "b # something"\\n' + + >>> c = ConfigObj() + >>> c.newlines = '\\n' + >>> c['a'] = 'b # something', 'c # something' + >>> collector = StringIO() + >>> c.write(collector) + >>> collector.getvalue() + 'a = "b # something", "c # something"\\n' + """ + + +def _test_lineendings(): + """ + NOTE: Need to use a real file because this code is only + exercised when reading from the filesystem. + + >>> h = open('temp', 'wb') + >>> h.write('\\r\\n') + >>> h.close() + >>> c = ConfigObj('temp') + >>> c.newlines + '\\r\\n' + >>> h = open('temp', 'wb') + >>> h.write('\\n') + >>> h.close() + >>> c = ConfigObj('temp') + >>> c.newlines + '\\n' + >>> os.remove('temp') + """ + + +def _test_validate_with_copy_and_many(): + """ + >>> spec = ''' + ... [section] + ... [[__many__]] + ... value = string(default='nothing') + ... ''' + >>> config = ''' + ... [section] + ... [[something]] + ... ''' + >>> c = ConfigObj(StringIO(config), configspec=StringIO(spec)) + >>> v = Validator() + >>> r = c.validate(v, copy=True) + >>> c['section']['something']['value'] + 'nothing' + """ + +def _test_configspec_with_hash(): + """ + >>> spec = ['stuff = string(default="#ff00dd")'] + >>> c = ConfigObj(spec, _inspec=True) + >>> c['stuff'] + 'string(default="#ff00dd")' + >>> c = ConfigObj(configspec=spec) + >>> v = Validator() + >>> c.validate(v) + 1 + >>> c['stuff'] + '#ff00dd' + + + >>> spec = ['stuff = string(default="fish") # wooble'] + >>> c = ConfigObj(spec, _inspec=True) + >>> c['stuff'] + 'string(default="fish") # wooble' + """ + +def _test_many_check(): + """ + >>> spec = ['__many__ = integer()'] + >>> config = ['a = 6', 'b = 7'] + >>> c = ConfigObj(config, configspec=spec) + >>> v = Validator() + >>> c.validate(v) + 1 + >>> type(c['a']) + <type 'int'> + >>> type(c['b']) + <type 'int'> + + + >>> spec = ['[name]', '__many__ = integer()'] + >>> config = ['[name]', 'a = 6', 'b = 7'] + >>> c = ConfigObj(config, configspec=spec) + >>> v = Validator() + >>> c.validate(v) + 1 + >>> type(c['name']['a']) + <type 'int'> + >>> type(c['name']['b']) + <type 'int'> + + + >>> spec = ['[__many__]', '__many__ = integer()'] + >>> config = ['[name]', 'hello = 7', '[thing]', 'fish = 0'] + >>> c = ConfigObj(config, configspec=spec) + >>> v = Validator() + >>> c.validate(v) + 1 + >>> type(c['name']['hello']) + <type 'int'> + >>> type(c['thing']['fish']) + <type 'int'> + + + >>> spec = ''' + ... ___many___ = integer + ... [__many__] + ... ___many___ = boolean + ... [[__many__]] + ... __many__ = float + ... '''.splitlines() + >>> config = ''' + ... fish = 8 + ... buggle = 4 + ... [hi] + ... one = true + ... two = false + ... [[bye]] + ... odd = 3 + ... whoops = 9.0 + ... [bye] + ... one = true + ... two = true + ... [[lots]] + ... odd = 3 + ... whoops = 9.0 + ... '''.splitlines() + >>> c = ConfigObj(config, configspec=spec) + >>> v = Validator() + >>> c.validate(v) + 1 + >>> type(c['fish']) + <type 'int'> + >>> type(c['buggle']) + <type 'int'> + >>> c['hi']['one'] + 1 + >>> c['hi']['two'] + 0 + >>> type(c['hi']['bye']['odd']) + <type 'float'> + >>> type(c['hi']['bye']['whoops']) + <type 'float'> + >>> c['bye']['one'] + 1 + >>> c['bye']['two'] + 1 + >>> type(c['bye']['lots']['odd']) + <type 'float'> + >>> type(c['bye']['lots']['whoops']) + <type 'float'> + + + >>> spec = ['___many___ = integer()'] + >>> config = ['a = 6', 'b = 7'] + >>> c = ConfigObj(config, configspec=spec) + >>> v = Validator() + >>> c.validate(v) + 1 + >>> type(c['a']) + <type 'int'> + >>> type(c['b']) + <type 'int'> + + + >>> spec = ''' + ... [__many__] + ... [[__many__]] + ... __many__ = float + ... '''.splitlines() + >>> config = ''' + ... [hi] + ... [[bye]] + ... odd = 3 + ... whoops = 9.0 + ... [bye] + ... [[lots]] + ... odd = 3 + ... whoops = 9.0 + ... '''.splitlines() + >>> c = ConfigObj(config, configspec=spec) + >>> v = Validator() + >>> c.validate(v) + 1 + >>> type(c['hi']['bye']['odd']) + <type 'float'> + >>> type(c['hi']['bye']['whoops']) + <type 'float'> + >>> type(c['bye']['lots']['odd']) + <type 'float'> + >>> type(c['bye']['lots']['whoops']) + <type 'float'> + + >>> s = ['[dog]', '[[cow]]', 'something = boolean', '[[__many__]]', + ... 'fish = integer'] + >>> c = ['[dog]', '[[cow]]', 'something = true', '[[ob]]', + ... 'fish = 3', '[[bo]]', 'fish = 6'] + >>> ini = ConfigObj(c, configspec=s) + >>> v = Validator() + >>> ini.validate(v) + 1 + >>> ini['dog']['cow']['something'] + 1 + >>> ini['dog']['ob']['fish'] + 3 + >>> ini['dog']['bo']['fish'] + 6 + + + >>> s = ['[cow]', 'something = boolean', '[__many__]', + ... 'fish = integer'] + >>> c = ['[cow]', 'something = true', '[ob]', + ... 'fish = 3', '[bo]', 'fish = 6'] + >>> ini = ConfigObj(c, configspec=s) + >>> v = Validator() + >>> ini.validate(v) + 1 + >>> ini['cow']['something'] + 1 + >>> ini['ob']['fish'] + 3 + >>> ini['bo']['fish'] + 6 + """ + + +def _unexpected_validation_errors(): + """ + Although the input is nonsensical we should not crash but correctly + report the failure to validate + + # section specified, got scalar + >>> from validate import ValidateError + >>> s = ['[cow]', 'something = boolean'] + >>> c = ['cow = true'] + >>> ini = ConfigObj(c, configspec=s) + >>> v = Validator() + >>> ini.validate(v) + 0 + + >>> ini = ConfigObj(c, configspec=s) + >>> res = ini.validate(v, preserve_errors=True) + >>> check = flatten_errors(ini, res) + >>> for entry in check: + ... isinstance(entry[2], ValidateError) + ... print str(entry[2]) + True + Section 'cow' was provided as a single value + + + # scalar specified, got section + >>> s = ['something = boolean'] + >>> c = ['[something]', 'cow = true'] + >>> ini = ConfigObj(c, configspec=s) + >>> v = Validator() + >>> ini.validate(v) + 0 + + >>> ini = ConfigObj(c, configspec=s) + >>> res = ini.validate(v, preserve_errors=True) + >>> check = flatten_errors(ini, res) + >>> for entry in check: + ... isinstance(entry[2], ValidateError) + ... print str(entry[2]) + True + Value 'something' was provided as a section + + # unexpected section + >>> s = [] + >>> c = ['[cow]', 'dog = true'] + >>> ini = ConfigObj(c, configspec=s) + >>> v = Validator() + >>> ini.validate(v) + 1 + + + >>> s = ['[cow]', 'dog = bool'] + >>> c = ['[cow]', 'dog = true'] + >>> ini = ConfigObj(c, configspec=s) + >>> v = Validator() + >>> ini.validate(v, preserve_errors=True) + 1 + """ + +def _test_pickle(): + """ + >>> import pickle + >>> s = ['[cow]', 'dog = boolean'] + >>> c = ['[cow]', 'dog = true'] + >>> ini = ConfigObj(c, configspec=s) + >>> v = Validator() + >>> string = pickle.dumps(ini) + >>> new = pickle.loads(string) + >>> new.validate(v) + 1 + """ + +# drop support for Python 2.2 ? + + + +# TODO: Test BOM handling +# TODO: Test error code for badly built multiline values +# TODO: Test handling of StringIO +# TODO: Test interpolation with writing + + +if __name__ == '__main__': + # run the code tests in doctest format + # + testconfig1 = """\ + key1= val # comment 1 + key2= val # comment 2 + # comment 3 + [lev1a] # comment 4 + key1= val # comment 5 + key2= val # comment 6 + # comment 7 + [lev1b] # comment 8 + key1= val # comment 9 + key2= val # comment 10 + # comment 11 + [[lev2ba]] # comment 12 + key1= val # comment 13 + # comment 14 + [[lev2bb]] # comment 15 + key1= val # comment 16 + # comment 17 + [lev1c] # comment 18 + # comment 19 + [[lev2c]] # comment 20 + # comment 21 + [[[lev3c]]] # comment 22 + key1 = val # comment 23""" + # + testconfig2 = """\ + key1 = 'val1' + key2 = "val2" + key3 = val3 + ["section 1"] # comment + keys11 = val1 + keys12 = val2 + keys13 = val3 + [section 2] + keys21 = val1 + keys22 = val2 + keys23 = val3 + + [['section 2 sub 1']] + fish = 3 + """ + # + testconfig6 = ''' + name1 = """ a single line value """ # comment + name2 = \''' another single line value \''' # comment + name3 = """ a single line value """ + name4 = \''' another single line value \''' + [ "multi section" ] + name1 = """ + Well, this is a + multiline value + """ + name2 = \''' + Well, this is a + multiline value + \''' + name3 = """ + Well, this is a + multiline value + """ # a comment + name4 = \''' + Well, this is a + multiline value + \''' # I guess this is a comment too + ''' + # + # these cannot be put among the doctests, because the doctest module + # does a string.expandtabs() on all of them, sigh + oneTabCfg = ['[sect]', '\t[[sect]]', '\t\tfoo = bar'] + twoTabsCfg = ['[sect]', '\t\t[[sect]]', '\t\t\t\tfoo = bar'] + tabsAndSpacesCfg = ['[sect]', '\t \t [[sect]]', '\t \t \t \t foo = bar'] + # + import doctest + m = sys.modules.get('__main__') + globs = m.__dict__.copy() + a = ConfigObj(testconfig1.split('\n'), raise_errors=True) + b = ConfigObj(testconfig2.split('\n'), raise_errors=True) + i = ConfigObj(testconfig6.split('\n'), raise_errors=True) + globs.update({'INTP_VER': INTP_VER, 'a': a, 'b': b, 'i': i, + 'oneTabCfg': oneTabCfg, 'twoTabsCfg': twoTabsCfg, + 'tabsAndSpacesCfg': tabsAndSpacesCfg}) + doctest.testmod(m, globs=globs) + + +# Man alive I prefer unittest ;-)
\ No newline at end of file diff --git a/validate.py b/validate.py new file mode 100644 index 0000000..3370fe8 --- /dev/null +++ b/validate.py @@ -0,0 +1,1423 @@ +# validate.py +# A Validator object +# Copyright (C) 2005 Michael Foord, Mark Andrews, Nicola Larosa +# E-mail: fuzzyman AT voidspace DOT org DOT uk +# mark AT la-la DOT com +# nico AT tekNico DOT net + +# This software is licensed under the terms of the BSD license. +# http://www.voidspace.org.uk/python/license.shtml +# Basically you're free to copy, modify, distribute and relicense it, +# So long as you keep a copy of the license with it. + +# 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. + +""" + The Validator object is used to check that supplied values + conform to a specification. + + The value can be supplied as a string - e.g. from a config file. + In this case the check will also *convert* the value to + the required type. This allows you to add validation + as a transparent layer to access data stored as strings. + The validation checks that the data is correct *and* + converts it to the expected type. + + Some standard checks are provided for basic data types. + Additional checks are easy to write. They can be + provided when the ``Validator`` is instantiated or + added afterwards. + + The standard functions work with the following basic data types : + + * integers + * floats + * booleans + * strings + * ip_addr + + plus lists of these datatypes + + Adding additional checks is done through coding simple functions. + + The full set of standard checks are : + + * 'integer': matches integer values (including negative) + Takes optional 'min' and 'max' arguments : :: + + integer() + integer(3, 9) # any value from 3 to 9 + integer(min=0) # any positive value + integer(max=9) + + * 'float': matches float values + Has the same parameters as the integer check. + + * 'boolean': matches boolean values - ``True`` or ``False`` + Acceptable string values for True are : + true, on, yes, 1 + Acceptable string values for False are : + false, off, no, 0 + + Any other value raises an error. + + * 'ip_addr': matches an Internet Protocol address, v.4, represented + by a dotted-quad string, i.e. '1.2.3.4'. + + * 'string': matches any string. + Takes optional keyword args 'min' and 'max' + to specify min and max lengths of the string. + + * 'list': matches any list. + Takes optional keyword args 'min', and 'max' to specify min and + max sizes of the list. (Always returns a list.) + + * 'tuple': matches any tuple. + Takes optional keyword args 'min', and 'max' to specify min and + max sizes of the tuple. (Always returns a tuple.) + + * 'int_list': Matches a list of integers. + Takes the same arguments as list. + + * 'float_list': Matches a list of floats. + Takes the same arguments as list. + + * 'bool_list': Matches a list of boolean values. + Takes the same arguments as list. + + * 'ip_addr_list': Matches a list of IP addresses. + Takes the same arguments as list. + + * 'string_list': Matches a list of strings. + Takes the same arguments as list. + + * 'mixed_list': Matches a list with different types in + specific positions. List size must match + the number of arguments. + + Each position can be one of : + 'integer', 'float', 'ip_addr', 'string', 'boolean' + + So to specify a list with two strings followed + by two integers, you write the check as : :: + + mixed_list('string', 'string', 'integer', 'integer') + + * 'pass': This check matches everything ! It never fails + and the value is unchanged. + + It is also the default if no check is specified. + + * 'option': This check matches any from a list of options. + You specify this check with : :: + + option('option 1', 'option 2', 'option 3') + + You can supply a default value (returned if no value is supplied) + using the default keyword argument. + + You specify a list argument for default using a list constructor syntax in + the check : :: + + checkname(arg1, arg2, default=list('val 1', 'val 2', 'val 3')) + + A badly formatted set of arguments will raise a ``VdtParamError``. +""" + +__docformat__ = "restructuredtext en" + +__version__ = '0.3.2' + +__revision__ = '$Id: validate.py 123 2005-09-08 08:54:28Z fuzzyman $' + +__all__ = ( + '__version__', + 'dottedQuadToNum', + 'numToDottedQuad', + 'ValidateError', + 'VdtUnknownCheckError', + 'VdtParamError', + 'VdtTypeError', + 'VdtValueError', + 'VdtValueTooSmallError', + 'VdtValueTooBigError', + 'VdtValueTooShortError', + 'VdtValueTooLongError', + 'VdtMissingValue', + 'Validator', + 'is_integer', + 'is_float', + 'is_boolean', + 'is_list', + 'is_tuple', + 'is_ip_addr', + 'is_string', + 'is_int_list', + 'is_bool_list', + 'is_float_list', + 'is_string_list', + 'is_ip_addr_list', + 'is_mixed_list', + 'is_option', + '__docformat__', +) + + +import sys +INTP_VER = sys.version_info[:2] +if INTP_VER < (2, 2): + raise RuntimeError("Python v.2.2 or later needed") + +import re +StringTypes = (str, unicode) + + +_list_arg = re.compile(r''' + (?: + ([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*list\( + ( + (?: + \s* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s\)][^,\)]*?) # unquoted + ) + \s*,\s* + )* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s\)][^,\)]*?) # unquoted + )? # last one + ) + \) + ) +''', re.VERBOSE) # two groups + +_list_members = re.compile(r''' + ( + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s=][^,=]*?) # unquoted + ) + (?: + (?:\s*,\s*)|(?:\s*$) # comma + ) +''', re.VERBOSE) # one group + +_paramstring = r''' + (?: + ( + (?: + [a-zA-Z_][a-zA-Z0-9_]*\s*=\s*list\( + (?: + \s* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s\)][^,\)]*?) # unquoted + ) + \s*,\s* + )* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s\)][^,\)]*?) # unquoted + )? # last one + \) + )| + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s=][^,=]*?)| # unquoted + (?: # keyword argument + [a-zA-Z_][a-zA-Z0-9_]*\s*=\s* + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s=][^,=]*?) # unquoted + ) + ) + ) + ) + (?: + (?:\s*,\s*)|(?:\s*$) # comma + ) + ) + ''' + +_matchstring = '^%s*' % _paramstring + +# Python pre 2.2.1 doesn't have bool +try: + bool +except NameError: + def bool(val): + """Simple boolean equivalent function. """ + if val: + return 1 + else: + return 0 + + +def dottedQuadToNum(ip): + """ + Convert decimal dotted quad string to long integer + + >>> int(dottedQuadToNum('1 ')) + 1 + >>> int(dottedQuadToNum(' 1.2')) + 16777218 + >>> int(dottedQuadToNum(' 1.2.3 ')) + 16908291 + >>> int(dottedQuadToNum('1.2.3.4')) + 16909060 + >>> dottedQuadToNum('1.2.3. 4') + 16909060 + >>> dottedQuadToNum('255.255.255.255') + 4294967295L + >>> dottedQuadToNum('255.255.255.256') + Traceback (most recent call last): + ValueError: Not a good dotted-quad IP: 255.255.255.256 + """ + + # import here to avoid it when ip_addr values are not used + import socket, struct + + try: + return struct.unpack('!L', + socket.inet_aton(ip.strip()))[0] + except socket.error: + # bug in inet_aton, corrected in Python 2.3 + if ip.strip() == '255.255.255.255': + return 0xFFFFFFFFL + else: + raise ValueError('Not a good dotted-quad IP: %s' % ip) + return + + +def numToDottedQuad(num): + """ + Convert long int to dotted quad string + + >>> numToDottedQuad(-1L) + Traceback (most recent call last): + ValueError: Not a good numeric IP: -1 + >>> numToDottedQuad(1L) + '0.0.0.1' + >>> numToDottedQuad(16777218L) + '1.0.0.2' + >>> numToDottedQuad(16908291L) + '1.2.0.3' + >>> numToDottedQuad(16909060L) + '1.2.3.4' + >>> numToDottedQuad(4294967295L) + '255.255.255.255' + >>> numToDottedQuad(4294967296L) + Traceback (most recent call last): + ValueError: Not a good numeric IP: 4294967296 + """ + + # import here to avoid it when ip_addr values are not used + import socket, struct + + # no need to intercept here, 4294967295L is fine + if num > 4294967295L or num < 0: + raise ValueError('Not a good numeric IP: %s' % num) + try: + return socket.inet_ntoa( + struct.pack('!L', long(num))) + except (socket.error, struct.error, OverflowError): + raise ValueError('Not a good numeric IP: %s' % num) + + +class ValidateError(Exception): + """ + This error indicates that the check failed. + It can be the base class for more specific errors. + + Any check function that fails ought to raise this error. + (or a subclass) + + >>> raise ValidateError + Traceback (most recent call last): + ValidateError + """ + + +class VdtMissingValue(ValidateError): + """No value was supplied to a check that needed one.""" + + +class VdtUnknownCheckError(ValidateError): + """An unknown check function was requested""" + + def __init__(self, value): + """ + >>> raise VdtUnknownCheckError('yoda') + Traceback (most recent call last): + VdtUnknownCheckError: the check "yoda" is unknown. + """ + ValidateError.__init__(self, 'the check "%s" is unknown.' % (value,)) + + +class VdtParamError(SyntaxError): + """An incorrect parameter was passed""" + + def __init__(self, name, value): + """ + >>> raise VdtParamError('yoda', 'jedi') + Traceback (most recent call last): + VdtParamError: passed an incorrect value "jedi" for parameter "yoda". + """ + SyntaxError.__init__(self, 'passed an incorrect value "%s" for parameter "%s".' % (value, name)) + + +class VdtTypeError(ValidateError): + """The value supplied was of the wrong type""" + + def __init__(self, value): + """ + >>> raise VdtTypeError('jedi') + Traceback (most recent call last): + VdtTypeError: the value "jedi" is of the wrong type. + """ + ValidateError.__init__(self, 'the value "%s" is of the wrong type.' % (value,)) + + +class VdtValueError(ValidateError): + """The value supplied was of the correct type, but was not an allowed value.""" + + def __init__(self, value): + """ + >>> raise VdtValueError('jedi') + Traceback (most recent call last): + VdtValueError: the value "jedi" is unacceptable. + """ + ValidateError.__init__(self, 'the value "%s" is unacceptable.' % (value,)) + + +class VdtValueTooSmallError(VdtValueError): + """The value supplied was of the correct type, but was too small.""" + + def __init__(self, value): + """ + >>> raise VdtValueTooSmallError('0') + Traceback (most recent call last): + VdtValueTooSmallError: the value "0" is too small. + """ + ValidateError.__init__(self, 'the value "%s" is too small.' % (value,)) + + +class VdtValueTooBigError(VdtValueError): + """The value supplied was of the correct type, but was too big.""" + + def __init__(self, value): + """ + >>> raise VdtValueTooBigError('1') + Traceback (most recent call last): + VdtValueTooBigError: the value "1" is too big. + """ + ValidateError.__init__(self, 'the value "%s" is too big.' % (value,)) + + +class VdtValueTooShortError(VdtValueError): + """The value supplied was of the correct type, but was too short.""" + + def __init__(self, value): + """ + >>> raise VdtValueTooShortError('jed') + Traceback (most recent call last): + VdtValueTooShortError: the value "jed" is too short. + """ + ValidateError.__init__( + self, + 'the value "%s" is too short.' % (value,)) + + +class VdtValueTooLongError(VdtValueError): + """The value supplied was of the correct type, but was too long.""" + + def __init__(self, value): + """ + >>> raise VdtValueTooLongError('jedie') + Traceback (most recent call last): + VdtValueTooLongError: the value "jedie" is too long. + """ + ValidateError.__init__(self, 'the value "%s" is too long.' % (value,)) + + +class Validator(object): + """ + Validator is an object that allows you to register a set of 'checks'. + These checks take input and test that it conforms to the check. + + This can also involve converting the value from a string into + the correct datatype. + + The ``check`` method takes an input string which configures which + check is to be used and applies that check to a supplied value. + + An example input string would be: + 'int_range(param1, param2)' + + You would then provide something like: + + >>> def int_range_check(value, min, max): + ... # turn min and max from strings to integers + ... min = int(min) + ... max = int(max) + ... # check that value is of the correct type. + ... # possible valid inputs are integers or strings + ... # that represent integers + ... if not isinstance(value, (int, long, StringTypes)): + ... raise VdtTypeError(value) + ... elif isinstance(value, StringTypes): + ... # if we are given a string + ... # attempt to convert to an integer + ... try: + ... value = int(value) + ... except ValueError: + ... raise VdtValueError(value) + ... # check the value is between our constraints + ... if not min <= value: + ... raise VdtValueTooSmallError(value) + ... if not value <= max: + ... raise VdtValueTooBigError(value) + ... return value + + >>> fdict = {'int_range': int_range_check} + >>> vtr1 = Validator(fdict) + >>> vtr1.check('int_range(20, 40)', '30') + 30 + >>> vtr1.check('int_range(20, 40)', '60') + Traceback (most recent call last): + VdtValueTooBigError: the value "60" is too big. + + New functions can be added with : :: + + >>> vtr2 = Validator() + >>> vtr2.functions['int_range'] = int_range_check + + Or by passing in a dictionary of functions when Validator + is instantiated. + + Your functions *can* use keyword arguments, + but the first argument should always be 'value'. + + If the function doesn't take additional arguments, + the parentheses are optional in the check. + It can be written with either of : :: + + keyword = function_name + keyword = function_name() + + The first program to utilise Validator() was Michael Foord's + ConfigObj, an alternative to ConfigParser which supports lists and + can validate a config file using a config schema. + For more details on using Validator with ConfigObj see: + http://www.voidspace.org.uk/python/configobj.html + """ + + # this regex does the initial parsing of the checks + _func_re = re.compile(r'(.+?)\((.*)\)') + + # this regex takes apart keyword arguments + _key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$') + + + # this regex finds keyword=list(....) type values + _list_arg = _list_arg + + # this regex takes individual values out of lists - in one pass + _list_members = _list_members + + # These regexes check a set of arguments for validity + # and then pull the members out + _paramfinder = re.compile(_paramstring, re.VERBOSE) + _matchfinder = re.compile(_matchstring, re.VERBOSE) + + + def __init__(self, functions=None): + """ + >>> vtri = Validator() + """ + self.functions = { + '': self._pass, + 'integer': is_integer, + 'float': is_float, + 'boolean': is_boolean, + 'ip_addr': is_ip_addr, + 'string': is_string, + 'list': is_list, + 'tuple': is_tuple, + 'int_list': is_int_list, + 'float_list': is_float_list, + 'bool_list': is_bool_list, + 'ip_addr_list': is_ip_addr_list, + 'string_list': is_string_list, + 'mixed_list': is_mixed_list, + 'pass': self._pass, + 'option': is_option, + } + if functions is not None: + self.functions.update(functions) + # tekNico: for use by ConfigObj + self.baseErrorClass = ValidateError + self._cache = {} + + + def check(self, check, value, missing=False): + """ + Usage: check(check, value) + + Arguments: + check: string representing check to apply (including arguments) + value: object to be checked + Returns value, converted to correct type if necessary + + If the check fails, raises a ``ValidateError`` subclass. + + >>> vtor.check('yoda', '') + Traceback (most recent call last): + VdtUnknownCheckError: the check "yoda" is unknown. + >>> vtor.check('yoda()', '') + Traceback (most recent call last): + VdtUnknownCheckError: the check "yoda" is unknown. + + >>> vtor.check('string(default="")', '', missing=True) + '' + """ + fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) + + if missing: + if default is None: + # no information needed here - to be handled by caller + raise VdtMissingValue() + value = self._handle_none(default) + + if value is None: + return None + + return self._check_value(value, fun_name, fun_args, fun_kwargs) + + + def _handle_none(self, value): + if value == 'None': + value = None + elif value in ("'None'", '"None"'): + # Special case a quoted None + value = self._unquote(value) + return value + + + def _parse_with_caching(self, check): + if check in self._cache: + fun_name, fun_args, fun_kwargs, default = self._cache[check] + # We call list and dict below to work with *copies* of the data + # rather than the original (which are mutable of course) + fun_args = list(fun_args) + fun_kwargs = dict(fun_kwargs) + else: + fun_name, fun_args, fun_kwargs, default = self._parse_check(check) + fun_kwargs = dict((str(key), value) for (key, value) in fun_kwargs.items()) + self._cache[check] = fun_name, list(fun_args), dict(fun_kwargs), default + return fun_name, fun_args, fun_kwargs, default + + + def _check_value(self, value, fun_name, fun_args, fun_kwargs): + try: + fun = self.functions[fun_name] + except KeyError: + raise VdtUnknownCheckError(fun_name) + else: + return fun(value, *fun_args, **fun_kwargs) + + + def _parse_check(self, check): + fun_match = self._func_re.match(check) + if fun_match: + fun_name = fun_match.group(1) + arg_string = fun_match.group(2) + arg_match = self._matchfinder.match(arg_string) + if arg_match is None: + # Bad syntax + raise VdtParamError('Bad syntax in check "%s".' % check) + fun_args = [] + fun_kwargs = {} + # pull out args of group 2 + for arg in self._paramfinder.findall(arg_string): + # args may need whitespace removing (before removing quotes) + arg = arg.strip() + listmatch = self._list_arg.match(arg) + if listmatch: + key, val = self._list_handle(listmatch) + fun_kwargs[key] = val + continue + keymatch = self._key_arg.match(arg) + if keymatch: + val = keymatch.group(2) + if not val in ("'None'", '"None"'): + # Special case a quoted None + val = self._unquote(val) + fun_kwargs[keymatch.group(1)] = val + continue + + fun_args.append(self._unquote(arg)) + else: + # allows for function names without (args) + return check, (), {}, None + + # Default must be deleted if the value is specified too, + # otherwise the check function will get a spurious "default" keyword arg + try: + default = fun_kwargs.pop('default', None) + except AttributeError: + # Python 2.2 compatibility + default = None + try: + default = fun_kwargs['default'] + del fun_kwargs['default'] + except KeyError: + pass + + return fun_name, fun_args, fun_kwargs, default + + + def _unquote(self, val): + """Unquote a value if necessary.""" + if (len(val) >= 2) and (val[0] in ("'", '"')) and (val[0] == val[-1]): + val = val[1:-1] + return val + + + def _list_handle(self, listmatch): + """Take apart a ``keyword=list('val, 'val')`` type string.""" + out = [] + name = listmatch.group(1) + args = listmatch.group(2) + for arg in self._list_members.findall(args): + out.append(self._unquote(arg)) + return name, out + + + def _pass(self, value): + """ + Dummy check that always passes + + >>> vtor.check('', 0) + 0 + >>> vtor.check('', '0') + '0' + """ + return value + + + def get_default_value(self, check): + """ + Given a check, return the default value for the check + (converted to the right type). + + If the check doesn't specify a default value then a + ``KeyError`` will be raised. + """ + fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) + if default is None: + raise KeyError('Check "%s" has no default value.' % check) + value = self._handle_none(default) + if value is None: + return value + return self._check_value(value, fun_name, fun_args, fun_kwargs) + + +def _is_num_param(names, values, to_float=False): + """ + Return numbers from inputs or raise VdtParamError. + + Lets ``None`` pass through. + Pass in keyword argument ``to_float=True`` to + use float for the conversion rather than int. + + >>> _is_num_param(('', ''), (0, 1.0)) + [0, 1] + >>> _is_num_param(('', ''), (0, 1.0), to_float=True) + [0.0, 1.0] + >>> _is_num_param(('a'), ('a')) + Traceback (most recent call last): + VdtParamError: passed an incorrect value "a" for parameter "a". + """ + fun = to_float and float or int + out_params = [] + for (name, val) in zip(names, values): + if val is None: + out_params.append(val) + elif isinstance(val, (int, long, float, StringTypes)): + try: + out_params.append(fun(val)) + except ValueError, e: + raise VdtParamError(name, val) + else: + raise VdtParamError(name, val) + return out_params + + +# built in checks +# you can override these by setting the appropriate name +# in Validator.functions +# note: if the params are specified wrongly in your input string, +# you will also raise errors. + +def is_integer(value, min=None, max=None): + """ + A check that tests that a given value is an integer (int, or long) + and optionally, between bounds. A negative value is accepted, while + a float will fail. + + If the value is a string, then the conversion is done - if possible. + Otherwise a VdtError is raised. + + >>> vtor.check('integer', '-1') + -1 + >>> vtor.check('integer', '0') + 0 + >>> vtor.check('integer', 9) + 9 + >>> vtor.check('integer', 'a') + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + >>> vtor.check('integer', '2.2') + Traceback (most recent call last): + VdtTypeError: the value "2.2" is of the wrong type. + >>> vtor.check('integer(10)', '20') + 20 + >>> vtor.check('integer(max=20)', '15') + 15 + >>> vtor.check('integer(10)', '9') + Traceback (most recent call last): + VdtValueTooSmallError: the value "9" is too small. + >>> vtor.check('integer(10)', 9) + Traceback (most recent call last): + VdtValueTooSmallError: the value "9" is too small. + >>> vtor.check('integer(max=20)', '35') + Traceback (most recent call last): + VdtValueTooBigError: the value "35" is too big. + >>> vtor.check('integer(max=20)', 35) + Traceback (most recent call last): + VdtValueTooBigError: the value "35" is too big. + >>> vtor.check('integer(0, 9)', False) + 0 + """ + (min_val, max_val) = _is_num_param(('min', 'max'), (min, max)) + if not isinstance(value, (int, long, StringTypes)): + raise VdtTypeError(value) + if isinstance(value, StringTypes): + # if it's a string - does it represent an integer ? + try: + value = int(value) + except ValueError: + raise VdtTypeError(value) + if (min_val is not None) and (value < min_val): + raise VdtValueTooSmallError(value) + if (max_val is not None) and (value > max_val): + raise VdtValueTooBigError(value) + return value + + +def is_float(value, min=None, max=None): + """ + A check that tests that a given value is a float + (an integer will be accepted), and optionally - that it is between bounds. + + If the value is a string, then the conversion is done - if possible. + Otherwise a VdtError is raised. + + This can accept negative values. + + >>> vtor.check('float', '2') + 2.0 + + From now on we multiply the value to avoid comparing decimals + + >>> vtor.check('float', '-6.8') * 10 + -68.0 + >>> vtor.check('float', '12.2') * 10 + 122.0 + >>> vtor.check('float', 8.4) * 10 + 84.0 + >>> vtor.check('float', 'a') + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + >>> vtor.check('float(10.1)', '10.2') * 10 + 102.0 + >>> vtor.check('float(max=20.2)', '15.1') * 10 + 151.0 + >>> vtor.check('float(10.0)', '9.0') + Traceback (most recent call last): + VdtValueTooSmallError: the value "9.0" is too small. + >>> vtor.check('float(max=20.0)', '35.0') + Traceback (most recent call last): + VdtValueTooBigError: the value "35.0" is too big. + """ + (min_val, max_val) = _is_num_param( + ('min', 'max'), (min, max), to_float=True) + if not isinstance(value, (int, long, float, StringTypes)): + raise VdtTypeError(value) + if not isinstance(value, float): + # if it's a string - does it represent a float ? + try: + value = float(value) + except ValueError: + raise VdtTypeError(value) + if (min_val is not None) and (value < min_val): + raise VdtValueTooSmallError(value) + if (max_val is not None) and (value > max_val): + raise VdtValueTooBigError(value) + return value + + +bool_dict = { + True: True, 'on': True, '1': True, 'true': True, 'yes': True, + False: False, 'off': False, '0': False, 'false': False, 'no': False, +} + + +def is_boolean(value): + """ + Check if the value represents a boolean. + + >>> vtor.check('boolean', 0) + 0 + >>> vtor.check('boolean', False) + 0 + >>> vtor.check('boolean', '0') + 0 + >>> vtor.check('boolean', 'off') + 0 + >>> vtor.check('boolean', 'false') + 0 + >>> vtor.check('boolean', 'no') + 0 + >>> vtor.check('boolean', 'nO') + 0 + >>> vtor.check('boolean', 'NO') + 0 + >>> vtor.check('boolean', 1) + 1 + >>> vtor.check('boolean', True) + 1 + >>> vtor.check('boolean', '1') + 1 + >>> vtor.check('boolean', 'on') + 1 + >>> vtor.check('boolean', 'true') + 1 + >>> vtor.check('boolean', 'yes') + 1 + >>> vtor.check('boolean', 'Yes') + 1 + >>> vtor.check('boolean', 'YES') + 1 + >>> vtor.check('boolean', '') + Traceback (most recent call last): + VdtTypeError: the value "" is of the wrong type. + >>> vtor.check('boolean', 'up') + Traceback (most recent call last): + VdtTypeError: the value "up" is of the wrong type. + + """ + if isinstance(value, StringTypes): + try: + return bool_dict[value.lower()] + except KeyError: + raise VdtTypeError(value) + # we do an equality test rather than an identity test + # this ensures Python 2.2 compatibilty + # and allows 0 and 1 to represent True and False + if value == False: + return False + elif value == True: + return True + else: + raise VdtTypeError(value) + + +def is_ip_addr(value): + """ + Check that the supplied value is an Internet Protocol address, v.4, + represented by a dotted-quad string, i.e. '1.2.3.4'. + + >>> vtor.check('ip_addr', '1 ') + '1' + >>> vtor.check('ip_addr', ' 1.2') + '1.2' + >>> vtor.check('ip_addr', ' 1.2.3 ') + '1.2.3' + >>> vtor.check('ip_addr', '1.2.3.4') + '1.2.3.4' + >>> vtor.check('ip_addr', '0.0.0.0') + '0.0.0.0' + >>> vtor.check('ip_addr', '255.255.255.255') + '255.255.255.255' + >>> vtor.check('ip_addr', '255.255.255.256') + Traceback (most recent call last): + VdtValueError: the value "255.255.255.256" is unacceptable. + >>> vtor.check('ip_addr', '1.2.3.4.5') + Traceback (most recent call last): + VdtValueError: the value "1.2.3.4.5" is unacceptable. + >>> vtor.check('ip_addr', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + """ + if not isinstance(value, StringTypes): + raise VdtTypeError(value) + value = value.strip() + try: + dottedQuadToNum(value) + except ValueError: + raise VdtValueError(value) + return value + + +def is_list(value, min=None, max=None): + """ + Check that the value is a list of values. + + You can optionally specify the minimum and maximum number of members. + + It does no check on list members. + + >>> vtor.check('list', ()) + [] + >>> vtor.check('list', []) + [] + >>> vtor.check('list', (1, 2)) + [1, 2] + >>> vtor.check('list', [1, 2]) + [1, 2] + >>> vtor.check('list(3)', (1, 2)) + Traceback (most recent call last): + VdtValueTooShortError: the value "(1, 2)" is too short. + >>> vtor.check('list(max=5)', (1, 2, 3, 4, 5, 6)) + Traceback (most recent call last): + VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. + >>> vtor.check('list(min=3, max=5)', (1, 2, 3, 4)) + [1, 2, 3, 4] + >>> vtor.check('list', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + >>> vtor.check('list', '12') + Traceback (most recent call last): + VdtTypeError: the value "12" is of the wrong type. + """ + (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) + if isinstance(value, StringTypes): + raise VdtTypeError(value) + try: + num_members = len(value) + except TypeError: + raise VdtTypeError(value) + if min_len is not None and num_members < min_len: + raise VdtValueTooShortError(value) + if max_len is not None and num_members > max_len: + raise VdtValueTooLongError(value) + return list(value) + + +def is_tuple(value, min=None, max=None): + """ + Check that the value is a tuple of values. + + You can optionally specify the minimum and maximum number of members. + + It does no check on members. + + >>> vtor.check('tuple', ()) + () + >>> vtor.check('tuple', []) + () + >>> vtor.check('tuple', (1, 2)) + (1, 2) + >>> vtor.check('tuple', [1, 2]) + (1, 2) + >>> vtor.check('tuple(3)', (1, 2)) + Traceback (most recent call last): + VdtValueTooShortError: the value "(1, 2)" is too short. + >>> vtor.check('tuple(max=5)', (1, 2, 3, 4, 5, 6)) + Traceback (most recent call last): + VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. + >>> vtor.check('tuple(min=3, max=5)', (1, 2, 3, 4)) + (1, 2, 3, 4) + >>> vtor.check('tuple', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + >>> vtor.check('tuple', '12') + Traceback (most recent call last): + VdtTypeError: the value "12" is of the wrong type. + """ + return tuple(is_list(value, min, max)) + + +def is_string(value, min=None, max=None): + """ + Check that the supplied value is a string. + + You can optionally specify the minimum and maximum number of members. + + >>> vtor.check('string', '0') + '0' + >>> vtor.check('string', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + >>> vtor.check('string(2)', '12') + '12' + >>> vtor.check('string(2)', '1') + Traceback (most recent call last): + VdtValueTooShortError: the value "1" is too short. + >>> vtor.check('string(min=2, max=3)', '123') + '123' + >>> vtor.check('string(min=2, max=3)', '1234') + Traceback (most recent call last): + VdtValueTooLongError: the value "1234" is too long. + """ + if not isinstance(value, StringTypes): + raise VdtTypeError(value) + (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) + try: + num_members = len(value) + except TypeError: + raise VdtTypeError(value) + if min_len is not None and num_members < min_len: + raise VdtValueTooShortError(value) + if max_len is not None and num_members > max_len: + raise VdtValueTooLongError(value) + return value + + +def is_int_list(value, min=None, max=None): + """ + Check that the value is a list of integers. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is an integer. + + >>> vtor.check('int_list', ()) + [] + >>> vtor.check('int_list', []) + [] + >>> vtor.check('int_list', (1, 2)) + [1, 2] + >>> vtor.check('int_list', [1, 2]) + [1, 2] + >>> vtor.check('int_list', [1, 'a']) + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + """ + return [is_integer(mem) for mem in is_list(value, min, max)] + + +def is_bool_list(value, min=None, max=None): + """ + Check that the value is a list of booleans. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is a boolean. + + >>> vtor.check('bool_list', ()) + [] + >>> vtor.check('bool_list', []) + [] + >>> check_res = vtor.check('bool_list', (True, False)) + >>> check_res == [True, False] + 1 + >>> check_res = vtor.check('bool_list', [True, False]) + >>> check_res == [True, False] + 1 + >>> vtor.check('bool_list', [True, 'a']) + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + """ + return [is_boolean(mem) for mem in is_list(value, min, max)] + + +def is_float_list(value, min=None, max=None): + """ + Check that the value is a list of floats. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is a float. + + >>> vtor.check('float_list', ()) + [] + >>> vtor.check('float_list', []) + [] + >>> vtor.check('float_list', (1, 2.0)) + [1.0, 2.0] + >>> vtor.check('float_list', [1, 2.0]) + [1.0, 2.0] + >>> vtor.check('float_list', [1, 'a']) + Traceback (most recent call last): + VdtTypeError: the value "a" is of the wrong type. + """ + return [is_float(mem) for mem in is_list(value, min, max)] + + +def is_string_list(value, min=None, max=None): + """ + Check that the value is a list of strings. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is a string. + + >>> vtor.check('string_list', ()) + [] + >>> vtor.check('string_list', []) + [] + >>> vtor.check('string_list', ('a', 'b')) + ['a', 'b'] + >>> vtor.check('string_list', ['a', 1]) + Traceback (most recent call last): + VdtTypeError: the value "1" is of the wrong type. + >>> vtor.check('string_list', 'hello') + Traceback (most recent call last): + VdtTypeError: the value "hello" is of the wrong type. + """ + if isinstance(value, StringTypes): + raise VdtTypeError(value) + return [is_string(mem) for mem in is_list(value, min, max)] + + +def is_ip_addr_list(value, min=None, max=None): + """ + Check that the value is a list of IP addresses. + + You can optionally specify the minimum and maximum number of members. + + Each list member is checked that it is an IP address. + + >>> vtor.check('ip_addr_list', ()) + [] + >>> vtor.check('ip_addr_list', []) + [] + >>> vtor.check('ip_addr_list', ('1.2.3.4', '5.6.7.8')) + ['1.2.3.4', '5.6.7.8'] + >>> vtor.check('ip_addr_list', ['a']) + Traceback (most recent call last): + VdtValueError: the value "a" is unacceptable. + """ + return [is_ip_addr(mem) for mem in is_list(value, min, max)] + + +fun_dict = { + 'integer': is_integer, + 'float': is_float, + 'ip_addr': is_ip_addr, + 'string': is_string, + 'boolean': is_boolean, +} + + +def is_mixed_list(value, *args): + """ + Check that the value is a list. + Allow specifying the type of each member. + Work on lists of specific lengths. + + You specify each member as a positional argument specifying type + + Each type should be one of the following strings : + 'integer', 'float', 'ip_addr', 'string', 'boolean' + + So you can specify a list of two strings, followed by + two integers as : + + mixed_list('string', 'string', 'integer', 'integer') + + The length of the list must match the number of positional + arguments you supply. + + >>> mix_str = "mixed_list('integer', 'float', 'ip_addr', 'string', 'boolean')" + >>> check_res = vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', True)) + >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] + 1 + >>> check_res = vtor.check(mix_str, ('1', '2.0', '1.2.3.4', 'a', 'True')) + >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] + 1 + >>> vtor.check(mix_str, ('b', 2.0, '1.2.3.4', 'a', True)) + Traceback (most recent call last): + VdtTypeError: the value "b" is of the wrong type. + >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a')) + Traceback (most recent call last): + VdtValueTooShortError: the value "(1, 2.0, '1.2.3.4', 'a')" is too short. + >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', 1, 'b')) + Traceback (most recent call last): + VdtValueTooLongError: the value "(1, 2.0, '1.2.3.4', 'a', 1, 'b')" is too long. + >>> vtor.check(mix_str, 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + + This test requires an elaborate setup, because of a change in error string + output from the interpreter between Python 2.2 and 2.3 . + + >>> res_seq = ( + ... 'passed an incorrect value "', + ... 'yoda', + ... '" for parameter "mixed_list".', + ... ) + >>> if INTP_VER == (2, 2): + ... res_str = "".join(res_seq) + ... else: + ... res_str = "'".join(res_seq) + >>> try: + ... vtor.check('mixed_list("yoda")', ('a')) + ... except VdtParamError, err: + ... str(err) == res_str + 1 + """ + try: + length = len(value) + except TypeError: + raise VdtTypeError(value) + if length < len(args): + raise VdtValueTooShortError(value) + elif length > len(args): + raise VdtValueTooLongError(value) + try: + return [fun_dict[arg](val) for arg, val in zip(args, value)] + except KeyError, e: + raise VdtParamError('mixed_list', e) + + +def is_option(value, *options): + """ + This check matches the value to any of a set of options. + + >>> vtor.check('option("yoda", "jedi")', 'yoda') + 'yoda' + >>> vtor.check('option("yoda", "jedi")', 'jed') + Traceback (most recent call last): + VdtValueError: the value "jed" is unacceptable. + >>> vtor.check('option("yoda", "jedi")', 0) + Traceback (most recent call last): + VdtTypeError: the value "0" is of the wrong type. + """ + if not isinstance(value, StringTypes): + raise VdtTypeError(value) + if not value in options: + raise VdtValueError(value) + return value + + +def _test(value, *args, **keywargs): + """ + A function that exists for test purposes. + + >>> checks = [ + ... '3, 6, min=1, max=3, test=list(a, b, c)', + ... '3', + ... '3, 6', + ... '3,', + ... 'min=1, test="a b c"', + ... 'min=5, test="a, b, c"', + ... 'min=1, max=3, test="a, b, c"', + ... 'min=-100, test=-99', + ... 'min=1, max=3', + ... '3, 6, test="36"', + ... '3, 6, test="a, b, c"', + ... '3, max=3, test=list("a", "b", "c")', + ... '''3, max=3, test=list("'a'", 'b', "x=(c)")''', + ... "test='x=fish(3)'", + ... ] + >>> v = Validator({'test': _test}) + >>> for entry in checks: + ... print v.check(('test(%s)' % entry), 3) + (3, ('3', '6'), {'test': ['a', 'b', 'c'], 'max': '3', 'min': '1'}) + (3, ('3',), {}) + (3, ('3', '6'), {}) + (3, ('3',), {}) + (3, (), {'test': 'a b c', 'min': '1'}) + (3, (), {'test': 'a, b, c', 'min': '5'}) + (3, (), {'test': 'a, b, c', 'max': '3', 'min': '1'}) + (3, (), {'test': '-99', 'min': '-100'}) + (3, (), {'max': '3', 'min': '1'}) + (3, ('3', '6'), {'test': '36'}) + (3, ('3', '6'), {'test': 'a, b, c'}) + (3, ('3',), {'test': ['a', 'b', 'c'], 'max': '3'}) + (3, ('3',), {'test': ["'a'", 'b', 'x=(c)'], 'max': '3'}) + (3, (), {'test': 'x=fish(3)'}) + + >>> v = Validator() + >>> v.check('integer(default=6)', '3') + 3 + >>> v.check('integer(default=6)', None, True) + 6 + >>> v.get_default_value('integer(default=6)') + 6 + >>> v.get_default_value('float(default=6)') + 6.0 + >>> v.get_default_value('pass(default=None)') + >>> v.get_default_value("string(default='None')") + 'None' + >>> v.get_default_value('pass') + Traceback (most recent call last): + KeyError: 'Check "pass" has no default value.' + >>> v.get_default_value('pass(default=list(1, 2, 3, 4))') + ['1', '2', '3', '4'] + + >>> v = Validator() + >>> v.check("pass(default=None)", None, True) + >>> v.check("pass(default='None')", None, True) + 'None' + >>> v.check('pass(default="None")', None, True) + 'None' + >>> v.check('pass(default=list(1, 2, 3, 4))', None, True) + ['1', '2', '3', '4'] + + Bug test for unicode arguments + >>> v = Validator() + >>> v.check(u'string(min=4)', u'test') + u'test' + + >>> v = Validator() + >>> v.get_default_value(u'string(min=4, default="1234")') + u'1234' + >>> v.check(u'string(min=4, default="1234")', u'test') + u'test' + + >>> v = Validator() + >>> default = v.get_default_value('string(default=None)') + >>> default == None + 1 + """ + return (value, args, keywargs) + + +def _test2(): + """ + >>> + >>> v = Validator() + >>> v.get_default_value('string(default="#ff00dd")') + '#ff00dd' + >>> v.get_default_value('integer(default=3) # comment') + 3 + """ + + +if __name__ == '__main__': + # run the code tests in doctest format + import doctest + m = sys.modules.get('__main__') + globs = m.__dict__.copy() + globs.update({ + 'INTP_VER': INTP_VER, + 'vtor': Validator(), + }) + doctest.testmod(m, globs=globs) |