diff --git a/lib/configobj.py b/lib/configobj/__init__.py similarity index 87% rename from lib/configobj.py rename to lib/configobj/__init__.py index eae556a7..928c2083 100644 --- a/lib/configobj.py +++ b/lib/configobj/__init__.py @@ -1,29 +1,38 @@ # configobj.py -# A config file reader/writer that supports nested sections in config files. -# Copyright (C) 2005-2010 Michael Foord, Nicola Larosa -# E-mail: fuzzyman AT voidspace DOT org DOT uk -# nico AT tekNico DOT net +# -*- coding: utf-8 -*- +# pylint: disable=bad-continuation -# ConfigObj 4 -# http://www.voidspace.org.uk/python/configobj.html +"""A config file reader/writer that supports nested sections in config files.""" -# Released subject to the BSD License -# Please see http://www.voidspace.org.uk/python/license.shtml +# 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 -# 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. +# This software is licensed under the terms of the BSD license. +# http://opensource.org/licenses/BSD-3-Clause -from __future__ import generators +# ConfigObj 5 - main repository for documentation and issue tracking: +# https://github.com/DiffSK/configobj import os import re import sys +import copy from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE +try: + # Python 3 + from collections.abc import Mapping +except ImportError: + # Python 2.7 + from collections import Mapping + +import six +from ._version import __version__ # imported lazily to avoid startup performance hit if it isn't used compiler = None @@ -83,20 +92,7 @@ tdquot = "'''%s'''" # Sentinel for use in getattr calls to replace hasattr MISSING = object() -__version__ = '4.7.2' - -try: - any -except NameError: - def any(iterable): - for entry in iterable: - if entry: - return True - return False - - __all__ = ( - '__version__', 'DEFAULT_INDENT_TYPE', 'DEFAULT_INTERPOLATION', 'ConfigObjError', @@ -137,6 +133,8 @@ 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): @@ -152,70 +150,13 @@ class UnknownType(Exception): pass -class Builder(object): - - def build(self, o): - m = getattr(self, 'build_' + o.__class__.__name__, None) - if m is None: - raise UnknownType(o.__class__.__name__) - return m(o) - - def build_List(self, o): - return map(self.build, o.getChildren()) - - def build_Const(self, o): - return o.value - - def build_Dict(self, o): - d = {} - i = iter(map(self.build, o.getChildren())) - for el in i: - d[el] = i.next() - return d - - def build_Tuple(self, o): - return tuple(self.build_List(o)) - - def build_Name(self, o): - if o.name == 'None': - return None - if o.name == 'True': - return True - if o.name == 'False': - return False - - # An undefined Name - raise UnknownType('Undefined Name') - - def build_Add(self, o): - real, imag = map(self.build_Const, o.getChildren()) - try: - real = float(real) - except TypeError: - raise UnknownType('Add') - if not isinstance(imag, complex) or imag.real != 0.0: - raise UnknownType('Add') - return real+imag - - def build_Getattr(self, o): - parent = self.build(o.expr) - return getattr(parent, o.attrname) - - def build_UnarySub(self, o): - return -self.build_Const(o.getChildren()[0]) - - def build_UnaryAdd(self, o): - return self.build_Const(o.getChildren()[0]) - - -_builder = Builder() - - def unrepr(s): if not s: return s - return _builder.build(getObj(s)) + # this is supposed to be safe + import ast + return ast.literal_eval(s) class ConfigObjError(SyntaxError): @@ -518,7 +459,7 @@ 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 @@ -566,11 +507,11 @@ class Section(dict): """Fetch the item and do string interpolation.""" val = dict.__getitem__(self, key) if self.main.interpolation: - if isinstance(val, basestring): + if isinstance(val, six.string_types): return self._interpolate(key, val) if isinstance(val, list): def _check(entry): - if isinstance(entry, basestring): + if isinstance(entry, six.string_types): return self._interpolate(key, entry) return entry new = [_check(entry) for entry in val] @@ -593,7 +534,7 @@ class Section(dict): ``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 @@ -608,7 +549,7 @@ class Section(dict): if key not in self: self.sections.append(key) dict.__setitem__(self, key, value) - elif isinstance(value, dict) and not unrepr: + elif isinstance(value, Mapping) and not unrepr: # First create the new depth level, # then create the section if key not in self: @@ -627,11 +568,11 @@ class Section(dict): 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) @@ -721,17 +662,17 @@ 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 [(key, self[key]) for key in self.keys()] def keys(self): """D.keys() -> list of D's keys""" - return (self.scalars + self.sections) + return self.scalars + self.sections def values(self): """D.values() -> list of D's values""" - return [self[key] for key in (self.scalars + self.sections)] + return [self[key] for key in self.keys()] def iteritems(self): @@ -741,7 +682,7 @@ class Section(dict): def iterkeys(self): """D.iterkeys() -> an iterator over the keys of D""" - return iter((self.scalars + self.sections)) + return iter(self.keys()) __iter__ = iterkeys @@ -758,7 +699,7 @@ class Section(dict): return self[key] except MissingInterpolationOption: return dict.__getitem__(self, key) - return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) + return '{%s}' % ', '.join([('{}: {}'.format(repr(key), repr(_getval(key)))) for key in (self.scalars + self.sections)]) __str__ = __repr__ @@ -795,10 +736,15 @@ class Section(dict): return newdict - def merge(self, indict): + def merge(self, indict, decoupled=False): """ A recursive update - useful for merging config files. + Note: if ``decoupled`` is ``True``, then the target object (self) + gets its own copy of any mutable objects in the source dictionary + (both sections and values), paid for by more work for ``merge()`` + and more memory usage. + >>> a = '''[section1] ... option1 = True ... [[subsection]] @@ -815,9 +761,11 @@ class Section(dict): ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}}) """ for key, val in indict.items(): - if (key in self and isinstance(self[key], dict) and - isinstance(val, dict)): - self[key].merge(val) + if decoupled: + val = copy.deepcopy(val) + if (key in self and isinstance(self[key], Mapping) and + isinstance(val, Mapping)): + self[key].merge(val, decoupled=decoupled) else: self[key] = val @@ -972,7 +920,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: @@ -1013,15 +961,15 @@ class Section(dict): >>> 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]) @@ -1081,9 +1029,23 @@ class Section(dict): self[section].restore_defaults() +def _get_triple_quote(value): + """Helper for triple-quoting round-trips.""" + if ('"""' in value) and ("'''" in value): + raise ConfigObjError('Value cannot be safely quoted: {!r}'.format(value)) + + return tsquot if "'''" in value else tdquot + + class ConfigObj(Section): """An object to read, create, and write config files.""" + MAX_PARSE_ERROR_DETAILS = 5 + + # Override/append to this class variable for alternative comment markers + # TODO: also support inline comments (needs dynamic compiling of the regex below) + COMMENT_MARKERS = ['#'] + _keyword = re.compile(r'''^ # line start (\s*) # indentation ( # keyword @@ -1106,7 +1068,7 @@ class ConfigObj(Section): (?:[^'"\s].*?) # at least one non-space unquoted ) # section name close ((?:\s*\])+) # 4: section marker close - \s*(\#.*)? # 5: optional comment + (\s*(?:\#.*)?)? # 5: optional comment $''', re.VERBOSE) @@ -1136,7 +1098,7 @@ class ConfigObj(Section): )| (,) # alternatively a single comma - empty list ) - \s*(\#.*)? # optional comment + (\s*(?:\#.*)?)? # optional comment $''', re.VERBOSE) @@ -1145,7 +1107,7 @@ class ConfigObj(Section): ( (?:".*?")| # double quotes (?:'.*?')| # single quotes - (?:[^'",\#]?.*?) # unquoted + (?:[^'",\#]?.*?) # unquoted ) \s*,\s* # comma ''', @@ -1160,15 +1122,16 @@ class ConfigObj(Section): (?:[^'"\#].*?)| # unquoted (?:) # Empty value ) - \s*(\#.*)? # optional comment + (\s*(?:\#.*)?)? # optional comment $''', re.VERBOSE) # regexes for finding triple quoted values on one line - _single_line_single = re.compile(r"^'''(.*?)'''\s*(#.*)?$") - _single_line_double = re.compile(r'^"""(.*?)"""\s*(#.*)?$') - _multi_line_single = re.compile(r"^(.*?)'''\s*(#.*)?$") - _multi_line_double = re.compile(r'^(.*?)"""\s*(#.*)?$') + _triple_trailer = r"(\s*(?:#.*)?)?$" + _single_line_single = re.compile(r"^'''(.*?)'''" + _triple_trailer) + _single_line_double = re.compile(r'^"""(.*?)"""' + _triple_trailer) + _multi_line_single = re.compile(r"^(.*?)'''" + _triple_trailer) + _multi_line_double = re.compile(r'^(.*?)"""' + _triple_trailer) _triple_quote = { "'''": (_single_line_single, _multi_line_single), @@ -1218,13 +1181,13 @@ class ConfigObj(Section): import warnings warnings.warn('Passing in an options dictionary to ConfigObj() is ' 'deprecated. Use **options instead.', - DeprecationWarning, stacklevel=2) + 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 OPTION_DEFAULTS.items(): + for entry, value in list(OPTION_DEFAULTS.items()): if entry not in options: options[entry] = value keyword_value = _options[entry] @@ -1241,14 +1204,17 @@ class ConfigObj(Section): self._original_configspec = configspec self._load(infile, configspec) - def _load(self, infile, configspec): - if isinstance(infile, basestring): + try: + infile = infile.__fspath__() + except AttributeError: + pass + + 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) @@ -1257,13 +1223,12 @@ 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 @@ -1291,21 +1256,21 @@ class ConfigObj(Section): 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.') + raise TypeError('infile must be a path-like object, 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): @@ -1313,15 +1278,20 @@ class ConfigObj(Section): break break - infile = [line.rstrip('\r\n') for line in 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(infile) + 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 if len(self._errors) > 1: - msg = "Parsing failed with several errors.\nFirst error %s" % info - error = ConfigObjError(msg) + msg = ["Parsing failed with {} errors.".format(len(self._errors))] + for error in self._errors[:self.MAX_PARSE_ERROR_DETAILS]: + msg.append(str(error)) + if len(self._errors) > self.MAX_PARSE_ERROR_DETAILS: + msg.append("{} more error(s)!" + .format(len(self._errors) - self.MAX_PARSE_ERROR_DETAILS)) + error = ConfigObjError('\n '.join(msg)) else: error = self._errors[0] # set the errors attribute; it's a list of tuples: @@ -1377,9 +1347,9 @@ class ConfigObj(Section): return self[key] except MissingInterpolationOption: return dict.__getitem__(self, key) - return ('ConfigObj({%s})' % - ', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) - for key in (self.scalars + self.sections)])) + return ('{}({{{}}})'.format(self.__class__.__name__, + ', '.join([('{}: {}'.format(repr(key), repr(_getval(key)))) + for key in (self.scalars + self.sections)]))) def _handle_bom(self, infile): @@ -1404,6 +1374,7 @@ class ConfigObj(Section): ``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 @@ -1415,6 +1386,13 @@ class ConfigObj(Section): 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 @@ -1423,7 +1401,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 @@ -1453,8 +1431,9 @@ class ConfigObj(Section): 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 @@ -1468,27 +1447,26 @@ 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 - - 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 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 aString + return self._decode(infile, 'utf-8') def _decode(self, infile, encoding): @@ -1497,34 +1475,42 @@ class ConfigObj(Section): if is a string, it also needs converting to a list. """ - if isinstance(infile, basestring): - # can't be unicode + 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 isinstance(infile, six.string_types): + 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 @@ -1542,6 +1528,7 @@ class ConfigObj(Section): maxline = len(infile) - 1 cur_index = -1 reset_comment = False + comment_markers = tuple(self.COMMENT_MARKERS) while cur_index < maxline: if reset_comment: @@ -1550,7 +1537,7 @@ class ConfigObj(Section): line = infile[cur_index] sline = line.strip() # do we have anything on the line ? - if not sline or sline.startswith('#'): + if not sline or sline.startswith(comment_markers): reset_comment = False comment_list.append(line) continue @@ -1571,7 +1558,7 @@ 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 @@ -1581,7 +1568,7 @@ class ConfigObj(Section): 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: @@ -1591,12 +1578,13 @@ 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 sect_name in parent: - self._handle_error('Duplicate section name at line %s.', + self._handle_error('Duplicate section name', DuplicateError, infile, cur_index) continue @@ -1615,10 +1603,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 ({!r}) (matched as neither section nor keyword)'.format(line), ParseError, infile, cur_index) else: # is a keyword value @@ -1633,7 +1619,7 @@ class ConfigObj(Section): 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: @@ -1641,26 +1627,24 @@ class ConfigObj(Section): comment = '' try: value = unrepr(value) - except Exception, e: - if type(e) == UnknownType: - msg = 'Unknown name or type in value at line %s.' + except Exception as cause: + if isinstance(cause, UnknownType): + msg = 'Unknown name or type in value' else: - msg = 'Parse error in value at line %s.' - self._handle_error(msg, UnreprError, infile, - cur_index) + msg = 'Parse error from unrepr-ing multiline value' + self._handle_error(msg, UnreprError, infile, cur_index) continue else: if self.unrepr: comment = '' try: value = unrepr(value) - except Exception, e: - if isinstance(e, UnknownType): - msg = 'Unknown name or type in value at line %s.' + except Exception as cause: + if isinstance(cause, UnknownType): + msg = 'Unknown name or type in value' else: - msg = 'Parse error in value at line %s.' - self._handle_error(msg, UnreprError, infile, - cur_index) + msg = 'Parse error from unrepr-ing value' + self._handle_error(msg, UnreprError, infile, cur_index) continue else: # extract comment and lists @@ -1668,14 +1652,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 key in this_section: self._handle_error( - 'Duplicate keyword name at line %s.', + 'Duplicate keyword name', DuplicateError, infile, cur_index) continue # add the key. @@ -1726,7 +1710,7 @@ class ConfigObj(Section): """ line = infile[cur_index] cur_index += 1 - message = text % cur_index + message = '{} at line {}.'.format(text, cur_index) error = ErrorClass(message, cur_index, line) if self.raise_errors: # raise the error - parsing stops here @@ -1777,8 +1761,10 @@ 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) @@ -1798,7 +1784,7 @@ class ConfigObj(Section): # for normal values either single or double quotes will do elif '\n' in value: # will only happen if multiline is off - e.g. '\n' in key - raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) + raise ConfigObjError('Value cannot be safely quoted: {!r}'.format(value)) elif ((value[0] not in wspace_plus) and (value[-1] not in wspace_plus) and (',' not in value)): @@ -1807,7 +1793,7 @@ class ConfigObj(Section): quot = self._get_single_quote(value) else: # if value has '\n' or "'" *and* '"', it will need triple quotes - quot = self._get_triple_quote(value) + quot = _get_triple_quote(value) if quot == noquot and '#' in value and self.list_values: quot = self._get_single_quote(value) @@ -1817,7 +1803,7 @@ class ConfigObj(Section): def _get_single_quote(self, value): if ("'" in value) and ('"' in value): - raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) + raise ConfigObjError('Value cannot be safely quoted: {!r}'.format(value)) elif '"' in value: quot = squot else: @@ -1825,16 +1811,6 @@ class ConfigObj(Section): return quot - def _get_triple_quote(self, value): - if (value.find('"""') != -1) and (value.find("'''") != -1): - raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) - if value.find('"""') == -1: - quot = tdquot - else: - quot = tsquot - return quot - - def _handle_value(self, value): """ Given a value string, unquote, remove comment, @@ -1929,12 +1905,12 @@ class ConfigObj(Section): raise_errors=True, file_error=True, _inspec=True) - except ConfigObjError, e: + except ConfigObjError as cause: # FIXME: Should these errors have a reference # to the already parsed ConfigObj ? - raise ConfigspecError('Parsing configspec failed: %s' % e) - except IOError, e: - raise IOError('Reading configspec failed: %s' % e) + raise ConfigspecError('Parsing configspec failed: %s' % cause) + except IOError as cause: + raise IOError('Reading configspec failed: %s' % cause) self.configspec = configspec @@ -1977,27 +1953,32 @@ class ConfigObj(Section): val = repr(this_entry) return '%s%s%s%s%s' % (indent_string, self._decode_element(self._quote(entry, multiline=False)), - self._a_to_u(' = '), + ' = ', val, self._decode_element(comment)) def _write_marker(self, indent_string, depth, entry, comment): """Write a section marker line""" + entry_str = self._decode_element(entry) + title = self._quote(entry_str, multiline=False) + if entry_str and title[0] in '\'"' and title[1:-1] == entry_str: + # titles are in '[]' already, so quoting for contained quotes is not necessary (#74) + title = entry_str return '%s%s%s%s%s' % (indent_string, - self._a_to_u('[' * depth), - self._quote(self._decode_element(entry), multiline=False), - self._a_to_u(']' * depth), + '[' * depth, + title, + ']' * depth, self._decode_element(comment)) def _handle_comment(self, comment): """Deal with a comment.""" - if not comment: - return '' + if not comment.strip(): + return comment or '' # return trailing whitespace as-is start = self.indent_type - if not comment.startswith('#'): - start += self._a_to_u(' # ') + if not comment.lstrip().startswith('#'): + start += ' # ' return (start + comment) @@ -2023,8 +2004,8 @@ class ConfigObj(Section): self.indent_type = DEFAULT_INDENT_TYPE out = [] - cs = self._a_to_u('#') - csp = self._a_to_u('# ') + comment_markers = tuple(self.COMMENT_MARKERS) + comment_marker_default = comment_markers[0] + ' ' if section is None: int_val = self.interpolation self.interpolation = False @@ -2032,8 +2013,8 @@ class ConfigObj(Section): for line in self.initial_comment: line = self._decode_element(line) stripped_line = line.strip() - if stripped_line and not stripped_line.startswith(cs): - line = csp + line + if stripped_line and not stripped_line.startswith(comment_markers): + line = comment_marker_default + line out.append(line) indent_string = self.indent_type * section.depth @@ -2043,13 +2024,13 @@ class ConfigObj(Section): continue for comment_line in section.comments[entry]: comment_line = self._decode_element(comment_line.lstrip()) - if comment_line and not comment_line.startswith(cs): - comment_line = csp + comment_line + if comment_line and not comment_line.startswith(comment_markers): + comment_line = comment_marker_default + comment_line out.append(indent_string + comment_line) this_entry = section[entry] comment = self._handle_comment(section.inline_comments[entry]) - if isinstance(this_entry, dict): + if isinstance(this_entry, Section): # a section out.append(self._write_marker( indent_string, @@ -2068,8 +2049,8 @@ class ConfigObj(Section): for line in self.final_comment: line = self._decode_element(line) stripped_line = line.strip() - if stripped_line and not stripped_line.startswith(cs): - line = csp + line + if stripped_line and not stripped_line.startswith(comment_markers): + line = comment_marker_default + line out.append(line) self.interpolation = int_val @@ -2096,22 +2077,26 @@ class ConfigObj(Section): 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 - + output = newline.join(out) 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): @@ -2155,7 +2140,7 @@ class ConfigObj(Section): if preserve_errors: # We do this once to remove a top level dependency on the validate module # Which makes importing configobj faster - from validate import VdtMissingValue + from configobj.validate import VdtMissingValue self._vdtMissingValue = VdtMissingValue section = self @@ -2189,12 +2174,12 @@ class ConfigObj(Section): val, missing=missing ) - except validator.baseErrorClass, e: - if not preserve_errors or isinstance(e, self._vdtMissingValue): + except validator.baseErrorClass as cause: + if not preserve_errors or isinstance(cause, self._vdtMissingValue): out[entry] = False else: # preserve the error - out[entry] = e + out[entry] = cause ret_false = False ret_true = False else: @@ -2338,7 +2323,7 @@ class ConfigObj(Section): 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 @@ -2416,16 +2401,16 @@ def flatten_errors(cfg, res, levels=None, results=None): levels = [] results = [] if res == True: - return results + 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), Mapping): # Go down one level levels.append(key) flatten_errors(cfg[key], val, levels, results) @@ -2436,7 +2421,7 @@ def flatten_errors(cfg, res, levels=None, results=None): if levels: levels.pop() # - return results + return sorted(results) def get_extra_values(conf, _prepend=()): diff --git a/lib/configobj/_version.py b/lib/configobj/_version.py new file mode 100644 index 00000000..19a71401 --- /dev/null +++ b/lib/configobj/_version.py @@ -0,0 +1,2 @@ +"""Project version""" +__version__ = '5.1.0' diff --git a/lib/configobj/validate.py b/lib/configobj/validate.py new file mode 100644 index 00000000..c21b5864 --- /dev/null +++ b/lib/configobj/validate.py @@ -0,0 +1,1474 @@ +# validate.py +# -*- coding: utf-8 -*- +# pylint: disable= +# +# 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``. +""" +import re +import sys +from pprint import pprint + +__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] +) + + +#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 + + +def dottedQuadToNum(ip): + """ + Convert decimal dotted quad string to long integer + + >>> int(dottedQuadToNum('1 ')) + 1 + >>> int(dottedQuadToNum('1.2.3.4')) + 16909060 + """ + + # 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 "{}" is unknown.'.format(value)) + + +class VdtParamError(SyntaxError): + """An incorrect parameter was passed""" + + NOT_GIVEN = object() + + def __init__(self, name_or_msg, value=NOT_GIVEN): + """ + >>> raise VdtParamError('yoda', 'jedi') + Traceback (most recent call last): + VdtParamError: passed an incorrect value "jedi" for parameter "yoda". + >>> raise VdtParamError('preformatted message') + Traceback (most recent call last): + VdtParamError: preformatted message + """ + if value is self.NOT_GIVEN: + SyntaxError.__init__(self, name_or_msg) + else: + SyntaxError.__init__(self, 'passed an incorrect value "{}" for parameter "{}".'.format(value, name_or_msg)) + + +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 "{}" is of the wrong type.'.format(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 "{}" is unacceptable.'.format(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 "{}" is too small.'.format(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 "{}" is too big.'.format(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 "{}" is too short.'.format(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 "{}" is too long.'.format(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.io/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 = {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: + 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( # pylint: disable=unbalanced-tuple-unpacking + ('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( # pylint: disable=unbalanced-tuple-unpacking + ('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( # pylint: disable=unbalanced-tuple-unpacking + ('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( # pylint: disable=unbalanced-tuple-unpacking + ('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 = { + int: is_integer, + 'int': is_integer, + 'integer': is_integer, + float: is_float, + 'float': is_float, + 'ip_addr': is_ip_addr, + str: is_string, + 'str': is_string, + 'string': is_string, + bool: is_boolean, + 'bool': is_boolean, + '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 cause: + raise VdtParamError('mixed_list', cause) + + +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) + print('{} {} failures out of {} tests' + .format("FAIL" if failures else "*OK*", failures, tests)) + sys.exit(bool(failures))