diff --git a/CHANGES.md b/CHANGES.md index 24175c01..2ebc9b22 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -46,6 +46,7 @@ * Update xmltodict library 0.9.0 to 0.9.2 (579a005) * Update dateutil library 2.2 to 2.4.2 (a6b8925) * Change zoneinfo update/loader to be compatible with dateutil 2.4.2 +* Update ConfigObj library 4.6.0 to 5.1.0 (a68530a) [develop changelog] * Update Requests library 2.7.0 (ab1f493) to 2.7.0 (8b5e457) diff --git a/lib/configobj.py b/lib/configobj/__init__.py similarity index 83% rename from lib/configobj.py rename to lib/configobj/__init__.py index 3d343553..3bc85840 100644 --- a/lib/configobj.py +++ b/lib/configobj/__init__.py @@ -1,52 +1,30 @@ # configobj.py # A config file reader/writer that supports nested sections in config files. -# Copyright (C) 2005-2009 Michael Foord, Nicola Larosa -# E-mail: fuzzyman AT voidspace DOT org DOT uk -# nico AT tekNico DOT net +# Copyright (C) 2005-2014: +# (name) : (email) +# Michael Foord: fuzzyman AT voidspace DOT org DOT uk +# Nicola Larosa: nico AT tekNico DOT net +# Rob Dennis: rdennis AT gmail DOT com +# Eli Courtwright: eli AT courtwright DOT org -# ConfigObj 4 -# http://www.voidspace.org.uk/python/configobj.html +# This software is licensed under the terms of the BSD license. +# http://opensource.org/licenses/BSD-3-Clause -# Released subject to the BSD License -# Please see http://www.voidspace.org.uk/python/license.shtml +# ConfigObj 5 - main repository for documentation and issue tracking: +# https://github.com/DiffSK/configobj -# 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 import os import re +import sys +import collections +from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE + +import six +from ._version import __version__ + +# imported lazily to avoid startup performance hit if it isn't used compiler = None -try: - import compiler -except ImportError: - # for IronPython - pass - - -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 @@ -100,27 +78,10 @@ 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 - # Sentinel for use in getattr calls to replace hasattr MISSING = object() -__version__ = '4.6.0' - -__revision__ = '$Id: configobj.py 156 2006-01-31 14:57:08Z fuzzyman $' - -__docformat__ = "restructuredtext en" - __all__ = ( - '__version__', 'DEFAULT_INDENT_TYPE', 'DEFAULT_INTERPOLATION', 'ConfigObjError', @@ -137,8 +98,8 @@ __all__ = ( 'ReloadError', 'UnreprError', 'UnknownType', - '__docformat__', 'flatten_errors', + 'get_extra_values' ) DEFAULT_INTERPOLATION = 'configparser' @@ -161,12 +122,15 @@ OPTION_DEFAULTS = { 'write_empty_values': False, } +# this could be replaced if six is used for compatibility, or there are no +# more assertions about items being a string def getObj(s): - s = "a=" + s + global compiler if compiler is None: - raise ImportError('compiler module not available') + import compiler + s = "a=" + s p = compiler.parse(s) return p.getChildren()[1].getChildren()[0].getChildren()[1] @@ -176,29 +140,28 @@ class UnknownType(Exception): 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()) - + return list(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() + d[el] = next(i) return d - + def build_Tuple(self, o): return tuple(self.build_List(o)) - + def build_Name(self, o): if o.name == 'None': return None @@ -206,27 +169,27 @@ class Builder(object): 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()) + real, imag = list(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 - + 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]) - + return -self.build_Const(o.getChildren()[0]) + def build_UnaryAdd(self, o): return self.build_Const(o.getChildren()[0]) @@ -237,8 +200,10 @@ _builder = Builder() def unrepr(s): if not s: return s - return _builder.build(getObj(s)) + # this is supposed to be safe + import ast + return ast.literal_eval(s) class ConfigObjError(SyntaxError): @@ -309,11 +274,9 @@ class RepeatSectionError(ConfigObjError): class MissingInterpolationOption(InterpolationError): """A value specified for interpolation was missing.""" - def __init__(self, option): - InterpolationError.__init__( - self, - 'missing option "%s" in interpolation.' % option) + msg = 'missing option "%s" in interpolation.' % option + InterpolationError.__init__(self, msg) class UnreprError(ConfigObjError): @@ -331,6 +294,7 @@ class InterpolationEngine(object): # compiled regexp to use in self.interpolate() _KEYCRE = re.compile(r"%\(([^)]*)\)s") + _cookie = '%' def __init__(self, section): # the Section instance that "owns" this engine @@ -338,6 +302,10 @@ class InterpolationEngine(object): def interpolate(self, key, value): + # short-cut + if not self._cookie in value: + return value + def recursive_interpolate(key, value, section, backtrail): """The function that does the actual work. @@ -349,7 +317,7 @@ class InterpolationEngine(object): This is similar to a depth-first-search algorithm. """ # Have we been here already? - if backtrail.has_key((key, section.name)): + if (key, section.name) in backtrail: # Yes - infinite loop detected raise InterpolationLoopError(key) # Place a marker on our backtrail so we won't come back here again @@ -400,11 +368,11 @@ class InterpolationEngine(object): while True: # try the current section first val = current_section.get(key) - if val is not None: + if val is not None and not isinstance(val, Section): break # try "DEFAULT" next val = current_section.get('DEFAULT', {}).get(key) - if val is not None: + if val is not None and not isinstance(val, Section): break # move up to parent and try again # top-level's parent is itself @@ -437,11 +405,12 @@ class InterpolationEngine(object): (e.g., if we interpolated "$$" and returned "$"). """ raise NotImplementedError() - + class ConfigParserInterpolation(InterpolationEngine): """Behaves like ConfigParser.""" + _cookie = '%' _KEYCRE = re.compile(r"%\(([^)]*)\)s") def _parse_match(self, match): @@ -453,6 +422,7 @@ class ConfigParserInterpolation(InterpolationEngine): class TemplateInterpolation(InterpolationEngine): """Behaves like string.Template.""" + _cookie = '$' _delimiter = '$' _KEYCRE = re.compile(r""" \$(?: @@ -484,27 +454,27 @@ interpolation_engines = { def __newobj__(cls, *args): # Hack for pickle - return cls.__new__(cls, *args) + 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]) @@ -512,8 +482,8 @@ class Section(dict): 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 @@ -536,10 +506,10 @@ class Section(dict): 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(): + for entry, value in indict.items(): self[entry] = value - - + + def _initialise(self): # the sequence of scalar values in this Section self.scalars = [] @@ -553,6 +523,8 @@ class Section(dict): # for defaults self.defaults = [] self.default_values = {} + self.extra_values = [] + self._created = False def _interpolate(self, key, value): @@ -581,30 +553,39 @@ class Section(dict): def __getitem__(self, key): """Fetch the item and do string interpolation.""" val = dict.__getitem__(self, key) - if self.main.interpolation and isinstance(val, basestring): - return self._interpolate(key, val) + if self.main.interpolation: + if isinstance(val, six.string_types): + return self._interpolate(key, val) + if isinstance(val, list): + def _check(entry): + if isinstance(entry, six.string_types): + return self._interpolate(key, entry) + return entry + new = [_check(entry) for entry in val] + if new != val: + return new return val def __setitem__(self, key, value, unrepr=False): """ Correctly set a value. - + Making dictionary values Section instances. (We have to special case 'Section' instances - which are also dicts) - + Keys must be strings. Values need only be strings (or lists of strings) if ``main.stringify`` is set. - + ``unrepr`` must be set when setting a value to a dictionary, without creating a new sub-section. """ - if not isinstance(key, basestring): + if not isinstance(key, six.string_types): raise ValueError('The key "%s" is not a string.' % key) - + # add the comment - if not self.comments.has_key(key): + if key not in self.comments: self.comments[key] = [] self.inline_comments[key] = '' # remove the entry from defaults @@ -612,13 +593,13 @@ class Section(dict): self.defaults.remove(key) # if isinstance(value, Section): - if not self.has_key(key): + if key not in self: self.sections.append(key) dict.__setitem__(self, key, value) - elif isinstance(value, dict) and not unrepr: + elif isinstance(value, collections.Mapping) and not unrepr: # First create the new depth level, # then create the section - if not self.has_key(key): + if key not in self: self.sections.append(key) new_depth = self.depth + 1 dict.__setitem__( @@ -631,14 +612,14 @@ class Section(dict): indict=value, name=key)) else: - if not self.has_key(key): + if key not in self: self.scalars.append(key) if not self.main.stringify: - if isinstance(value, basestring): + if isinstance(value, six.string_types): pass elif isinstance(value, (list, tuple)): for entry in value: - if not isinstance(entry, basestring): + if not isinstance(entry, six.string_types): raise TypeError('Value is not a string "%s".' % entry) else: raise TypeError('Value is not a string "%s".' % value) @@ -672,22 +653,19 @@ class Section(dict): self[entry] = indict[entry] - def pop(self, key, *args): + def pop(self, key, default=MISSING): """ 'D.pop(k[,d]) -> v, remove specified key and return the corresponding value. If key is not found, d is returned if given, otherwise KeyError is raised' """ - 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, basestring): - return self._interpolate(key, val) + try: + val = self[key] + except KeyError: + if default is MISSING: + raise + val = default + else: + del self[key] return val @@ -697,7 +675,7 @@ class Section(dict): if not sequence: raise KeyError(": 'popitem(): dictionary is empty'") key = sequence[0] - val = self[key] + val = self[key] del self[key] return key, val @@ -706,7 +684,7 @@ class Section(dict): """ A version of clear that also affects scalars/sections Also clears comments and configspec. - + Leaves other attributes alone : depth/main/parent are not affected """ @@ -716,6 +694,8 @@ class Section(dict): self.comments = {} self.inline_comments = {} self.configspec = None + self.defaults = [] + self.extra_values = [] def setdefault(self, key, default=None): @@ -729,7 +709,7 @@ class Section(dict): def items(self): """D.items() -> list of D's (key, value) pairs, as 2-tuples""" - return zip((self.scalars + self.sections), self.values()) + return list(zip((self.scalars + self.sections), list(self.values()))) def keys(self): @@ -744,7 +724,7 @@ class Section(dict): def iteritems(self): """D.iteritems() -> an iterator over the (key, value) items of D""" - return iter(self.items()) + return iter(list(self.items())) def iterkeys(self): @@ -756,12 +736,17 @@ class Section(dict): def itervalues(self): """D.itervalues() -> an iterator over the values of D""" - return iter(self.values()) + return iter(list(self.values())) def __repr__(self): """x.__repr__() <==> repr(x)""" - return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(self[key]))) + def _getval(key): + try: + return self[key] + except MissingInterpolationOption: + return dict.__getitem__(self, key) + return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) for key in (self.scalars + self.sections)]) __str__ = __repr__ @@ -773,10 +758,10 @@ class Section(dict): 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 @@ -801,7 +786,7 @@ class Section(dict): def merge(self, indict): """ A recursive update - useful for merging config files. - + >>> a = '''[section1] ... option1 = True ... [[subsection]] @@ -817,21 +802,21 @@ class Section(dict): >>> c2 ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}}) """ - for key, val in indict.items(): - if (key in self and isinstance(self[key], dict) and - isinstance(val, dict)): + for key, val in list(indict.items()): + if (key in self and isinstance(self[key], collections.Mapping) and + isinstance(val, collections.Mapping)): self[key].merge(val) - else: + 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: @@ -859,30 +844,30 @@ class Section(dict): 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) @@ -945,17 +930,17 @@ class Section(dict): 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 + + 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 + + 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') @@ -975,7 +960,7 @@ class Section(dict): return False else: try: - if not isinstance(val, basestring): + if not isinstance(val, six.string_types): # TODO: Why do we raise a KeyError here? raise KeyError() else: @@ -987,10 +972,10 @@ class Section(dict): 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') @@ -1010,30 +995,30 @@ class Section(dict): 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') + >>> a.as_float('a') #doctest: +IGNORE_EXCEPTION_DETAIL 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 + >>> a.as_float('b') #doctest: +ELLIPSIS + 3.2... """ return float(self[key]) - - + + def as_list(self, key): """ A convenience method which fetches the specified value, guaranteeing that it is a list. - + >>> a = ConfigObj() >>> a['a'] = 1 >>> a.as_list('a') @@ -1049,15 +1034,15 @@ class Section(dict): if isinstance(result, (tuple, list)): return list(result) return [result] - + def restore_default(self, key): """ Restore (and return) default value for the specified key. - + This method will only work for a ConfigObj that was created with a configspec and has been validated. - + If there is no default value for this key, ``KeyError`` is raised. """ default = self.default_values[key] @@ -1066,20 +1051,20 @@ class Section(dict): 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() @@ -1148,7 +1133,7 @@ class ConfigObj(Section): ( (?:".*?")| # double quotes (?:'.*?')| # single quotes - (?:[^'",\#].*?) # unquoted + (?:[^'",\#]?.*?) # unquoted ) \s*,\s* # comma ''', @@ -1187,45 +1172,70 @@ class ConfigObj(Section): } - def __init__(self, infile=None, options=None, _inspec=False, **kwargs): + def __init__(self, infile=None, options=None, configspec=None, encoding=None, + interpolation=True, raise_errors=False, list_values=True, + create_empty=False, file_error=False, stringify=True, + indent_type=None, default_encoding=None, unrepr=False, + write_empty_values=False, _inspec=False): """ Parse a config file or create a config file object. - - ``ConfigObj(infile=None, options=None, **kwargs)`` + + ``ConfigObj(infile=None, configspec=None, encoding=None, + interpolation=True, raise_errors=False, list_values=True, + create_empty=False, file_error=False, stringify=True, + indent_type=None, default_encoding=None, unrepr=False, + write_empty_values=False, _inspec=False)`` """ self._inspec = _inspec # init the superclass Section.__init__(self, self, 0, self) - + infile = infile or [] - options = dict(options or {}) - - # keyword arguments take precedence over an options dictionary - options.update(kwargs) + + _options = {'configspec': configspec, + 'encoding': encoding, 'interpolation': interpolation, + 'raise_errors': raise_errors, 'list_values': list_values, + 'create_empty': create_empty, 'file_error': file_error, + 'stringify': stringify, 'indent_type': indent_type, + 'default_encoding': default_encoding, 'unrepr': unrepr, + 'write_empty_values': write_empty_values} + + if options is None: + options = _options + else: + import warnings + warnings.warn('Passing in an options dictionary to ConfigObj() is ' + 'deprecated. Use **options instead.', + DeprecationWarning) + + # TODO: check the values too. + for entry in options: + if entry not in OPTION_DEFAULTS: + raise TypeError('Unrecognised option "%s".' % entry) + for entry, value in list(OPTION_DEFAULTS.items()): + if entry not in options: + options[entry] = value + keyword_value = _options[entry] + if value != keyword_value: + options[entry] = keyword_value + + # XXXX this ignores an explicit list_values = True in combination + # with _inspec. The user should *never* do that anyway, but still... if _inspec: options['list_values'] = False - - 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._initialise(options) + configspec = options['configspec'] self._original_configspec = configspec self._load(infile, configspec) - - + + def _load(self, infile, configspec): - if isinstance(infile, basestring): + if isinstance(infile, six.string_types): self.filename = infile if os.path.isfile(infile): - h = open(infile, 'rb') - infile = h.read() or [] - h.close() + with open(infile, 'rb') as h: + content = h.readlines() or [] elif self.file_error: # raise an error if the file doesn't exist raise IOError('Config file not found: "%s".' % self.filename) @@ -1234,48 +1244,54 @@ class ConfigObj(Section): 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 = [] - + with open(infile, 'w') as h: + h.write('') + content = [] + elif isinstance(infile, (list, tuple)): - infile = list(infile) - + content = 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] + def set_section(in_section, this_section): + for entry in in_section.scalars: + this_section[entry] = in_section[entry] + for section in in_section.sections: + this_section[section] = {} + set_section(in_section[section], this_section[section]) + set_section(infile, self) + + else: + for entry in infile: + self[entry] = infile[entry] del self._errors - + if configspec is not None: self._handle_configspec(configspec) else: self.configspec = None return - + elif getattr(infile, 'read', MISSING) is not MISSING: # This supports file like objects - infile = infile.read() or [] + content = 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: + + if content: # don't do it for the empty ConfigObj - infile = self._handle_bom(infile) + content = self._handle_bom(content) # 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')): + for line in content: + if (not line) or (line[-1] not in ('\r', '\n')): continue for end in ('\r\n', '\n', '\r'): if line.endswith(end): @@ -1283,9 +1299,10 @@ class ConfigObj(Section): break break - infile = [line.rstrip('\r\n') for line in infile] - - self._parse(infile) + assert all(isinstance(line, six.string_types) for line in content), repr(content) + content = [line.rstrip('\r\n') for line in content] + + self._parse(content) # if we had any errors, now is the time to raise them if self._errors: info = "at line %s." % self._errors[0].line_number @@ -1302,17 +1319,17 @@ class ConfigObj(Section): 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 = [] @@ -1329,57 +1346,70 @@ class ConfigObj(Section): 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 _getval(key): + try: + return self[key] + except MissingInterpolationOption: + return dict.__getitem__(self, key) + return ('%s({%s})' % (self.__class__.__name__, + ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) + for key in (self.scalars + self.sections)]))) + + def _handle_bom(self, infile): """ Handle any BOM, and decode if necessary. - + If an encoding is specified, that *must* be used - but the BOM should still be removed (and the BOM attribute set). - + (If the encoding is wrongly specified, then a BOM for an alternative encoding won't be discovered or removed.) - + If an encoding is not specified, UTF8 or UTF16 BOM will be detected and removed. The BOM attribute will be set. UTF16 will be decoded to unicode. - + NOTE: This method must not be called with an empty ``infile``. - + Specifying the *wrong* encoding is likely to cause a ``UnicodeDecodeError``. - + ``infile`` must always be returned as a list of lines, but may be passed in as a single string. """ + if ((self.encoding is not None) and (self.encoding.lower() not in BOM_LIST)): # No need to check for a BOM # the encoding specified doesn't have one # just decode return self._decode(infile, self.encoding) - + if isinstance(infile, (list, tuple)): line = infile[0] else: line = infile + + if isinstance(line, six.text_type): + # it's already decoded and there's no need to do anything + # else, just use the _decode utility method to handle + # listifying appropriately + return self._decode(infile, self.encoding) + if self.encoding is not None: # encoding explicitly supplied # And it could have an associated BOM @@ -1388,7 +1418,7 @@ class ConfigObj(Section): 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(): + for BOM, (encoding, final_encoding) in list(BOMS.items()): if not final_encoding: # skip UTF8 continue @@ -1397,18 +1427,18 @@ class ConfigObj(Section): ##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 @@ -1416,10 +1446,11 @@ class ConfigObj(Section): 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): + for BOM, (encoding, final_encoding) in list(BOMS.items()): + if not isinstance(line, six.binary_type) or not line.startswith(BOM): + # didn't specify a BOM, or it's not a bytestring continue else: # BOM discovered @@ -1433,25 +1464,32 @@ class ConfigObj(Section): infile[0] = newline else: infile = newline - # UTF8 - don't decode - if isinstance(infile, basestring): + # UTF-8 + if isinstance(infile, six.text_type): return infile.splitlines(True) + elif isinstance(infile, six.binary_type): + return infile.decode('utf-8').splitlines(True) else: - return infile + return self._decode(infile, 'utf-8') # UTF16 - have to decode return self._decode(infile, encoding) - - # No BOM discovered and no encoding specified, just return - if isinstance(infile, basestring): - # infile read from a file will be a single string - return infile.splitlines(True) - return infile + + + if six.PY2 and isinstance(line, str): + # don't actually do any decoding, since we're on python 2 and + # returning a bytestring is fine + return self._decode(infile, None) + # No BOM discovered and no encoding specified, default to UTF-8 + if isinstance(infile, six.binary_type): + return infile.decode('utf-8').splitlines(True) + else: + return self._decode(infile, 'utf-8') def _a_to_u(self, aString): """Decode ASCII strings to unicode if a self.encoding is specified.""" - if self.encoding: - return aString.decode('ascii') + if isinstance(aString, six.binary_type) and self.encoding: + return aString.decode(self.encoding) else: return aString @@ -1459,37 +1497,45 @@ class ConfigObj(Section): def _decode(self, infile, encoding): """ Decode infile to unicode. Using the specified encoding. - + if is a string, it also needs converting to a list. """ - if isinstance(infile, basestring): - # can't be unicode + if isinstance(infile, six.string_types): + return infile.splitlines(True) + if isinstance(infile, six.binary_type): # 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) + if encoding: + return infile.decode(encoding).splitlines(True) + else: + return infile.splitlines(True) + + if encoding: + for i, line in enumerate(infile): + if isinstance(line, six.binary_type): + # 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: + if isinstance(line, six.binary_type) and self.default_encoding: return line.decode(self.default_encoding) - return line + else: + return line + # TODO: this may need to be modified def _str(self, value): """ Used by ``stringify`` within validate, to turn non-string values into strings. """ - if not isinstance(value, basestring): + if not isinstance(value, six.string_types): + # intentially 'str' because it's just whatever the "normal" + # string type is for the python version we're dealing with return str(value) else: return value @@ -1500,14 +1546,14 @@ class ConfigObj(Section): 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 = [] @@ -1519,13 +1565,13 @@ class ConfigObj(Section): 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) @@ -1536,17 +1582,17 @@ class ConfigObj(Section): 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.", + self._handle_error("Cannot compute the section depth", 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.", + self._handle_error("Cannot compute nesting level", NestingError, infile, cur_index) continue elif cur_depth == this_section.depth: @@ -1556,15 +1602,16 @@ class ConfigObj(Section): # the new section is a child the current section parent = this_section else: - self._handle_error("Section too nested at line %s.", + self._handle_error("Section too nested", NestingError, infile, cur_index) - + continue + sect_name = self._unquote(sect_name) - if parent.has_key(sect_name): - self._handle_error('Duplicate section name at line %s.', + if sect_name in parent: + self._handle_error('Duplicate section name', DuplicateError, infile, cur_index) continue - + # create the new section this_section = Section( parent, @@ -1580,10 +1627,8 @@ class ConfigObj(Section): # 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".', + 'Invalid line ({0!r}) (matched as neither section nor keyword)'.format(line), ParseError, infile, cur_index) else: # is a keyword value @@ -1594,11 +1639,11 @@ class ConfigObj(Section): # check for a multiline value if value[:3] in ['"""', "'''"]: try: - (value, comment, cur_index) = self._multiline( + value, comment, cur_index = self._multiline( value, infile, cur_index, maxline) except SyntaxError: self._handle_error( - 'Parse error in value at line %s.', + 'Parse error in multiline value', ParseError, infile, cur_index) continue else: @@ -1606,11 +1651,11 @@ class ConfigObj(Section): comment = '' try: value = unrepr(value) - except Exception, e: + except Exception as e: if type(e) == UnknownType: - msg = 'Unknown name or type in value at line %s.' + msg = 'Unknown name or type in value' else: - msg = 'Parse error in value at line %s.' + msg = 'Parse error from unrepr-ing multiline value' self._handle_error(msg, UnreprError, infile, cur_index) continue @@ -1619,11 +1664,11 @@ class ConfigObj(Section): comment = '' try: value = unrepr(value) - except Exception, e: + except Exception as e: if isinstance(e, UnknownType): - msg = 'Unknown name or type in value at line %s.' + msg = 'Unknown name or type in value' else: - msg = 'Parse error in value at line %s.' + msg = 'Parse error from unrepr-ing value' self._handle_error(msg, UnreprError, infile, cur_index) continue @@ -1633,14 +1678,14 @@ class ConfigObj(Section): (value, comment) = self._handle_value(value) except SyntaxError: self._handle_error( - 'Parse error in value at line %s.', + 'Parse error in value', ParseError, infile, cur_index) continue # key = self._unquote(key) - if this_section.has_key(key): + if key in this_section: self._handle_error( - 'Duplicate keyword name at line %s.', + 'Duplicate keyword name', DuplicateError, infile, cur_index) continue # add the key. @@ -1667,7 +1712,7 @@ class ConfigObj(Section): """ 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. """ @@ -1685,13 +1730,13 @@ class ConfigObj(Section): 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 + message = '{0} at line {1}.'.format(text, cur_index) error = ErrorClass(message, cur_index, line) if self.raise_errors: # raise the error - parsing stops here @@ -1703,6 +1748,9 @@ class ConfigObj(Section): def _unquote(self, value): """Return an unquoted version of a value""" + if not value: + # should only happen during parsing of lists + raise SyntaxError if (value[0] == value[-1]) and (value[0] in ('"', "'")): value = value[1:-1] return value @@ -1711,19 +1759,19 @@ class ConfigObj(Section): 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. """ @@ -1731,7 +1779,7 @@ class ConfigObj(Section): # 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 ',' @@ -1739,20 +1787,22 @@ class ConfigObj(Section): return self._quote(value[0], multiline=False) + ',' return ', '.join([self._quote(val, multiline=False) for val in value]) - if not isinstance(value, basestring): + if not isinstance(value, six.string_types): if self.stringify: + # intentially 'str' because it's just whatever the "normal" + # string type is for the python version we're dealing with 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)) + 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`` @@ -1770,13 +1820,13 @@ class ConfigObj(Section): 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) @@ -1785,15 +1835,15 @@ class ConfigObj(Section): 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 + quot = tsquot return quot @@ -1883,7 +1933,7 @@ class ConfigObj(Section): def _handle_configspec(self, configspec): """Parse the configspec.""" - # FIXME: Should we check that the configspec was created with the + # FIXME: Should we check that the configspec was created with the # correct settings ? (i.e. ``list_values=False``) if not isinstance(configspec, ConfigObj): try: @@ -1891,17 +1941,17 @@ class ConfigObj(Section): raise_errors=True, file_error=True, _inspec=True) - except ConfigObjError, e: + except ConfigObjError as e: # FIXME: Should these errors have a reference # to the already parsed ConfigObj ? raise ConfigspecError('Parsing configspec failed: %s' % e) - except IOError, e: + except IOError as e: raise IOError('Reading configspec failed: %s' % e) - - self.configspec = configspec - - + self.configspec = configspec + + + def _set_configspec(self, section, copy): """ Called by validate. Handles setting the configspec on subsections @@ -1913,21 +1963,22 @@ class ConfigObj(Section): for entry in section.sections: if entry not in configspec: section[entry].configspec = many - + for entry in configspec.sections: if entry == '__many__': continue if entry not in section: section[entry] = {} + section[entry]._created = True if copy: # copy comments section.comments[entry] = configspec.comments.get(entry, []) section.inline_comments[entry] = configspec.inline_comments.get(entry, '') - + # Could be a scalar when we expect a section if isinstance(section[entry], Section): section[entry].configspec = configspec[entry] - + def _write_line(self, indent_string, entry, this_entry, comment): """Write an individual line, for the write method""" @@ -1967,20 +2018,22 @@ class ConfigObj(Section): def write(self, outfile=None, section=None): """ Write the current ConfigObj as a file - + tekNico: FIXME: use StringIO instead of real files - + >>> filename = a.filename >>> a.filename = 'test.ini' >>> a.write() >>> a.filename = filename >>> a == ConfigObj('test.ini', raise_errors=True) 1 + >>> import os + >>> os.remove('test.ini') """ if self.indent_type is None: # this can be true if initialised from a dictionary self.indent_type = DEFAULT_INDENT_TYPE - + out = [] cs = self._a_to_u('#') csp = self._a_to_u('# ') @@ -1994,7 +2047,7 @@ class ConfigObj(Section): 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: @@ -2007,8 +2060,8 @@ class ConfigObj(Section): out.append(indent_string + comment_line) this_entry = section[entry] comment = self._handle_comment(section.inline_comments[entry]) - - if isinstance(this_entry, dict): + + if isinstance(this_entry, Section): # a section out.append(self._write_marker( indent_string, @@ -2022,7 +2075,7 @@ class ConfigObj(Section): entry, this_entry, comment)) - + if section is self: for line in self.final_comment: line = self._decode_element(line) @@ -2031,10 +2084,10 @@ class ConfigObj(Section): 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 @@ -2048,58 +2101,66 @@ class ConfigObj(Section): out.append('') out[0] = BOM_UTF8 + out[0] return out - + # Turn the list to a string, joined with correct newlines newline = self.newlines or os.linesep + if (getattr(outfile, 'mode', None) is not None and outfile.mode == 'w' + and sys.platform == 'win32' and newline == '\r\n'): + # Windows specific hack to avoid writing '\r\r\n' + newline = '\n' output = self._a_to_u(newline).join(out) - if self.encoding: - output = output.encode(self.encoding) - if self.BOM and ((self.encoding is None) or match_utf8(self.encoding)): - # Add the UTF8 BOM - output = BOM_UTF8 + output - if not output.endswith(newline): output += newline - if outfile is not None: - outfile.write(output) - else: - h = open(self.filename, 'wb') - h.write(output) - h.close() + if isinstance(output, six.binary_type): + output_bytes = output + else: + output_bytes = output.encode(self.encoding or + self.default_encoding or + 'ascii') + + if self.BOM and ((self.encoding is None) or match_utf8(self.encoding)): + # Add the UTF8 BOM + output_bytes = BOM_UTF8 + output_bytes + + if outfile is not None: + outfile.write(output_bytes) + else: + with open(self.filename, 'wb') as h: + h.write(output_bytes) 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. @@ -2112,7 +2173,7 @@ class ConfigObj(Section): # Which makes importing configobj faster from validate import VdtMissingValue self._vdtMissingValue = VdtMissingValue - + section = self if copy: @@ -2122,18 +2183,29 @@ class ConfigObj(Section): section.BOM = section.configspec.BOM section.newlines = section.configspec.newlines section.indent_type = section.configspec.indent_type - + # + # section.default_values.clear() #?? configspec = section.configspec self._set_configspec(section, copy) - + + def validate_entry(entry, spec, val, missing, ret_true, ret_false): + section.default_values.pop(entry, None) + + try: + section.default_values[entry] = validator.get_default_value(configspec[entry]) + except (KeyError, AttributeError, validator.baseErrorClass): + # No default, bad default or validator has no 'get_default_value' + # (e.g. SimpleVal) + pass + try: check = validator.check(spec, val, missing=missing ) - except validator.baseErrorClass, e: + except validator.baseErrorClass as e: if not preserve_errors or isinstance(e, self._vdtMissingValue): out[entry] = False else: @@ -2142,21 +2214,6 @@ class ConfigObj(Section): 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: @@ -2176,27 +2233,26 @@ class ConfigObj(Section): 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_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: + if copy and entry not in section.scalars: # copy comments section.comments[entry] = ( configspec.comments.get(entry, [])) @@ -2206,21 +2262,22 @@ class ConfigObj(Section): else: missing = False val = section[entry] - + ret_true, ret_false = validate_entry(entry, configspec[entry], val, missing, ret_true, ret_false) - + many = None if '__many__' in configspec.scalars: many = configspec['__many__'] elif '___many___' in configspec.scalars: many = configspec['___many___'] - + if many is not None: for entry in unvalidated: val = section[entry] ret_true, ret_false = validate_entry(entry, many, val, False, ret_true, ret_false) + unvalidated = [] for entry in incorrect_scalars: ret_true = False @@ -2238,7 +2295,7 @@ class ConfigObj(Section): 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: @@ -2246,6 +2303,7 @@ class ConfigObj(Section): if section is self and entry == 'DEFAULT': continue if section[entry].configspec is None: + unvalidated.append(entry) continue if copy: section.comments[entry] = configspec.comments.get(entry, []) @@ -2258,8 +2316,19 @@ class ConfigObj(Section): ret_false = False else: ret_true = False - ret_false = False + + section.extra_values = unvalidated + if preserve_errors and not section._created: + # If the section wasn't created (i.e. it wasn't missing) + # then we can't return False, we need to preserve errors + ret_false = False # + if ret_false and preserve_errors and out: + # If we are preserving errors, but all + # the failures are from missing sections / values + # then we can return False. Otherwise there is a + # real failure that we need to preserve. + ret_false = not any(out.values()) if ret_true: return True elif ret_false: @@ -2276,16 +2345,16 @@ class ConfigObj(Section): self.configspec = None # Just to be sure ;-) self._original_configspec = None - - + + def reload(self): """ Reload a ConfigObj from file. - + This method raises a ``ReloadError`` if the ConfigObj doesn't have a filename attribute pointing to a file. """ - if not isinstance(self.filename, basestring): + if not isinstance(self.filename, six.string_types): raise ReloadError() filename = self.filename @@ -2294,31 +2363,31 @@ class ConfigObj(Section): 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: @@ -2326,119 +2395,53 @@ class SimpleVal(object): 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 : - - :: - + + 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): + if res == True: + return sorted(results) + if res == False or isinstance(res, Exception): results.append((levels[:], None, res)) if levels: levels.pop() - return results - for (key, val) in res.items(): + return sorted(results) + for (key, val) in list(res.items()): if val == True: continue - if isinstance(cfg.get(key), dict): + if isinstance(cfg.get(key), collections.Mapping): # Go down one level levels.append(key) flatten_errors(cfg[key], val, levels, results) @@ -2449,7 +2452,33 @@ def flatten_errors(cfg, res, levels=None, results=None): if levels: levels.pop() # - return results + return sorted(results) + + +def get_extra_values(conf, _prepend=()): + """ + Find all the values and sections not in the configspec from a validated + ConfigObj. + + ``get_extra_values`` returns a list of tuples where each tuple represents + either an extra section, or an extra value. + + The tuples contain two values, a tuple representing the section the value + is in and the name of the extra values. For extra values in the top level + section the first member will be an empty tuple. For values in the 'foo' + section the first member will be ``('foo',)``. For members in the 'bar' + subsection of the 'foo' section the first member will be ``('foo', 'bar')``. + + NOTE: If you call ``get_extra_values`` on a ConfigObj instance that hasn't + been validated it will return an empty list. + """ + out = [] + + out.extend([(_prepend, name) for name in conf.extra_values]) + for name in conf.sections: + if name not in conf.extra_values: + out.extend(get_extra_values(conf[name], _prepend + (name,))) + return out """*A programming language is a medium of expression.* - Paul Graham""" diff --git a/lib/configobj/_version.py b/lib/configobj/_version.py new file mode 100644 index 00000000..a5e45131 --- /dev/null +++ b/lib/configobj/_version.py @@ -0,0 +1 @@ +__version__ = '5.1.0' diff --git a/lib/configobj/validate.py b/lib/configobj/validate.py new file mode 100644 index 00000000..9da877e4 --- /dev/null +++ b/lib/configobj/validate.py @@ -0,0 +1,1471 @@ +# validate.py +# A Validator object +# Copyright (C) 2005-2014: +# (name) : (email) +# Michael Foord: fuzzyman AT voidspace DOT org DOT uk +# Mark Andrews: mark AT la-la DOT com +# Nicola Larosa: nico AT tekNico DOT net +# Rob Dennis: rdennis AT gmail DOT com +# Eli Courtwright: eli AT courtwright DOT org + +# This software is licensed under the terms of the BSD license. +# http://opensource.org/licenses/BSD-3-Clause + +# ConfigObj 5 - main repository for documentation and issue tracking: +# https://github.com/DiffSK/configobj + +""" + 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``. +""" + +__version__ = '1.0.1' + + +__all__ = ( + '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__', -- where is this supposed to come from? [jhe 2015-04-11] +) + + +import re +import sys +from pprint import pprint + +#TODO - #21 - six is part of the repo now, but we didn't switch over to it here +# this could be replaced if six is used for compatibility, or there are no +# more assertions about items being a string +if sys.version_info < (3,): + string_type = basestring +else: + string_type = str + # so tests that care about unicode on 2.x can specify unicode, and the same + # tests when run on 3.x won't complain about a undefined name "unicode" + # since all strings are unicode on 3.x we just want to pass it through + # unchanged + unicode = lambda x: x + # in python 3, all ints are equivalent to python 2 longs, and they'll + # never show "L" in the repr + long = int + +_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 | re.DOTALL) # two groups + +_list_members = re.compile(r''' + ( + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\s=][^,=]*?) # unquoted + ) + (?: + (?:\s*,\s*)|(?:\s*$) # comma + ) +''', re.VERBOSE | re.DOTALL) # 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('255.255.255.255') + 4294967295 + >>> 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: + raise ValueError('Not a good dotted-quad IP: %s' % ip) + return + + +def numToDottedQuad(num): + """ + Convert int or long int to dotted quad string + + >>> numToDottedQuad(long(-1)) + Traceback (most recent call last): + ValueError: Not a good numeric IP: -1 + >>> numToDottedQuad(long(1)) + '0.0.0.1' + >>> numToDottedQuad(long(16777218)) + '1.0.0.2' + >>> numToDottedQuad(long(16908291)) + '1.2.0.3' + >>> numToDottedQuad(long(16909060)) + '1.2.3.4' + >>> numToDottedQuad(long(4294967295)) + '255.255.255.255' + >>> numToDottedQuad(long(4294967296)) + Traceback (most recent call last): + ValueError: Not a good numeric IP: 4294967296 + >>> numToDottedQuad(-1) + Traceback (most recent call last): + ValueError: Not a good numeric IP: -1 + >>> numToDottedQuad(1) + '0.0.0.1' + >>> numToDottedQuad(16777218) + '1.0.0.2' + >>> numToDottedQuad(16908291) + '1.2.0.3' + >>> numToDottedQuad(16909060) + '1.2.3.4' + >>> numToDottedQuad(4294967295) + '255.255.255.255' + >>> numToDottedQuad(4294967296) + 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 > long(4294967295) 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, string_type)): + ... raise VdtTypeError(value) + ... elif isinstance(value, string_type): + ... # 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: + https://configobj.readthedocs.org/en/latest/configobj.html + """ + + # this regex does the initial parsing of the checks + _func_re = re.compile(r'(.+?)\((.*)\)', re.DOTALL) + + # this regex takes apart keyword arguments + _key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$', re.DOTALL) + + + # 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 | re.DOTALL) + _matchfinder = re.compile(_matchstring, re.VERBOSE | re.DOTALL) + + + 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, + 'force_list': force_list, + } + 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': + return 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 list(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 + default = fun_kwargs.pop('default', None) + 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, string_type)): + try: + out_params.append(fun(val)) + except ValueError as 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, string_type)): + raise VdtTypeError(value) + if isinstance(value, string_type): + # 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, string_type)): + 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, string_type): + 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, string_type): + 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, string_type): + 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, string_type): + 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, string_type): + 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)] + + +def force_list(value, min=None, max=None): + """ + Check that a value is a list, coercing strings into + a list with one member. Useful where users forget the + trailing comma that turns a single value into a list. + + You can optionally specify the minimum and maximum number of members. + A minumum of greater than one will fail if the user only supplies a + string. + + >>> vtor.check('force_list', ()) + [] + >>> vtor.check('force_list', []) + [] + >>> vtor.check('force_list', 'hello') + ['hello'] + """ + if not isinstance(value, (list, tuple)): + value = [value] + return 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. + + >>> vtor.check('mixed_list("yoda")', ('a')) + Traceback (most recent call last): + VdtParamError: passed an incorrect value "KeyError('yoda',)" for parameter "'mixed_list'" + """ + 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 as 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, string_type): + 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: + ... pprint(v.check(('test(%s)' % entry), 3)) + (3, ('3', '6'), {'max': '3', 'min': '1', 'test': ['a', 'b', 'c']}) + (3, ('3',), {}) + (3, ('3', '6'), {}) + (3, ('3',), {}) + (3, (), {'min': '1', 'test': 'a b c'}) + (3, (), {'min': '5', 'test': 'a, b, c'}) + (3, (), {'max': '3', 'min': '1', 'test': 'a, b, c'}) + (3, (), {'min': '-100', 'test': '-99'}) + (3, (), {'max': '3', 'min': '1'}) + (3, ('3', '6'), {'test': '36'}) + (3, ('3', '6'), {'test': 'a, b, c'}) + (3, ('3',), {'max': '3', 'test': ['a', 'b', 'c']}) + (3, ('3',), {'max': '3', 'test': ["'a'", 'b', 'x=(c)']}) + (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(unicode('string(min=4)'), unicode('test')) == unicode('test') + True + + >>> v = Validator() + >>> v.get_default_value(unicode('string(min=4, default="1234")')) == unicode('1234') + True + >>> v.check(unicode('string(min=4, default="1234")'), unicode('test')) == unicode('test') + True + + >>> 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 + """ + +def _test3(): + r""" + >>> vtor.check('string(default="")', '', missing=True) + '' + >>> vtor.check('string(default="\n")', '', missing=True) + '\n' + >>> print(vtor.check('string(default="\n")', '', missing=True)) + + + >>> vtor.check('string()', '\n') + '\n' + >>> vtor.check('string(default="\n\n\n")', '', missing=True) + '\n\n\n' + >>> vtor.check('string()', 'random \n text goes here\n\n') + 'random \n text goes here\n\n' + >>> vtor.check('string(default=" \nrandom text\ngoes \n here\n\n ")', + ... '', missing=True) + ' \nrandom text\ngoes \n here\n\n ' + >>> vtor.check("string(default='\n\n\n')", '', missing=True) + '\n\n\n' + >>> vtor.check("option('\n','a','b',default='\n')", '', missing=True) + '\n' + >>> vtor.check("string_list()", ['foo', '\n', 'bar']) + ['foo', '\n', 'bar'] + >>> vtor.check("string_list(default=list('\n'))", '', missing=True) + ['\n'] + """ + + +if __name__ == '__main__': + # run the code tests in doctest format + import sys + import doctest + m = sys.modules.get('__main__') + globs = m.__dict__.copy() + globs.update({ + 'vtor': Validator(), + }) + + failures, tests = doctest.testmod( + m, globs=globs, + optionflags=doctest.IGNORE_EXCEPTION_DETAIL | doctest.ELLIPSIS) + assert not failures, '{} failures out of {} tests'.format(failures, tests)