/psychopy/contrib/configobj/__init__.py
Python | 1488 lines | 1397 code | 34 blank | 57 comment | 11 complexity | 325b6db3508496f8e53952fa77922cbd MD5 | raw file
- # configobj.py
- # -*- coding: utf-8 -*-
- # pylint: disable=bad-continuation
- """A config file reader/writer that supports nested sections in config files."""
- # 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
- # 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
- 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
- # imported lazily to avoid startup performance hit if it isn't used
- compiler = None
- # A dictionary mapping BOM to
- # the encoding to decode with, and what to set the
- # encoding attribute to.
- BOMS = {
- BOM_UTF8: ('utf_8', None),
- BOM_UTF16_BE: ('utf16_be', 'utf_16'),
- BOM_UTF16_LE: ('utf16_le', 'utf_16'),
- BOM_UTF16: ('utf_16', 'utf_16'),
- }
- # All legal variants of the BOM codecs.
- # TODO: the list of aliases is not meant to be exhaustive, is there a
- # better way ?
- BOM_LIST = {
- 'utf_16': 'utf_16',
- 'u16': 'utf_16',
- 'utf16': 'utf_16',
- 'utf-16': 'utf_16',
- 'utf16_be': 'utf16_be',
- 'utf_16_be': 'utf16_be',
- 'utf-16be': 'utf16_be',
- 'utf16_le': 'utf16_le',
- 'utf_16_le': 'utf16_le',
- 'utf-16le': 'utf16_le',
- 'utf_8': 'utf_8',
- 'u8': 'utf_8',
- 'utf': 'utf_8',
- 'utf8': 'utf_8',
- 'utf-8': 'utf_8',
- }
- # Map of encodings to the BOM to write.
- BOM_SET = {
- 'utf_8': BOM_UTF8,
- 'utf_16': BOM_UTF16,
- 'utf16_be': BOM_UTF16_BE,
- 'utf16_le': BOM_UTF16_LE,
- None: BOM_UTF8
- }
- def match_utf8(encoding):
- return BOM_LIST.get(encoding.lower()) == 'utf_8'
- # Quote strings used for writing values
- squot = "'%s'"
- dquot = '"%s"'
- noquot = "%s"
- wspace_plus = ' \r\n\v\t\'"'
- tsquot = '"""%s"""'
- tdquot = "'''%s'''"
- # Sentinel for use in getattr calls to replace hasattr
- MISSING = object()
- __all__ = (
- 'DEFAULT_INDENT_TYPE',
- 'DEFAULT_INTERPOLATION',
- 'ConfigObjError',
- 'NestingError',
- 'ParseError',
- 'DuplicateError',
- 'ConfigspecError',
- 'ConfigObj',
- 'SimpleVal',
- 'InterpolationError',
- 'InterpolationLoopError',
- 'MissingInterpolationOption',
- 'RepeatSectionError',
- 'ReloadError',
- 'UnreprError',
- 'UnknownType',
- 'flatten_errors',
- 'get_extra_values'
- )
- DEFAULT_INTERPOLATION = 'configparser'
- DEFAULT_INDENT_TYPE = ' '
- MAX_INTERPOL_DEPTH = 10
- OPTION_DEFAULTS = {
- 'interpolation': True,
- 'raise_errors': False,
- 'list_values': True,
- 'create_empty': False,
- 'file_error': False,
- 'configspec': None,
- 'stringify': True,
- # option may be set to one of ('', ' ', '\t')
- 'indent_type': None,
- 'encoding': None,
- 'default_encoding': None,
- 'unrepr': False,
- 'write_empty_values': False,
- }
- # this could be replaced if six is used for compatibility, or there are no
- # more assertions about items being a string
- def getObj(s):
- global compiler
- if compiler is None:
- import compiler
- s = "a=" + s
- p = compiler.parse(s)
- return p.getChildren()[1].getChildren()[0].getChildren()[1]
- class UnknownType(Exception):
- pass
- def unrepr(s):
- if not s:
- return s
- # this is supposed to be safe
- import ast
- return ast.literal_eval(s)
- class ConfigObjError(SyntaxError):
- """
- This is the base class for all errors that ConfigObj raises.
- It is a subclass of SyntaxError.
- """
- def __init__(self, message='', line_number=None, line=''):
- self.line = line
- self.line_number = line_number
- SyntaxError.__init__(self, message)
- class NestingError(ConfigObjError):
- """
- This error indicates a level of nesting that doesn't match.
- """
- class ParseError(ConfigObjError):
- """
- This error indicates that a line is badly written.
- It is neither a valid ``key = value`` line,
- nor a valid section marker line.
- """
- class ReloadError(IOError):
- """
- A 'reload' operation failed.
- This exception is a subclass of ``IOError``.
- """
- def __init__(self):
- IOError.__init__(self, 'reload failed, filename is not set.')
- class DuplicateError(ConfigObjError):
- """
- The keyword or section specified already exists.
- """
- class ConfigspecError(ConfigObjError):
- """
- An error occured whilst parsing a configspec.
- """
- class InterpolationError(ConfigObjError):
- """Base class for the two interpolation errors."""
- class InterpolationLoopError(InterpolationError):
- """Maximum interpolation depth exceeded in string interpolation."""
- def __init__(self, option):
- InterpolationError.__init__(
- self,
- 'interpolation loop detected in value "%s".' % option)
- class RepeatSectionError(ConfigObjError):
- """
- This error indicates additional sections in a section with a
- ``__many__`` (repeated) section.
- """
- class MissingInterpolationOption(InterpolationError):
- """A value specified for interpolation was missing."""
- def __init__(self, option):
- msg = 'missing option "%s" in interpolation.' % option
- InterpolationError.__init__(self, msg)
- class UnreprError(ConfigObjError):
- """An error parsing in unrepr mode."""
- class InterpolationEngine(object):
- """
- A helper class to help perform string interpolation.
- This class is an abstract base class; its descendants perform
- the actual work.
- """
- # compiled regexp to use in self.interpolate()
- _KEYCRE = re.compile(r"%\(([^)]*)\)s")
- _cookie = '%'
- def __init__(self, section):
- # the Section instance that "owns" this engine
- self.section = section
- def interpolate(self, key, value):
- # short-cut
- if not self._cookie in value:
- return value
- def recursive_interpolate(key, value, section, backtrail):
- """The function that does the actual work.
- ``value``: the string we're trying to interpolate.
- ``section``: the section in which that string was found
- ``backtrail``: a dict to keep track of where we've been,
- to detect and prevent infinite recursion loops
- This is similar to a depth-first-search algorithm.
- """
- # Have we been here already?
- if (key, section.name) in backtrail:
- # Yes - infinite loop detected
- raise InterpolationLoopError(key)
- # Place a marker on our backtrail so we won't come back here again
- backtrail[(key, section.name)] = 1
- # Now start the actual work
- match = self._KEYCRE.search(value)
- while match:
- # The actual parsing of the match is implementation-dependent,
- # so delegate to our helper function
- k, v, s = self._parse_match(match)
- if k is None:
- # That's the signal that no further interpolation is needed
- replacement = v
- else:
- # Further interpolation may be needed to obtain final value
- replacement = recursive_interpolate(k, v, s, backtrail)
- # Replace the matched string with its final value
- start, end = match.span()
- value = ''.join((value[:start], replacement, value[end:]))
- new_search_start = start + len(replacement)
- # Pick up the next interpolation key, if any, for next time
- # through the while loop
- match = self._KEYCRE.search(value, new_search_start)
- # Now safe to come back here again; remove marker from backtrail
- del backtrail[(key, section.name)]
- return value
- # Back in interpolate(), all we have to do is kick off the recursive
- # function with appropriate starting values
- value = recursive_interpolate(key, value, self.section, {})
- return value
- def _fetch(self, key):
- """Helper function to fetch values from owning section.
- Returns a 2-tuple: the value, and the section where it was found.
- """
- # switch off interpolation before we try and fetch anything !
- save_interp = self.section.main.interpolation
- self.section.main.interpolation = False
- # Start at section that "owns" this InterpolationEngine
- current_section = self.section
- while True:
- # try the current section first
- val = current_section.get(key)
- if val is not None and not isinstance(val, Section):
- break
- # try "DEFAULT" next
- val = current_section.get('DEFAULT', {}).get(key)
- if val is not None and not isinstance(val, Section):
- break
- # move up to parent and try again
- # top-level's parent is itself
- if current_section.parent is current_section:
- # reached top level, time to give up
- break
- current_section = current_section.parent
- # restore interpolation to previous value before returning
- self.section.main.interpolation = save_interp
- if val is None:
- raise MissingInterpolationOption(key)
- return val, current_section
- def _parse_match(self, match):
- """Implementation-dependent helper function.
- Will be passed a match object corresponding to the interpolation
- key we just found (e.g., "%(foo)s" or "$foo"). Should look up that
- key in the appropriate config file section (using the ``_fetch()``
- helper function) and return a 3-tuple: (key, value, section)
- ``key`` is the name of the key we're looking for
- ``value`` is the value found for that key
- ``section`` is a reference to the section where it was found
- ``key`` and ``section`` should be None if no further
- interpolation should be performed on the resulting value
- (e.g., if we interpolated "$$" and returned "$").
- """
- raise NotImplementedError()
- class ConfigParserInterpolation(InterpolationEngine):
- """Behaves like ConfigParser."""
- _cookie = '%'
- _KEYCRE = re.compile(r"%\(([^)]*)\)s")
- def _parse_match(self, match):
- key = match.group(1)
- value, section = self._fetch(key)
- return key, value, section
- class TemplateInterpolation(InterpolationEngine):
- """Behaves like string.Template."""
- _cookie = '$'
- _delimiter = '$'
- _KEYCRE = re.compile(r"""
- \$(?:
- (?P<escaped>\$) | # Two $ signs
- (?P<named>[_a-z][_a-z0-9]*) | # $name format
- {(?P<braced>[^}]*)} # ${name} format
- )
- """, re.IGNORECASE | re.VERBOSE)
- def _parse_match(self, match):
- # Valid name (in or out of braces): fetch value from section
- key = match.group('named') or match.group('braced')
- if key is not None:
- value, section = self._fetch(key)
- return key, value, section
- # Escaped delimiter (e.g., $$): return single delimiter
- if match.group('escaped') is not None:
- # Return None for key and section to indicate it's time to stop
- return None, self._delimiter, None
- # Anything else: ignore completely, just return it unchanged
- return None, match.group(), None
- interpolation_engines = {
- 'configparser': ConfigParserInterpolation,
- 'template': TemplateInterpolation,
- }
- def __newobj__(cls, *args):
- # Hack for pickle
- return cls.__new__(cls, *args)
- class Section(dict):
- """
- A dictionary-like object that represents a section in a config file.
- It does string interpolation if the 'interpolation' attribute
- of the 'main' object is set to True.
- Interpolation is tried first from this object, then from the 'DEFAULT'
- section of this object, next from the parent and its 'DEFAULT' section,
- and so on until the main object is reached.
- A Section will behave like an ordered dictionary - following the
- order of the ``scalars`` and ``sections`` attributes.
- You can use this to change the order of members.
- Iteration follows the order: scalars, then sections.
- """
- def __setstate__(self, state):
- dict.update(self, state[0])
- self.__dict__.update(state[1])
- def __reduce__(self):
- state = (dict(self), self.__dict__)
- return (__newobj__, (self.__class__,), state)
- def __init__(self, parent, depth, main, indict=None, name=None):
- """
- * parent is the section above
- * depth is the depth level of this section
- * main is the main ConfigObj
- * indict is a dictionary to initialise the section with
- """
- if indict is None:
- indict = {}
- dict.__init__(self)
- # used for nesting level *and* interpolation
- self.parent = parent
- # used for the interpolation attribute
- self.main = main
- # level of nesting depth of this Section
- self.depth = depth
- # purely for information
- self.name = name
- #
- self._initialise()
- # we do this explicitly so that __setitem__ is used properly
- # (rather than just passing to ``dict.__init__``)
- for entry, value in indict.items():
- self[entry] = value
- def _initialise(self):
- # the sequence of scalar values in this Section
- self.scalars = []
- # the sequence of sections in this Section
- self.sections = []
- # for comments :-)
- self.comments = {}
- self.inline_comments = {}
- # the configspec
- self.configspec = None
- # for defaults
- self.defaults = []
- self.default_values = {}
- self.extra_values = []
- self._created = False
- def _interpolate(self, key, value):
- try:
- # do we already have an interpolation engine?
- engine = self._interpolation_engine
- except AttributeError:
- # not yet: first time running _interpolate(), so pick the engine
- name = self.main.interpolation
- if name == True: # note that "if name:" would be incorrect here
- # backwards-compatibility: interpolation=True means use default
- name = DEFAULT_INTERPOLATION
- name = name.lower() # so that "Template", "template", etc. all work
- class_ = interpolation_engines.get(name, None)
- if class_ is None:
- # invalid value for self.main.interpolation
- self.main.interpolation = False
- return value
- else:
- # save reference to engine so we don't have to do this again
- engine = self._interpolation_engine = class_(self)
- # let the engine do the actual work
- return engine.interpolate(key, value)
- def __getitem__(self, key):
- """Fetch the item and do string interpolation."""
- val = dict.__getitem__(self, key)
- if self.main.interpolation:
- if isinstance(val, six.string_types):
- return self._interpolate(key, val)
- if isinstance(val, list):
- def _check(entry):
- if isinstance(entry, six.string_types):
- return self._interpolate(key, entry)
- return entry
- new = [_check(entry) for entry in val]
- if new != val:
- return new
- return val
- def __setitem__(self, key, value, unrepr=False):
- """
- Correctly set a value.
- Making dictionary values Section instances.
- (We have to special case 'Section' instances - which are also dicts)
- Keys must be strings.
- Values need only be strings (or lists of strings) if
- ``main.stringify`` is set.
- ``unrepr`` must be set when setting a value to a dictionary, without
- creating a new sub-section.
- """
- if not isinstance(key, six.string_types):
- raise ValueError('The key "%s" is not a string.' % key)
- # add the comment
- if key not in self.comments:
- self.comments[key] = []
- self.inline_comments[key] = ''
- # remove the entry from defaults
- if key in self.defaults:
- self.defaults.remove(key)
- #
- if isinstance(value, Section):
- if key not in self:
- self.sections.append(key)
- dict.__setitem__(self, key, value)
- elif isinstance(value, Mapping) and not unrepr:
- # First create the new depth level,
- # then create the section
- if key not in self:
- self.sections.append(key)
- new_depth = self.depth + 1
- dict.__setitem__(
- self,
- key,
- Section(
- self,
- new_depth,
- self.main,
- indict=value,
- name=key))
- else:
- if key not in self:
- self.scalars.append(key)
- if not self.main.stringify:
- if isinstance(value, six.string_types):
- pass
- elif isinstance(value, (list, tuple)):
- for entry in value:
- 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)
- dict.__setitem__(self, key, value)
- def __delitem__(self, key):
- """Remove items from the sequence when deleting."""
- dict. __delitem__(self, key)
- if key in self.scalars:
- self.scalars.remove(key)
- else:
- self.sections.remove(key)
- del self.comments[key]
- del self.inline_comments[key]
- def get(self, key, default=None):
- """A version of ``get`` that doesn't bypass string interpolation."""
- try:
- return self[key]
- except KeyError:
- return default
- def update(self, indict):
- """
- A version of update that uses our ``__setitem__``.
- """
- for entry in indict:
- self[entry] = indict[entry]
- def pop(self, key, default=MISSING):
- """
- 'D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
- If key is not found, d is returned if given, otherwise KeyError is raised'
- """
- try:
- val = self[key]
- except KeyError:
- if default is MISSING:
- raise
- val = default
- else:
- del self[key]
- return val
- def popitem(self):
- """Pops the first (key,val)"""
- sequence = (self.scalars + self.sections)
- if not sequence:
- raise KeyError(": 'popitem(): dictionary is empty'")
- key = sequence[0]
- val = self[key]
- del self[key]
- return key, val
- def clear(self):
- """
- A version of clear that also affects scalars/sections
- Also clears comments and configspec.
- Leaves other attributes alone :
- depth/main/parent are not affected
- """
- dict.clear(self)
- self.scalars = []
- self.sections = []
- self.comments = {}
- self.inline_comments = {}
- self.configspec = None
- self.defaults = []
- self.extra_values = []
- def setdefault(self, key, default=None):
- """A version of setdefault that sets sequence if appropriate."""
- try:
- return self[key]
- except KeyError:
- self[key] = default
- return self[key]
- def items(self):
- """D.items() -> list of D's (key, value) pairs, as 2-tuples"""
- return [(key, self[key]) for key in self.keys()]
- def keys(self):
- """D.keys() -> list of D's keys"""
- return self.scalars + self.sections
- def values(self):
- """D.values() -> list of D's values"""
- return [self[key] for key in self.keys()]
- def iteritems(self):
- """D.iteritems() -> an iterator over the (key, value) items of D"""
- return iter(self.items())
- def iterkeys(self):
- """D.iterkeys() -> an iterator over the keys of D"""
- return iter(self.keys())
- __iter__ = iterkeys
- def itervalues(self):
- """D.itervalues() -> an iterator over the values of D"""
- return iter(self.values())
- def __repr__(self):
- """x.__repr__() <==> repr(x)"""
- def _getval(key):
- try:
- return self[key]
- except MissingInterpolationOption:
- return dict.__getitem__(self, key)
- return '{%s}' % ', '.join([('{}: {}'.format(repr(key), repr(_getval(key))))
- for key in (self.scalars + self.sections)])
- __str__ = __repr__
- __str__.__doc__ = "x.__str__() <==> str(x)"
- # Extra methods - not in a normal dictionary
- def dict(self):
- """
- Return a deepcopy of self as a dictionary.
- All members that are ``Section`` instances are recursively turned to
- ordinary dictionaries - by calling their ``dict`` method.
- >>> n = a.dict()
- >>> n == a
- 1
- >>> n is a
- 0
- """
- newdict = {}
- for entry in self:
- this_entry = self[entry]
- if isinstance(this_entry, Section):
- this_entry = this_entry.dict()
- elif isinstance(this_entry, list):
- # create a copy rather than a reference
- this_entry = list(this_entry)
- elif isinstance(this_entry, tuple):
- # create a copy rather than a reference
- this_entry = tuple(this_entry)
- newdict[entry] = this_entry
- return newdict
- def merge(self, indict, 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]]
- ... more_options = False
- ... # end of file'''.splitlines()
- >>> b = '''# File is user.ini
- ... [section1]
- ... option1 = False
- ... # end of file'''.splitlines()
- >>> c1 = ConfigObj(b)
- >>> c2 = ConfigObj(a)
- >>> c2.merge(c1)
- >>> c2
- ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}})
- """
- for key, val in indict.items():
- if 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
- def rename(self, oldkey, newkey):
- """
- Change a keyname to another, without changing position in sequence.
- Implemented so that transformations can be made on keys,
- as well as on values. (used by encode and decode)
- Also renames comments.
- """
- if oldkey in self.scalars:
- the_list = self.scalars
- elif oldkey in self.sections:
- the_list = self.sections
- else:
- raise KeyError('Key "%s" not found.' % oldkey)
- pos = the_list.index(oldkey)
- #
- val = self[oldkey]
- dict.__delitem__(self, oldkey)
- dict.__setitem__(self, newkey, val)
- the_list.remove(oldkey)
- the_list.insert(pos, newkey)
- comm = self.comments[oldkey]
- inline_comment = self.inline_comments[oldkey]
- del self.comments[oldkey]
- del self.inline_comments[oldkey]
- self.comments[newkey] = comm
- self.inline_comments[newkey] = inline_comment
- def walk(self, function, raise_errors=True,
- call_on_sections=False, **keywargs):
- """
- Walk every member and call a function on the keyword and value.
- Return a dictionary of the return values
- If the function raises an exception, raise the errror
- unless ``raise_errors=False``, in which case set the return value to
- ``False``.
- Any unrecognised keyword arguments you pass to walk, will be pased on
- to the function you pass in.
- Note: if ``call_on_sections`` is ``True`` then - on encountering a
- subsection, *first* the function is called for the *whole* subsection,
- and then recurses into it's members. This means your function must be
- able to handle strings, dictionaries and lists. This allows you
- to change the key of subsections as well as for ordinary members. The
- return value when called on the whole subsection has to be discarded.
- See the encode and decode methods for examples, including functions.
- .. admonition:: caution
- You can use ``walk`` to transform the names of members of a section
- but you mustn't add or delete members.
- >>> config = '''[XXXXsection]
- ... XXXXkey = XXXXvalue'''.splitlines()
- >>> cfg = ConfigObj(config)
- >>> cfg
- ConfigObj({'XXXXsection': {'XXXXkey': 'XXXXvalue'}})
- >>> def transform(section, key):
- ... val = section[key]
- ... newkey = key.replace('XXXX', 'CLIENT1')
- ... section.rename(key, newkey)
- ... if isinstance(val, (tuple, list, dict)):
- ... pass
- ... else:
- ... val = val.replace('XXXX', 'CLIENT1')
- ... section[newkey] = val
- >>> cfg.walk(transform, call_on_sections=True)
- {'CLIENT1section': {'CLIENT1key': None}}
- >>> cfg
- ConfigObj({'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}})
- """
- out = {}
- # scalars first
- for i in range(len(self.scalars)):
- entry = self.scalars[i]
- try:
- val = function(self, entry, **keywargs)
- # bound again in case name has changed
- entry = self.scalars[i]
- out[entry] = val
- except Exception:
- if raise_errors:
- raise
- else:
- entry = self.scalars[i]
- out[entry] = False
- # then sections
- for i in range(len(self.sections)):
- entry = self.sections[i]
- if call_on_sections:
- try:
- function(self, entry, **keywargs)
- except Exception:
- if raise_errors:
- raise
- else:
- entry = self.sections[i]
- out[entry] = False
- # bound again in case name has changed
- entry = self.sections[i]
- # previous result is discarded
- out[entry] = self[entry].walk(
- function,
- raise_errors=raise_errors,
- call_on_sections=call_on_sections,
- **keywargs)
- return out
- def as_bool(self, key):
- """
- Accepts a key as input. The corresponding value must be a string or
- the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to
- retain compatibility with Python 2.2.
- If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns
- ``True``.
- If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns
- ``False``.
- ``as_bool`` is not case sensitive.
- Any other input will raise a ``ValueError``.
- >>> a = ConfigObj()
- >>> a['a'] = 'fish'
- >>> a.as_bool('a')
- Traceback (most recent call last):
- ValueError: Value "fish" is neither True nor False
- >>> a['b'] = 'True'
- >>> a.as_bool('b')
- 1
- >>> a['b'] = 'off'
- >>> a.as_bool('b')
- 0
- """
- val = self[key]
- if val == True:
- return True
- elif val == False:
- return False
- else:
- try:
- if not isinstance(val, six.string_types):
- # TODO: Why do we raise a KeyError here?
- raise KeyError()
- else:
- return self.main._bools[val.lower()]
- except KeyError:
- raise ValueError('Value "%s" is neither True nor False' % val)
- def as_int(self, key):
- """
- A convenience method which coerces the specified value to an integer.
- If the value is an invalid literal for ``int``, a ``ValueError`` will
- be raised.
- >>> a = ConfigObj()
- >>> a['a'] = 'fish'
- >>> a.as_int('a')
- Traceback (most recent call last):
- ValueError: invalid literal for int() with base 10: 'fish'
- >>> a['b'] = '1'
- >>> a.as_int('b')
- 1
- >>> a['b'] = '3.2'
- >>> a.as_int('b')
- Traceback (most recent call last):
- ValueError: invalid literal for int() with base 10: '3.2'
- """
- return int(self[key])
- def as_float(self, key):
- """
- A convenience method which coerces the specified value to a float.
- If the value is an invalid literal for ``float``, a ``ValueError`` will
- be raised.
- >>> a = ConfigObj()
- >>> a['a'] = 'fish'
- >>> a.as_float('a') #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') #doctest: +ELLIPSIS
- 3.2...
- """
- return float(self[key])
- def as_list(self, key):
- """
- A convenience method which fetches the specified value, guaranteeing
- that it is a list.
- >>> a = ConfigObj()
- >>> a['a'] = 1
- >>> a.as_list('a')
- [1]
- >>> a['a'] = (1,)
- >>> a.as_list('a')
- [1]
- >>> a['a'] = [1]
- >>> a.as_list('a')
- [1]
- """
- result = self[key]
- if isinstance(result, (tuple, list)):
- return list(result)
- return [result]
- def restore_default(self, key):
- """
- Restore (and return) default value for the specified key.
- This method will only work for a ConfigObj that was created
- with a configspec and has been validated.
- If there is no default value for this key, ``KeyError`` is raised.
- """
- default = self.default_values[key]
- dict.__setitem__(self, key, default)
- if key not in self.defaults:
- self.defaults.append(key)
- return default
- def restore_defaults(self):
- """
- Recursively restore default values to all members
- that have them.
- This method will only work for a ConfigObj that was created
- with a configspec and has been validated.
- It doesn't delete or modify entries without default values.
- """
- for key in self.default_values:
- self.restore_default(key)
- for section in self.sections:
- self[section].restore_defaults()
- 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
- (?:".*?")| # double quotes
- (?:'.*?')| # single quotes
- (?:[^'"=].*?) # no quotes
- )
- \s*=\s* # divider
- (.*) # value (including list values and comments)
- $ # line end
- ''',
- re.VERBOSE)
- _sectionmarker = re.compile(r'''^
- (\s*) # 1: indentation
- ((?:\[\s*)+) # 2: section marker open
- ( # 3: section name open
- (?:"\s*\S.*?\s*")| # at least one non-space with double quotes
- (?:'\s*\S.*?\s*')| # at least one non-space with single quotes
- (?:[^'"\s].*?) # at least one non-space unquoted
- ) # section name close
- ((?:\s*\])+) # 4: section marker close
- (\s*(?:\#.*)?)? # 5: optional comment
- $''',
- re.VERBOSE)
- # this regexp pulls list values out as a single string
- # or single values and comments
- # FIXME: this regex adds a '' to the end of comma terminated lists
- # workaround in ``_handle_value``
- _valueexp = re.compile(r'''^
- (?:
- (?:
- (
- (?:
- (?:
- (?:".*?")| # double quotes
- (?:'.*?')| # single quotes
- (?:[^'",\#][^,\#]*?) # unquoted
- )
- \s*,\s* # comma
- )* # match all list items ending in a comma (if any)
- )
- (
- (?:".*?")| # double quotes
- (?:'.*?')| # single quotes
- (?:[^'",\#\s][^,]*?)| # unquoted
- (?:(?<!,)) # Empty value
- )? # last item in a list - or string value
- )|
- (,) # alternatively a single comma - empty list
- )
- (\s*(?:\#.*)?)? # optional comment
- $''',
- re.VERBOSE)
- # use findall to get the members of a list value
- _listvalueexp = re.compile(r'''
- (
- (?:".*?")| # double quotes
- (?:'.*?')| # single quotes
- (?:[^'",\#]?.*?) # unquoted
- )
- \s*,\s* # comma
- ''',
- re.VERBOSE)
- # this regexp is used for the value
- # when lists are switched off
- _nolistvalue = re.compile(r'''^
- (
- (?:".*?")| # double quotes
- (?:'.*?')| # single quotes
- (?:[^'"\#].*?)| # unquoted
- (?:) # Empty value
- )
- (\s*(?:\#.*)?)? # optional comment
- $''',
- re.VERBOSE)
- # regexes for finding triple quoted values on one line
- _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),
- '"""': (_single_line_double, _multi_line_double),
- }
- # Used by the ``istrue`` Section method
- _bools = {
- 'yes': True, 'no': False,
- 'on': True, 'off': False,
- '1': True, '0': False,
- 'true': True, 'false': False,
- }
- def __init__(self, infile=None, options=None, configspec=None, encoding=None,
- interpolation=True, raise_errors=False, list_values=True,
- create_empty=False, file_error=False, stringify=True,
- indent_type=None, default_encoding=None, unrepr=False,
- write_empty_values=False, _inspec=False):
- """
- Parse a config file or create a config file object.
- ``ConfigObj(infile=None, configspec=None, encoding=None,
- interpolation=True, raise_errors=False, list_values=True,
- create_empty=False, file_error=False, stringify=True,
- indent_type=None, default_encoding=None, unrepr=False,
- write_empty_values=False, _inspec=False)``
- """
- self._inspec = _inspec
- # init the superclass
- Section.__init__(self, self, 0, self)
- infile = infile or []
- _options = {'configspec': configspec,
- 'encoding': encoding, 'interpolation': interpolation,
- 'raise_errors': raise_errors, 'list_values': list_values,
- 'create_empty': create_empty, 'file_error': file_error,
- 'stringify': stringify, 'indent_type': indent_type,
- 'default_encoding': default_encoding, 'unrepr': unrepr,
- 'write_empty_values': write_empty_values}
- if options is None:
- options = _options
- else:
- import warnings
- warnings.warn('Passing in an options dictionary to ConfigObj() is '
- 'deprecated. Use **options instead.',
- DeprecationWarning)
- # TODO: check the values too.
- for entry in options:
- if entry not in OPTION_DEFAULTS:
- raise TypeError('Unrecognised option "%s".' % entry)
- for entry, value in list(OPTION_DEFAULTS.items()):
- if entry not in options:
- options[entry] = value
- keyword_value = _options[entry]
- if value != keyword_value:
- options[entry] = keyword_value
- # XXXX this ignores an explicit list_values = True in combination
- # with _inspec. The user should *never* do that anyway, but still...
- if _inspec:
- options['list_values'] = False
- self._initialise(options)
- configspec = options['configspec']
- self._original_configspec = configspec
- self._load(infile, configspec)
- def _load(self, infile, configspec):
- try:
- infile = infile.__fspath__()
- except AttributeError:
- pass
- if isinstance(infile, six.string_types):
- self.filename = infile
- if os.path.isfile(infile):
- 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)
- else:
- # file doesn't already exist
- if self.create_empty:
- # this is a good test that the filename specified
- # isn't impossible - like on a non-existent device
- with open(infile, 'w') as h:
- h.write('')
- content = []
- elif isinstance(infile, (list, tuple)):
- content = list(infile)
- elif isinstance(infile, dict):
- # initialise self
- # the Section class handles creating subsections
- if isinstance(infile, ConfigObj):
- # get a copy of our ConfigObj
- def set_section(in_section, this_section):
- for entry in in_section.scalars:
- this_section[entry] = in_section[entry]
- for section in in_section.sections:
- this_section[section] = {}
- set_section(in_section[section], this_section[section])
- set_section(infile, self)
- else:
- for entry in infile:
- self[entry] = infile[entry]
- del self._errors
- if configspec is not None:
- self._handle_configspec(configspec)
- else:
- self.configspec = None
- return
- elif getattr(infile, 'read', MISSING) is not MISSING:
- # This supports file like objects
- 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 path-like object, file like object, or list of lines.')
- if content:
- # don't do it for the empty ConfigObj
- 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 content:
- if (not line) or (line[-1] not in ('\r', '\n')):
- continue
- for end in ('\r\n', '\n', '\r'):
- if line.endswith(end):
- self.newlines = end
- break
- break
- assert all(isinstance(line, six.string_types) for line in content), repr(content)
- content = [line.rstrip('\r\n') for line in content]
- self._parse(content)
- # if we had any errors, now is the time to raise them
- if self._errors:
- if len(self._errors) > 1:
- 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:
- # (error_type, message, line_number)
- error.errors = self._errors
- # set the config attribute
- error.config = self
- raise error
- # delete private attributes
- del self._errors
- if configspec is None:
- self.configspec = None
- else:
- self._handle_configspec(configspec)
- def _initialise(self, options=None):
- if options is None:
- options = OPTION_DEFAULTS
- # initialise a few variables
- self.filename = None
- self._errors = []
- self.raise_errors = options['raise_errors']
- self.interpolation = options['interpolation']
- self.list_values = options['list_values']
- self.create_empty = options['create_empty']
- self.file_error = options['file_error']
- self.stringify = options['stringify']
- self.indent_type = options['indent_type']
- self.encoding = options['encoding']
- self.default_encoding = options['default_encoding']
- self.BOM = False
- self.newlines = None
- self.write_empty_values = options['write_empty_values']
- self.unrepr = options['unrepr']
- self.initial_comment = []
- self.final_comment = []
- self.configspec = None
- if self._inspec:
- self.list_values = False
- # Clear section attributes as well
- Section._initialise(self)
- def __repr__(self):
- def _getval(key):
- try:
- return self[key]
- except MissingInterpolationOption:
- return dict.__getitem__(self, key)
- return ('{}({{{}}})'.format(self.__class__.__name__,
- ', '.join([('{}: {}'.format(repr(key), repr(_getval(key))))
- for key in (self.scalars + self.sections)])))
- def _handle_bom(self, infile):
- """
- Handle any BOM, and decode if necessary.
- If an encoding is specified, that *must* be used - but the BOM should
- still be removed (and the BOM attribute set).
- (If the encoding is wrongly specified, then a BOM for an alternative
- encoding won't be discovered or removed.)
- If an encoding is not specified, UTF8 or UTF16 BOM will be detected and
- removed. The BOM attribute will be set. UTF16 will be decoded to
- unicode.
- NOTE: This method must not be called with an empty ``infile``.
- Specifying the *wrong* encoding is likely to cause a
- ``UnicodeDecodeError``.
- ``infile`` must always be returned as a list of lines, but may be
- passed in as a single string.
- """
- if ((self.encoding is not None) and
- (self.encoding.lower() not in BOM_LIST)):
- # No need to check for a BOM
- # the encoding specified doesn't have one
- # just decode
- return self._decode(infile, self.encoding)
- if isinstance(infile, (list, tuple)):
- line = infile[0]
- else:
- line = infile
- if isinstance(line, six.text_type):
- # it's already decoded and there's no need to do anything
- # else, just use the _decode utility method to handle
- # listifying appropriately
- return self._decode(infile, self.encoding)
- if self.encoding is not None:
- # encoding explicitly supplied
- # And it could have an associated BOM
- # TODO: if encoding is just UTF16 - we ought to check for both
- # TODO: big endian and little endian versions.
- enc = BOM_LIST[self.encoding.lower()]
- if enc == 'utf_16':
- # For UTF16 we try big endian and little endian
- for BOM, (encoding, final_encoding) in list(BOMS.items()):
- if not final_encoding:
- # skip UTF8
- continue
- if infile.startswith(BOM):
- ### BOM discovered
- ##self.BOM = True
- # Don't need to remove BOM
- return self._decode(infile, encoding)
- # If we get this far, will *probably* raise a DecodeError
- # As it doesn't appear to start with a BOM
- return self._decode(infile, self.encoding)
- # Must be UTF8
- BOM = BOM_SET[enc]
- if not line.startswith(BOM):
- return self._decode(infile, self.encoding)
- newline = line[len(BOM):]
- # BOM removed
- if isinstance(infile, (list, tuple)):
- infile[0] = newline
- else:
- infile = newline
- self.BOM = True
- return self._decode(infile, self.encoding)
- # No encoding specified - so we need to check for UTF8/UTF16
- for BOM, (encoding, final_encoding) in 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
- self.encoding = final_encoding
- if not final_encoding:
- self.BOM = True
- # UTF8
- # remove BOM
- newline = line[len(BOM):]
- if isinstance(infile, (list, tuple)):
- infile[0] = newline
- else:
- infile = newline
- # 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 self._decode(infile, 'utf-8')
- # UTF16 - have to decode
- return self._decode(infile, encoding)
- if six.PY2 and isinstance(line, str):
- # don't actually do any decoding, since we're on python 2 and
- # returning a bytestring is fine
- return self._decode(infile, None)
- # No BOM discovered and no encoding specified, default to UTF-8
- if isinstance(infile, six.binary_type):
- return infile.decode('utf-8').splitlines(True)
- else:
- return self._decode(infile, 'utf-8')
- def _decode(self, infile, encoding):
- """
- Decode infile to unicode. Using the specified encoding.
- if is a string, it also needs converting to a list.
- """
- if isinstance(infile, six.binary_type):
- # NOTE: Could raise a ``UnicodeDecodeError``
- 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,