Update configobj to 5.1.0

This commit is contained in:
JonnyWong16 2019-11-23 18:57:54 -08:00
parent f2d7beec90
commit 183c810c76
3 changed files with 1715 additions and 254 deletions

View file

@ -1,29 +1,38 @@
# configobj.py # configobj.py
# A config file reader/writer that supports nested sections in config files. # -*- coding: utf-8 -*-
# Copyright (C) 2005-2010 Michael Foord, Nicola Larosa # pylint: disable=bad-continuation
# E-mail: fuzzyman AT voidspace DOT org DOT uk
# nico AT tekNico DOT net
# ConfigObj 4 """A config file reader/writer that supports nested sections in config files."""
# http://www.voidspace.org.uk/python/configobj.html
# Released subject to the BSD License # Copyright (C) 2005-2014:
# Please see http://www.voidspace.org.uk/python/license.shtml # (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 # This software is licensed under the terms of the BSD license.
# For information about bugfixes, updates and support, please join the # http://opensource.org/licenses/BSD-3-Clause
# ConfigObj mailing list:
# http://lists.sourceforge.net/lists/listinfo/configobj-develop
# Comments, suggestions and bug reports welcome.
from __future__ import generators # ConfigObj 5 - main repository for documentation and issue tracking:
# https://github.com/DiffSK/configobj
import os import os
import re import re
import sys import sys
import copy
from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE 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 # imported lazily to avoid startup performance hit if it isn't used
compiler = None compiler = None
@ -83,20 +92,7 @@ tdquot = "'''%s'''"
# Sentinel for use in getattr calls to replace hasattr # Sentinel for use in getattr calls to replace hasattr
MISSING = object() MISSING = object()
__version__ = '4.7.2'
try:
any
except NameError:
def any(iterable):
for entry in iterable:
if entry:
return True
return False
__all__ = ( __all__ = (
'__version__',
'DEFAULT_INDENT_TYPE', 'DEFAULT_INDENT_TYPE',
'DEFAULT_INTERPOLATION', 'DEFAULT_INTERPOLATION',
'ConfigObjError', 'ConfigObjError',
@ -137,6 +133,8 @@ OPTION_DEFAULTS = {
'write_empty_values': False, '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): def getObj(s):
@ -152,70 +150,13 @@ class UnknownType(Exception):
pass 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): def unrepr(s):
if not s: if not s:
return s return s
return _builder.build(getObj(s))
# this is supposed to be safe
import ast
return ast.literal_eval(s)
class ConfigObjError(SyntaxError): class ConfigObjError(SyntaxError):
@ -518,7 +459,7 @@ class Section(dict):
self._initialise() self._initialise()
# we do this explicitly so that __setitem__ is used properly # we do this explicitly so that __setitem__ is used properly
# (rather than just passing to ``dict.__init__``) # (rather than just passing to ``dict.__init__``)
for entry, value in indict.iteritems(): for entry, value in indict.items():
self[entry] = value self[entry] = value
@ -566,11 +507,11 @@ class Section(dict):
"""Fetch the item and do string interpolation.""" """Fetch the item and do string interpolation."""
val = dict.__getitem__(self, key) val = dict.__getitem__(self, key)
if self.main.interpolation: if self.main.interpolation:
if isinstance(val, basestring): if isinstance(val, six.string_types):
return self._interpolate(key, val) return self._interpolate(key, val)
if isinstance(val, list): if isinstance(val, list):
def _check(entry): def _check(entry):
if isinstance(entry, basestring): if isinstance(entry, six.string_types):
return self._interpolate(key, entry) return self._interpolate(key, entry)
return entry return entry
new = [_check(entry) for entry in val] 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 ``unrepr`` must be set when setting a value to a dictionary, without
creating a new sub-section. 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) raise ValueError('The key "%s" is not a string.' % key)
# add the comment # add the comment
@ -608,7 +549,7 @@ class Section(dict):
if key not in self: if key not in self:
self.sections.append(key) self.sections.append(key)
dict.__setitem__(self, key, value) 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, # First create the new depth level,
# then create the section # then create the section
if key not in self: if key not in self:
@ -627,11 +568,11 @@ class Section(dict):
if key not in self: if key not in self:
self.scalars.append(key) self.scalars.append(key)
if not self.main.stringify: if not self.main.stringify:
if isinstance(value, basestring): if isinstance(value, six.string_types):
pass pass
elif isinstance(value, (list, tuple)): elif isinstance(value, (list, tuple)):
for entry in value: 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) raise TypeError('Value is not a string "%s".' % entry)
else: else:
raise TypeError('Value is not a string "%s".' % value) raise TypeError('Value is not a string "%s".' % value)
@ -721,17 +662,17 @@ class Section(dict):
def items(self): def items(self):
"""D.items() -> list of D's (key, value) pairs, as 2-tuples""" """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): def keys(self):
"""D.keys() -> list of D's keys""" """D.keys() -> list of D's keys"""
return (self.scalars + self.sections) return self.scalars + self.sections
def values(self): def values(self):
"""D.values() -> list of D's values""" """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): def iteritems(self):
@ -741,7 +682,7 @@ class Section(dict):
def iterkeys(self): def iterkeys(self):
"""D.iterkeys() -> an iterator over the keys of D""" """D.iterkeys() -> an iterator over the keys of D"""
return iter((self.scalars + self.sections)) return iter(self.keys())
__iter__ = iterkeys __iter__ = iterkeys
@ -758,7 +699,7 @@ class Section(dict):
return self[key] return self[key]
except MissingInterpolationOption: except MissingInterpolationOption:
return dict.__getitem__(self, key) 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)]) for key in (self.scalars + self.sections)])
__str__ = __repr__ __str__ = __repr__
@ -795,10 +736,15 @@ class Section(dict):
return newdict return newdict
def merge(self, indict): def merge(self, indict, decoupled=False):
""" """
A recursive update - useful for merging config files. 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] >>> a = '''[section1]
... option1 = True ... option1 = True
... [[subsection]] ... [[subsection]]
@ -815,9 +761,11 @@ class Section(dict):
ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}}) ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}})
""" """
for key, val in indict.items(): for key, val in indict.items():
if (key in self and isinstance(self[key], dict) and if decoupled:
isinstance(val, dict)): val = copy.deepcopy(val)
self[key].merge(val) if (key in self and isinstance(self[key], Mapping) and
isinstance(val, Mapping)):
self[key].merge(val, decoupled=decoupled)
else: else:
self[key] = val self[key] = val
@ -972,7 +920,7 @@ class Section(dict):
return False return False
else: else:
try: try:
if not isinstance(val, basestring): if not isinstance(val, six.string_types):
# TODO: Why do we raise a KeyError here? # TODO: Why do we raise a KeyError here?
raise KeyError() raise KeyError()
else: else:
@ -1013,15 +961,15 @@ class Section(dict):
>>> a = ConfigObj() >>> a = ConfigObj()
>>> a['a'] = 'fish' >>> a['a'] = 'fish'
>>> a.as_float('a') >>> a.as_float('a') #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last): Traceback (most recent call last):
ValueError: invalid literal for float(): fish ValueError: invalid literal for float(): fish
>>> a['b'] = '1' >>> a['b'] = '1'
>>> a.as_float('b') >>> a.as_float('b')
1.0 1.0
>>> a['b'] = '3.2' >>> a['b'] = '3.2'
>>> a.as_float('b') >>> a.as_float('b') #doctest: +ELLIPSIS
3.2000000000000002 3.2...
""" """
return float(self[key]) return float(self[key])
@ -1081,9 +1029,23 @@ class Section(dict):
self[section].restore_defaults() 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): class ConfigObj(Section):
"""An object to read, create, and write config files.""" """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 _keyword = re.compile(r'''^ # line start
(\s*) # indentation (\s*) # indentation
( # keyword ( # keyword
@ -1106,7 +1068,7 @@ class ConfigObj(Section):
(?:[^'"\s].*?) # at least one non-space unquoted (?:[^'"\s].*?) # at least one non-space unquoted
) # section name close ) # section name close
((?:\s*\])+) # 4: section marker close ((?:\s*\])+) # 4: section marker close
\s*(\#.*)? # 5: optional comment (\s*(?:\#.*)?)? # 5: optional comment
$''', $''',
re.VERBOSE) re.VERBOSE)
@ -1136,7 +1098,7 @@ class ConfigObj(Section):
)| )|
(,) # alternatively a single comma - empty list (,) # alternatively a single comma - empty list
) )
\s*(\#.*)? # optional comment (\s*(?:\#.*)?)? # optional comment
$''', $''',
re.VERBOSE) re.VERBOSE)
@ -1160,15 +1122,16 @@ class ConfigObj(Section):
(?:[^'"\#].*?)| # unquoted (?:[^'"\#].*?)| # unquoted
(?:) # Empty value (?:) # Empty value
) )
\s*(\#.*)? # optional comment (\s*(?:\#.*)?)? # optional comment
$''', $''',
re.VERBOSE) re.VERBOSE)
# regexes for finding triple quoted values on one line # regexes for finding triple quoted values on one line
_single_line_single = re.compile(r"^'''(.*?)'''\s*(#.*)?$") _triple_trailer = r"(\s*(?:#.*)?)?$"
_single_line_double = re.compile(r'^"""(.*?)"""\s*(#.*)?$') _single_line_single = re.compile(r"^'''(.*?)'''" + _triple_trailer)
_multi_line_single = re.compile(r"^(.*?)'''\s*(#.*)?$") _single_line_double = re.compile(r'^"""(.*?)"""' + _triple_trailer)
_multi_line_double = re.compile(r'^(.*?)"""\s*(#.*)?$') _multi_line_single = re.compile(r"^(.*?)'''" + _triple_trailer)
_multi_line_double = re.compile(r'^(.*?)"""' + _triple_trailer)
_triple_quote = { _triple_quote = {
"'''": (_single_line_single, _multi_line_single), "'''": (_single_line_single, _multi_line_single),
@ -1218,13 +1181,13 @@ class ConfigObj(Section):
import warnings import warnings
warnings.warn('Passing in an options dictionary to ConfigObj() is ' warnings.warn('Passing in an options dictionary to ConfigObj() is '
'deprecated. Use **options instead.', 'deprecated. Use **options instead.',
DeprecationWarning, stacklevel=2) DeprecationWarning)
# TODO: check the values too. # TODO: check the values too.
for entry in options: for entry in options:
if entry not in OPTION_DEFAULTS: if entry not in OPTION_DEFAULTS:
raise TypeError('Unrecognised option "%s".' % entry) 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: if entry not in options:
options[entry] = value options[entry] = value
keyword_value = _options[entry] keyword_value = _options[entry]
@ -1241,14 +1204,17 @@ class ConfigObj(Section):
self._original_configspec = configspec self._original_configspec = configspec
self._load(infile, configspec) self._load(infile, configspec)
def _load(self, 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 self.filename = infile
if os.path.isfile(infile): if os.path.isfile(infile):
h = open(infile, 'rb') with open(infile, 'rb') as h:
infile = h.read() or [] content = h.readlines() or []
h.close()
elif self.file_error: elif self.file_error:
# raise an error if the file doesn't exist # raise an error if the file doesn't exist
raise IOError('Config file not found: "%s".' % self.filename) raise IOError('Config file not found: "%s".' % self.filename)
@ -1257,13 +1223,12 @@ class ConfigObj(Section):
if self.create_empty: if self.create_empty:
# this is a good test that the filename specified # this is a good test that the filename specified
# isn't impossible - like on a non-existent device # isn't impossible - like on a non-existent device
h = open(infile, 'w') with open(infile, 'w') as h:
h.write('') h.write('')
h.close() content = []
infile = []
elif isinstance(infile, (list, tuple)): elif isinstance(infile, (list, tuple)):
infile = list(infile) content = list(infile)
elif isinstance(infile, dict): elif isinstance(infile, dict):
# initialise self # initialise self
@ -1291,21 +1256,21 @@ class ConfigObj(Section):
elif getattr(infile, 'read', MISSING) is not MISSING: elif getattr(infile, 'read', MISSING) is not MISSING:
# This supports file like objects # This supports file like objects
infile = infile.read() or [] content = infile.read() or []
# needs splitting into lines - but needs doing *after* decoding # needs splitting into lines - but needs doing *after* decoding
# in case it's not an 8 bit encoding # in case it's not an 8 bit encoding
else: 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 # don't do it for the empty ConfigObj
infile = self._handle_bom(infile) content = self._handle_bom(content)
# infile is now *always* a list # infile is now *always* a list
# #
# Set the newlines attribute (first line ending it finds) # Set the newlines attribute (first line ending it finds)
# and strip trailing '\n' or '\r' from lines # and strip trailing '\n' or '\r' from lines
for line in infile: for line in content:
if (not line) or (line[-1] not in ('\r', '\n', '\r\n')): if (not line) or (line[-1] not in ('\r', '\n')):
continue continue
for end in ('\r\n', '\n', '\r'): for end in ('\r\n', '\n', '\r'):
if line.endswith(end): if line.endswith(end):
@ -1313,15 +1278,20 @@ class ConfigObj(Section):
break break
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 we had any errors, now is the time to raise them
if self._errors: if self._errors:
info = "at line %s." % self._errors[0].line_number
if len(self._errors) > 1: if len(self._errors) > 1:
msg = "Parsing failed with several errors.\nFirst error %s" % info msg = ["Parsing failed with {} errors.".format(len(self._errors))]
error = ConfigObjError(msg) 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: else:
error = self._errors[0] error = self._errors[0]
# set the errors attribute; it's a list of tuples: # set the errors attribute; it's a list of tuples:
@ -1377,9 +1347,9 @@ class ConfigObj(Section):
return self[key] return self[key]
except MissingInterpolationOption: except MissingInterpolationOption:
return dict.__getitem__(self, key) return dict.__getitem__(self, key)
return ('ConfigObj({%s})' % return ('{}({{{}}})'.format(self.__class__.__name__,
', '.join([('%s: %s' % (repr(key), repr(_getval(key)))) ', '.join([('{}: {}'.format(repr(key), repr(_getval(key))))
for key in (self.scalars + self.sections)])) for key in (self.scalars + self.sections)])))
def _handle_bom(self, infile): 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 ``infile`` must always be returned as a list of lines, but may be
passed in as a single string. passed in as a single string.
""" """
if ((self.encoding is not None) and if ((self.encoding is not None) and
(self.encoding.lower() not in BOM_LIST)): (self.encoding.lower() not in BOM_LIST)):
# No need to check for a BOM # No need to check for a BOM
@ -1415,6 +1386,13 @@ class ConfigObj(Section):
line = infile[0] line = infile[0]
else: else:
line = infile 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: if self.encoding is not None:
# encoding explicitly supplied # encoding explicitly supplied
# And it could have an associated BOM # And it could have an associated BOM
@ -1423,7 +1401,7 @@ class ConfigObj(Section):
enc = BOM_LIST[self.encoding.lower()] enc = BOM_LIST[self.encoding.lower()]
if enc == 'utf_16': if enc == 'utf_16':
# For UTF16 we try big endian and little endian # 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: if not final_encoding:
# skip UTF8 # skip UTF8
continue continue
@ -1453,8 +1431,9 @@ class ConfigObj(Section):
return self._decode(infile, self.encoding) return self._decode(infile, self.encoding)
# No encoding specified - so we need to check for UTF8/UTF16 # No encoding specified - so we need to check for UTF8/UTF16
for BOM, (encoding, final_encoding) in BOMS.items(): for BOM, (encoding, final_encoding) in list(BOMS.items()):
if not line.startswith(BOM): if not isinstance(line, six.binary_type) or not line.startswith(BOM):
# didn't specify a BOM, or it's not a bytestring
continue continue
else: else:
# BOM discovered # BOM discovered
@ -1468,27 +1447,26 @@ class ConfigObj(Section):
infile[0] = newline infile[0] = newline
else: else:
infile = newline infile = newline
# UTF8 - don't decode # UTF-8
if isinstance(infile, basestring): if isinstance(infile, six.text_type):
return infile.splitlines(True) return infile.splitlines(True)
elif isinstance(infile, six.binary_type):
return infile.decode('utf-8').splitlines(True)
else: else:
return infile return self._decode(infile, 'utf-8')
# UTF16 - have to decode # UTF16 - have to decode
return self._decode(infile, encoding) 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):
def _a_to_u(self, aString): # don't actually do any decoding, since we're on python 2 and
"""Decode ASCII strings to unicode if a self.encoding is specified.""" # returning a bytestring is fine
if self.encoding: return self._decode(infile, None)
return aString.decode('ascii') # 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: else:
return aString return self._decode(infile, 'utf-8')
def _decode(self, infile, encoding): def _decode(self, infile, encoding):
@ -1497,12 +1475,18 @@ class ConfigObj(Section):
if is a string, it also needs converting to a list. if is a string, it also needs converting to a list.
""" """
if isinstance(infile, basestring): if isinstance(infile, six.binary_type):
# can't be unicode
# NOTE: Could raise a ``UnicodeDecodeError`` # NOTE: Could raise a ``UnicodeDecodeError``
if encoding:
return infile.decode(encoding).splitlines(True) 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): for i, line in enumerate(infile):
if not isinstance(line, unicode): if isinstance(line, six.binary_type):
# NOTE: The isinstance test here handles mixed lists of unicode/string # NOTE: The isinstance test here handles mixed lists of unicode/string
# NOTE: But the decode will break on any non-string values # NOTE: But the decode will break on any non-string values
# NOTE: Or could raise a ``UnicodeDecodeError`` # NOTE: Or could raise a ``UnicodeDecodeError``
@ -1512,19 +1496,21 @@ class ConfigObj(Section):
def _decode_element(self, line): def _decode_element(self, line):
"""Decode element to unicode if necessary.""" """Decode element to unicode if necessary."""
if not self.encoding: if isinstance(line, six.binary_type) and self.default_encoding:
return line
if isinstance(line, str) and self.default_encoding:
return line.decode(self.default_encoding) return line.decode(self.default_encoding)
else:
return line return line
# TODO: this may need to be modified
def _str(self, value): def _str(self, value):
""" """
Used by ``stringify`` within validate, to turn non-string values Used by ``stringify`` within validate, to turn non-string values
into strings. 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) return str(value)
else: else:
return value return value
@ -1542,6 +1528,7 @@ class ConfigObj(Section):
maxline = len(infile) - 1 maxline = len(infile) - 1
cur_index = -1 cur_index = -1
reset_comment = False reset_comment = False
comment_markers = tuple(self.COMMENT_MARKERS)
while cur_index < maxline: while cur_index < maxline:
if reset_comment: if reset_comment:
@ -1550,7 +1537,7 @@ class ConfigObj(Section):
line = infile[cur_index] line = infile[cur_index]
sline = line.strip() sline = line.strip()
# do we have anything on the line ? # do we have anything on the line ?
if not sline or sline.startswith('#'): if not sline or sline.startswith(comment_markers):
reset_comment = False reset_comment = False
comment_list.append(line) comment_list.append(line)
continue continue
@ -1571,7 +1558,7 @@ class ConfigObj(Section):
self.indent_type = indent self.indent_type = indent
cur_depth = sect_open.count('[') cur_depth = sect_open.count('[')
if cur_depth != sect_close.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) NestingError, infile, cur_index)
continue continue
@ -1581,7 +1568,7 @@ class ConfigObj(Section):
parent = self._match_depth(this_section, parent = self._match_depth(this_section,
cur_depth).parent cur_depth).parent
except SyntaxError: except SyntaxError:
self._handle_error("Cannot compute nesting level at line %s.", self._handle_error("Cannot compute nesting level",
NestingError, infile, cur_index) NestingError, infile, cur_index)
continue continue
elif cur_depth == this_section.depth: elif cur_depth == this_section.depth:
@ -1591,12 +1578,13 @@ class ConfigObj(Section):
# the new section is a child the current section # the new section is a child the current section
parent = this_section parent = this_section
else: else:
self._handle_error("Section too nested at line %s.", self._handle_error("Section too nested",
NestingError, infile, cur_index) NestingError, infile, cur_index)
continue
sect_name = self._unquote(sect_name) sect_name = self._unquote(sect_name)
if sect_name in parent: if sect_name in parent:
self._handle_error('Duplicate section name at line %s.', self._handle_error('Duplicate section name',
DuplicateError, infile, cur_index) DuplicateError, infile, cur_index)
continue continue
@ -1615,10 +1603,8 @@ class ConfigObj(Section):
# so it should be a valid ``key = value`` line # so it should be a valid ``key = value`` line
mat = self._keyword.match(line) mat = self._keyword.match(line)
if mat is None: if mat is None:
# it neither matched as a keyword
# or a section marker
self._handle_error( self._handle_error(
'Invalid line at line "%s".', 'Invalid line ({!r}) (matched as neither section nor keyword)'.format(line),
ParseError, infile, cur_index) ParseError, infile, cur_index)
else: else:
# is a keyword value # is a keyword value
@ -1633,7 +1619,7 @@ class ConfigObj(Section):
value, infile, cur_index, maxline) value, infile, cur_index, maxline)
except SyntaxError: except SyntaxError:
self._handle_error( self._handle_error(
'Parse error in value at line %s.', 'Parse error in multiline value',
ParseError, infile, cur_index) ParseError, infile, cur_index)
continue continue
else: else:
@ -1641,26 +1627,24 @@ class ConfigObj(Section):
comment = '' comment = ''
try: try:
value = unrepr(value) value = unrepr(value)
except Exception, e: except Exception as cause:
if type(e) == UnknownType: if isinstance(cause, UnknownType):
msg = 'Unknown name or type in value at line %s.' msg = 'Unknown name or type in value'
else: else:
msg = 'Parse error in value at line %s.' msg = 'Parse error from unrepr-ing multiline value'
self._handle_error(msg, UnreprError, infile, self._handle_error(msg, UnreprError, infile, cur_index)
cur_index)
continue continue
else: else:
if self.unrepr: if self.unrepr:
comment = '' comment = ''
try: try:
value = unrepr(value) value = unrepr(value)
except Exception, e: except Exception as cause:
if isinstance(e, UnknownType): if isinstance(cause, UnknownType):
msg = 'Unknown name or type in value at line %s.' msg = 'Unknown name or type in value'
else: else:
msg = 'Parse error in value at line %s.' msg = 'Parse error from unrepr-ing value'
self._handle_error(msg, UnreprError, infile, self._handle_error(msg, UnreprError, infile, cur_index)
cur_index)
continue continue
else: else:
# extract comment and lists # extract comment and lists
@ -1668,14 +1652,14 @@ class ConfigObj(Section):
(value, comment) = self._handle_value(value) (value, comment) = self._handle_value(value)
except SyntaxError: except SyntaxError:
self._handle_error( self._handle_error(
'Parse error in value at line %s.', 'Parse error in value',
ParseError, infile, cur_index) ParseError, infile, cur_index)
continue continue
# #
key = self._unquote(key) key = self._unquote(key)
if key in this_section: if key in this_section:
self._handle_error( self._handle_error(
'Duplicate keyword name at line %s.', 'Duplicate keyword name',
DuplicateError, infile, cur_index) DuplicateError, infile, cur_index)
continue continue
# add the key. # add the key.
@ -1726,7 +1710,7 @@ class ConfigObj(Section):
""" """
line = infile[cur_index] line = infile[cur_index]
cur_index += 1 cur_index += 1
message = text % cur_index message = '{} at line {}.'.format(text, cur_index)
error = ErrorClass(message, cur_index, line) error = ErrorClass(message, cur_index, line)
if self.raise_errors: if self.raise_errors:
# raise the error - parsing stops here # raise the error - parsing stops here
@ -1777,8 +1761,10 @@ class ConfigObj(Section):
return self._quote(value[0], multiline=False) + ',' return self._quote(value[0], multiline=False) + ','
return ', '.join([self._quote(val, multiline=False) return ', '.join([self._quote(val, multiline=False)
for val in value]) for val in value])
if not isinstance(value, basestring): if not isinstance(value, six.string_types):
if self.stringify: 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) value = str(value)
else: else:
raise TypeError('Value "%s" is not a string.' % value) 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 # for normal values either single or double quotes will do
elif '\n' in value: elif '\n' in value:
# will only happen if multiline is off - e.g. '\n' in key # 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 elif ((value[0] not in wspace_plus) and
(value[-1] not in wspace_plus) and (value[-1] not in wspace_plus) and
(',' not in value)): (',' not in value)):
@ -1807,7 +1793,7 @@ class ConfigObj(Section):
quot = self._get_single_quote(value) quot = self._get_single_quote(value)
else: else:
# if value has '\n' or "'" *and* '"', it will need triple quotes # 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: if quot == noquot and '#' in value and self.list_values:
quot = self._get_single_quote(value) quot = self._get_single_quote(value)
@ -1817,7 +1803,7 @@ class ConfigObj(Section):
def _get_single_quote(self, value): def _get_single_quote(self, value):
if ("'" in value) and ('"' in 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: elif '"' in value:
quot = squot quot = squot
else: else:
@ -1825,16 +1811,6 @@ class ConfigObj(Section):
return quot 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): def _handle_value(self, value):
""" """
Given a value string, unquote, remove comment, Given a value string, unquote, remove comment,
@ -1929,12 +1905,12 @@ class ConfigObj(Section):
raise_errors=True, raise_errors=True,
file_error=True, file_error=True,
_inspec=True) _inspec=True)
except ConfigObjError, e: except ConfigObjError as cause:
# FIXME: Should these errors have a reference # FIXME: Should these errors have a reference
# to the already parsed ConfigObj ? # to the already parsed ConfigObj ?
raise ConfigspecError('Parsing configspec failed: %s' % e) raise ConfigspecError('Parsing configspec failed: %s' % cause)
except IOError, e: except IOError as cause:
raise IOError('Reading configspec failed: %s' % e) raise IOError('Reading configspec failed: %s' % cause)
self.configspec = configspec self.configspec = configspec
@ -1977,27 +1953,32 @@ class ConfigObj(Section):
val = repr(this_entry) val = repr(this_entry)
return '%s%s%s%s%s' % (indent_string, return '%s%s%s%s%s' % (indent_string,
self._decode_element(self._quote(entry, multiline=False)), self._decode_element(self._quote(entry, multiline=False)),
self._a_to_u(' = '), ' = ',
val, val,
self._decode_element(comment)) self._decode_element(comment))
def _write_marker(self, indent_string, depth, entry, comment): def _write_marker(self, indent_string, depth, entry, comment):
"""Write a section marker line""" """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, return '%s%s%s%s%s' % (indent_string,
self._a_to_u('[' * depth), '[' * depth,
self._quote(self._decode_element(entry), multiline=False), title,
self._a_to_u(']' * depth), ']' * depth,
self._decode_element(comment)) self._decode_element(comment))
def _handle_comment(self, comment): def _handle_comment(self, comment):
"""Deal with a comment.""" """Deal with a comment."""
if not comment: if not comment.strip():
return '' return comment or '' # return trailing whitespace as-is
start = self.indent_type start = self.indent_type
if not comment.startswith('#'): if not comment.lstrip().startswith('#'):
start += self._a_to_u(' # ') start += ' # '
return (start + comment) return (start + comment)
@ -2023,8 +2004,8 @@ class ConfigObj(Section):
self.indent_type = DEFAULT_INDENT_TYPE self.indent_type = DEFAULT_INDENT_TYPE
out = [] out = []
cs = self._a_to_u('#') comment_markers = tuple(self.COMMENT_MARKERS)
csp = self._a_to_u('# ') comment_marker_default = comment_markers[0] + ' '
if section is None: if section is None:
int_val = self.interpolation int_val = self.interpolation
self.interpolation = False self.interpolation = False
@ -2032,8 +2013,8 @@ class ConfigObj(Section):
for line in self.initial_comment: for line in self.initial_comment:
line = self._decode_element(line) line = self._decode_element(line)
stripped_line = line.strip() stripped_line = line.strip()
if stripped_line and not stripped_line.startswith(cs): if stripped_line and not stripped_line.startswith(comment_markers):
line = csp + line line = comment_marker_default + line
out.append(line) out.append(line)
indent_string = self.indent_type * section.depth indent_string = self.indent_type * section.depth
@ -2043,13 +2024,13 @@ class ConfigObj(Section):
continue continue
for comment_line in section.comments[entry]: for comment_line in section.comments[entry]:
comment_line = self._decode_element(comment_line.lstrip()) comment_line = self._decode_element(comment_line.lstrip())
if comment_line and not comment_line.startswith(cs): if comment_line and not comment_line.startswith(comment_markers):
comment_line = csp + comment_line comment_line = comment_marker_default + comment_line
out.append(indent_string + comment_line) out.append(indent_string + comment_line)
this_entry = section[entry] this_entry = section[entry]
comment = self._handle_comment(section.inline_comments[entry]) comment = self._handle_comment(section.inline_comments[entry])
if isinstance(this_entry, dict): if isinstance(this_entry, Section):
# a section # a section
out.append(self._write_marker( out.append(self._write_marker(
indent_string, indent_string,
@ -2068,8 +2049,8 @@ class ConfigObj(Section):
for line in self.final_comment: for line in self.final_comment:
line = self._decode_element(line) line = self._decode_element(line)
stripped_line = line.strip() stripped_line = line.strip()
if stripped_line and not stripped_line.startswith(cs): if stripped_line and not stripped_line.startswith(comment_markers):
line = csp + line line = comment_marker_default + line
out.append(line) out.append(line)
self.interpolation = int_val self.interpolation = int_val
@ -2096,22 +2077,26 @@ class ConfigObj(Section):
and sys.platform == 'win32' and newline == '\r\n'): and sys.platform == 'win32' and newline == '\r\n'):
# Windows specific hack to avoid writing '\r\r\n' # Windows specific hack to avoid writing '\r\r\n'
newline = '\n' newline = '\n'
output = self._a_to_u(newline).join(out) output = 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): if not output.endswith(newline):
output += 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, def validate(self, validator, preserve_errors=False, copy=False,
section=None): section=None):
@ -2155,7 +2140,7 @@ class ConfigObj(Section):
if preserve_errors: if preserve_errors:
# We do this once to remove a top level dependency on the validate module # We do this once to remove a top level dependency on the validate module
# Which makes importing configobj faster # Which makes importing configobj faster
from validate import VdtMissingValue from configobj.validate import VdtMissingValue
self._vdtMissingValue = VdtMissingValue self._vdtMissingValue = VdtMissingValue
section = self section = self
@ -2189,12 +2174,12 @@ class ConfigObj(Section):
val, val,
missing=missing missing=missing
) )
except validator.baseErrorClass, e: except validator.baseErrorClass as cause:
if not preserve_errors or isinstance(e, self._vdtMissingValue): if not preserve_errors or isinstance(cause, self._vdtMissingValue):
out[entry] = False out[entry] = False
else: else:
# preserve the error # preserve the error
out[entry] = e out[entry] = cause
ret_false = False ret_false = False
ret_true = False ret_true = False
else: else:
@ -2338,7 +2323,7 @@ class ConfigObj(Section):
This method raises a ``ReloadError`` if the ConfigObj doesn't have This method raises a ``ReloadError`` if the ConfigObj doesn't have
a filename attribute pointing to a file. a filename attribute pointing to a file.
""" """
if not isinstance(self.filename, basestring): if not isinstance(self.filename, six.string_types):
raise ReloadError() raise ReloadError()
filename = self.filename filename = self.filename
@ -2416,16 +2401,16 @@ def flatten_errors(cfg, res, levels=None, results=None):
levels = [] levels = []
results = [] results = []
if res == True: if res == True:
return results return sorted(results)
if res == False or isinstance(res, Exception): if res == False or isinstance(res, Exception):
results.append((levels[:], None, res)) results.append((levels[:], None, res))
if levels: if levels:
levels.pop() levels.pop()
return results return sorted(results)
for (key, val) in res.items(): for (key, val) in list(res.items()):
if val == True: if val == True:
continue continue
if isinstance(cfg.get(key), dict): if isinstance(cfg.get(key), Mapping):
# Go down one level # Go down one level
levels.append(key) levels.append(key)
flatten_errors(cfg[key], val, levels, results) flatten_errors(cfg[key], val, levels, results)
@ -2436,7 +2421,7 @@ def flatten_errors(cfg, res, levels=None, results=None):
if levels: if levels:
levels.pop() levels.pop()
# #
return results return sorted(results)
def get_extra_values(conf, _prepend=()): def get_extra_values(conf, _prepend=()):

View file

@ -0,0 +1,2 @@
"""Project version"""
__version__ = '5.1.0'

1474
lib/configobj/validate.py Normal file

File diff suppressed because it is too large Load diff