From: <ds...@us...> - 2007-07-27 15:45:29
|
Revision: 3623 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=3623&view=rev Author: dsdale Date: 2007-07-27 08:45:26 -0700 (Fri, 27 Jul 2007) Log Message: ----------- added config module, for testing a traited config system Modified Paths: -------------- trunk/matplotlib/lib/matplotlib/mpl-data/matplotlibrc Added Paths: ----------- trunk/matplotlib/lib/matplotlib/config/ trunk/matplotlib/lib/matplotlib/config/__init__.py trunk/matplotlib/lib/matplotlib/config/api.py trunk/matplotlib/lib/matplotlib/config/checkdep.py trunk/matplotlib/lib/matplotlib/config/configobj.py trunk/matplotlib/lib/matplotlib/config/configtest.py trunk/matplotlib/lib/matplotlib/config/cutils.py trunk/matplotlib/lib/matplotlib/config/matplotlib.conf.default trunk/matplotlib/lib/matplotlib/config/mplconfig.py trunk/matplotlib/lib/matplotlib/config/mpltraits.py trunk/matplotlib/lib/matplotlib/config/rcparams.py trunk/matplotlib/lib/matplotlib/config/rcsetup.py trunk/matplotlib/lib/matplotlib/config/tconfig.py trunk/matplotlib/lib/matplotlib/config/verbose.py trunk/matplotlib/lib/matplotlib/mpl-data/matplotlib.conf Added: trunk/matplotlib/lib/matplotlib/config/__init__.py =================================================================== --- trunk/matplotlib/lib/matplotlib/config/__init__.py (rev 0) +++ trunk/matplotlib/lib/matplotlib/config/__init__.py 2007-07-27 15:45:26 UTC (rev 3623) @@ -0,0 +1 @@ +# Please keep this file empty \ No newline at end of file Property changes on: trunk/matplotlib/lib/matplotlib/config/__init__.py ___________________________________________________________________ Name: svn:eol-style + native Added: trunk/matplotlib/lib/matplotlib/config/api.py =================================================================== --- trunk/matplotlib/lib/matplotlib/config/api.py (rev 0) +++ trunk/matplotlib/lib/matplotlib/config/api.py 2007-07-27 15:45:26 UTC (rev 3623) @@ -0,0 +1,12 @@ +""" +""" + +USE_NEW_CONFIG = True + +from rcparams import rc +from cutils import get_config_file + +if USE_NEW_CONFIG: + from mplconfig import rcParams, mplConfig, save_config +else: + from rcparams import rcParams Property changes on: trunk/matplotlib/lib/matplotlib/config/api.py ___________________________________________________________________ Name: svn:eol-style + native Added: trunk/matplotlib/lib/matplotlib/config/checkdep.py =================================================================== --- trunk/matplotlib/lib/matplotlib/config/checkdep.py (rev 0) +++ trunk/matplotlib/lib/matplotlib/config/checkdep.py 2007-07-27 15:45:26 UTC (rev 3623) @@ -0,0 +1,133 @@ +import os, re, sys +import distutils.version as version + +def dvipng(): + try: + stdin, stdout = os.popen4('dvipng -version') + line = stdout.readlines()[1] + v = line.split()[-1] + float(v) + return v + except (IndexError, ValueError): + return None + +def ghostscript(): + try: + if sys.platform == 'win32': + command = 'gswin32c -v' + else: + command = 'gs -v' + stdin, stdout = os.popen4(command) + line = stdout.readlines()[0] + v = line.split()[2] + vtest = '.'.join(v.split('.')[:2]) # deal with version numbers like '7.07.1' + float(vtest) + return vtest + except (IndexError, ValueError): + return None + +def tex(): + try: + stdin, stdout = os.popen4('tex -version') + line = stdout.readlines()[0] + pattern = '3\.1\d+' + match = re.search(pattern, line) + v = match.group(0) + float(v) + return v + except (IndexError, ValueError): + return None + +def pdftops(): + try: + stdin, stdout = os.popen4('pdftops -v') + for line in stdout.readlines(): + if 'version' in line: + v = line.split()[-1] + float(v) + return v + except (IndexError, ValueError): + return None + +def compare_versions(a, b): + "return True if a is greater than or equal to b" + if a: + a = version.LooseVersion(a) + b = version.LooseVersion(b) + if a>=b: return True + else: return False + else: return False + +def ps_distiller(s): + if not s: + return False + + flag = True + gs_req = '7.07' + gs_sugg = '7.07' + gs_v = ghostscript() + if compare_versions(gs_v, gs_sugg): pass + elif compare_versions(gs_v, gs_req): + verbose.report(('ghostscript-%s found. ghostscript-%s or later ' + 'is recommended to use the ps.usedistiller option.') %\ + (gs_v, gs_sugg)) + else: + flag = False + warnings.warn(('matplotlibrc ps.usedistiller option can not be used ' + 'unless ghostscript-%s or later is installed on your ' + 'system.') % gs_req) + + if s == 'xpdf': + pdftops_req = '3.0' + pdftops_v = pdftops() + if compare_versions(pdftops_v, pdftops_req): pass + else: + flag = False + warnings.warn(('matplotlibrc ps.usedistiller can not be set to ' + 'xpdf unless xpdf-%s or later is installed on your ' + 'system.') % pdftops_req) + + if flag: + return s + else: + return False + +def usetex(s): + if not s: + return False + + tex_req = '3.1415' + gs_req = '7.07' + gs_sugg = '7.07' + dvipng_req = '1.5' + flag = True + + tex_v = tex() + if compare_versions(tex_v, tex_req): pass + else: + flag = False + warnings.warn(('matplotlibrc text.usetex option can not be used ' + 'unless TeX-%s or later is ' + 'installed on your system') % tex_req) + + dvipng_v = dvipng() + if compare_versions(dvipng_v, dvipng_req): pass + else: + flag = False + warnings.warn( 'matplotlibrc text.usetex can not be used with *Agg ' + 'backend unless dvipng-1.5 or later is ' + 'installed on your system') + + gs_v = ghostscript() + if compare_versions(gs_v, gs_sugg): pass + elif compare_versions(gs_v, gs_req): + verbose.report(('ghostscript-%s found. ghostscript-%s or later is ' + 'recommended for use with the text.usetex ' + 'option.') % (gs_v, gs_sugg)) + else: + flag = False + warnings.warn(('matplotlibrc text.usetex can not be used ' + 'unless ghostscript-%s or later is ' + 'installed on your system') % gs_req) + + return flag \ No newline at end of file Property changes on: trunk/matplotlib/lib/matplotlib/config/checkdep.py ___________________________________________________________________ Name: svn:eol-style + native Added: trunk/matplotlib/lib/matplotlib/config/configobj.py =================================================================== --- trunk/matplotlib/lib/matplotlib/config/configobj.py (rev 0) +++ trunk/matplotlib/lib/matplotlib/config/configobj.py 2007-07-27 15:45:26 UTC (rev 3623) @@ -0,0 +1,2279 @@ +# configobj.py +# A config file reader/writer that supports nested sections in config files. +# Copyright (C) 2005-2006 Michael Foord, Nicola Larosa +# E-mail: fuzzyman AT voidspace DOT org DOT uk +# nico AT tekNico DOT net + +# ConfigObj 4 +# http://www.voidspace.org.uk/python/configobj.html + +# Released subject to the BSD License +# Please see http://www.voidspace.org.uk/python/license.shtml + +# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml +# For information about bugfixes, updates and support, please join the +# ConfigObj mailing list: +# http://lists.sourceforge.net/lists/listinfo/configobj-develop +# Comments, suggestions and bug reports welcome. + +from __future__ import generators + +import sys +INTP_VER = sys.version_info[:2] +if INTP_VER < (2, 2): + raise RuntimeError("Python v.2.2 or later needed") + +import os, re +compiler = None +try: + import compiler +except ImportError: + # for IronPython + pass +from types import StringTypes +from warnings import warn +try: + from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE +except ImportError: + # Python 2.2 does not have these + # UTF-8 + BOM_UTF8 = '\xef\xbb\xbf' + # UTF-16, little endian + BOM_UTF16_LE = '\xff\xfe' + # UTF-16, big endian + BOM_UTF16_BE = '\xfe\xff' + if sys.byteorder == 'little': + # UTF-16, native endianness + BOM_UTF16 = BOM_UTF16_LE + else: + # UTF-16, native endianness + BOM_UTF16 = BOM_UTF16_BE + +# A dictionary mapping BOM to +# the encoding to decode with, and what to set the +# encoding attribute to. +BOMS = { + BOM_UTF8: ('utf_8', None), + BOM_UTF16_BE: ('utf16_be', 'utf_16'), + BOM_UTF16_LE: ('utf16_le', 'utf_16'), + BOM_UTF16: ('utf_16', 'utf_16'), + } +# All legal variants of the BOM codecs. +# TODO: the list of aliases is not meant to be exhaustive, is there a +# better way ? +BOM_LIST = { + 'utf_16': 'utf_16', + 'u16': 'utf_16', + 'utf16': 'utf_16', + 'utf-16': 'utf_16', + 'utf16_be': 'utf16_be', + 'utf_16_be': 'utf16_be', + 'utf-16be': 'utf16_be', + 'utf16_le': 'utf16_le', + 'utf_16_le': 'utf16_le', + 'utf-16le': 'utf16_le', + 'utf_8': 'utf_8', + 'u8': 'utf_8', + 'utf': 'utf_8', + 'utf8': 'utf_8', + 'utf-8': 'utf_8', + } + +# Map of encodings to the BOM to write. +BOM_SET = { + 'utf_8': BOM_UTF8, + 'utf_16': BOM_UTF16, + 'utf16_be': BOM_UTF16_BE, + 'utf16_le': BOM_UTF16_LE, + None: BOM_UTF8 + } + +try: + from validate import VdtMissingValue +except ImportError: + VdtMissingValue = None + +try: + enumerate +except NameError: + def enumerate(obj): + """enumerate for Python 2.2.""" + i = -1 + for item in obj: + i += 1 + yield i, item + +try: + True, False +except NameError: + True, False = 1, 0 + + +__version__ = '4.4.0' + +__revision__ = '$Id: configobj.py 156 2006-01-31 14:57:08Z fuzzyman $' + +__docformat__ = "restructuredtext en" + +__all__ = ( + '__version__', + 'DEFAULT_INDENT_TYPE', + 'DEFAULT_INTERPOLATION', + 'ConfigObjError', + 'NestingError', + 'ParseError', + 'DuplicateError', + 'ConfigspecError', + 'ConfigObj', + 'SimpleVal', + 'InterpolationError', + 'InterpolationLoopError', + 'MissingInterpolationOption', + 'RepeatSectionError', + 'UnreprError', + 'UnknownType', + '__docformat__', + 'flatten_errors', +) + +DEFAULT_INTERPOLATION = 'configparser' +DEFAULT_INDENT_TYPE = ' ' +MAX_INTERPOL_DEPTH = 10 + +OPTION_DEFAULTS = { + 'interpolation': True, + 'raise_errors': False, + 'list_values': True, + 'create_empty': False, + 'file_error': False, + 'configspec': None, + 'stringify': True, + # option may be set to one of ('', ' ', '\t') + 'indent_type': None, + 'encoding': None, + 'default_encoding': None, + 'unrepr': False, + 'write_empty_values': False, +} + + +def getObj(s): + s = "a=" + s + if compiler is None: + raise ImportError('compiler module not available') + p = compiler.parse(s) + return p.getChildren()[1].getChildren()[0].getChildren()[1] + +class UnknownType(Exception): + pass + +class Builder: + + 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 undefinted 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]) + +def unrepr(s): + if not s: + return s + return Builder().build(getObj(s)) + +def _splitlines(instring): + """Split a string on lines, without losing line endings or truncating.""" + + +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 + self.message = message + 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 DuplicateError(ConfigObjError): + """ + The keyword or section specified already exists. + """ + +class ConfigspecError(ConfigObjError): + """ + An error occured whilst parsing a configspec. + """ + +class InterpolationError(ConfigObjError): + """Base class for the two interpolation errors.""" + +class InterpolationLoopError(InterpolationError): + """Maximum interpolation depth exceeded in string interpolation.""" + + def __init__(self, option): + InterpolationError.__init__( + self, + 'interpolation loop detected in value "%s".' % option) + +class RepeatSectionError(ConfigObjError): + """ + This error indicates additional sections in a section with a + ``__many__`` (repeated) section. + """ + +class MissingInterpolationOption(InterpolationError): + """A value specified for interpolation was missing.""" + + def __init__(self, option): + InterpolationError.__init__( + self, + 'missing option "%s" in interpolation.' % option) + +class UnreprError(ConfigObjError): + """An error parsing in unrepr mode.""" + + +class InterpolationEngine(object): + """ + A helper class to help perform string interpolation. + + This class is an abstract base class; its descendants perform + the actual work. + """ + + # compiled regexp to use in self.interpolate() + _KEYCRE = re.compile(r"%\(([^)]*)\)s") + + def __init__(self, section): + # the Section instance that "owns" this engine + self.section = section + + def interpolate(self, key, value): + def recursive_interpolate(key, value, section, backtrail): + """The function that does the actual work. + + ``value``: the string we're trying to interpolate. + ``section``: the section in which that string was found + ``backtrail``: a dict to keep track of where we've been, + to detect and prevent infinite recursion loops + + This is similar to a depth-first-search algorithm. + """ + # Have we been here already? + if backtrail.has_key((key, section.name)): + # Yes - infinite loop detected + raise InterpolationLoopError(key) + # Place a marker on our backtrail so we won't come back here again + backtrail[(key, section.name)] = 1 + + # Now start the actual work + match = self._KEYCRE.search(value) + while match: + # The actual parsing of the match is implementation-dependent, + # so delegate to our helper function + k, v, s = self._parse_match(match) + if k is None: + # That's the signal that no further interpolation is needed + replacement = v + else: + # Further interpolation may be needed to obtain final value + replacement = recursive_interpolate(k, v, s, backtrail) + # Replace the matched string with its final value + start, end = match.span() + value = ''.join((value[:start], replacement, value[end:])) + new_search_start = start + len(replacement) + # Pick up the next interpolation key, if any, for next time + # through the while loop + match = self._KEYCRE.search(value, new_search_start) + + # Now safe to come back here again; remove marker from backtrail + del backtrail[(key, section.name)] + + return value + + # Back in interpolate(), all we have to do is kick off the recursive + # function with appropriate starting values + value = recursive_interpolate(key, value, self.section, {}) + return value + + def _fetch(self, key): + """Helper function to fetch values from owning section. + + Returns a 2-tuple: the value, and the section where it was found. + """ + # switch off interpolation before we try and fetch anything ! + save_interp = self.section.main.interpolation + self.section.main.interpolation = False + + # Start at section that "owns" this InterpolationEngine + current_section = self.section + while True: + # try the current section first + val = current_section.get(key) + if val is not None: + break + # try "DEFAULT" next + val = current_section.get('DEFAULT', {}).get(key) + if val is not None: + break + # move up to parent and try again + # top-level's parent is itself + if current_section.parent is current_section: + # reached top level, time to give up + break + current_section = current_section.parent + + # restore interpolation to previous value before returning + self.section.main.interpolation = save_interp + if val is None: + raise MissingInterpolationOption(key) + return val, current_section + + def _parse_match(self, match): + """Implementation-dependent helper function. + + Will be passed a match object corresponding to the interpolation + key we just found (e.g., "%(foo)s" or "$foo"). Should look up that + key in the appropriate config file section (using the ``_fetch()`` + helper function) and return a 3-tuple: (key, value, section) + + ``key`` is the name of the key we're looking for + ``value`` is the value found for that key + ``section`` is a reference to the section where it was found + + ``key`` and ``section`` should be None if no further + interpolation should be performed on the resulting value + (e.g., if we interpolated "$$" and returned "$"). + """ + raise NotImplementedError + + +class ConfigParserInterpolation(InterpolationEngine): + """Behaves like ConfigParser.""" + _KEYCRE = re.compile(r"%\(([^)]*)\)s") + + def _parse_match(self, match): + key = match.group(1) + value, section = self._fetch(key) + return key, value, section + + +class TemplateInterpolation(InterpolationEngine): + """Behaves like string.Template.""" + _delimiter = '$' + _KEYCRE = re.compile(r""" + \$(?: + (?P<escaped>\$) | # Two $ signs + (?P<named>[_a-z][_a-z0-9]*) | # $name format + {(?P<braced>[^}]*)} # ${name} format + ) + """, re.IGNORECASE | re.VERBOSE) + + def _parse_match(self, match): + # Valid name (in or out of braces): fetch value from section + key = match.group('named') or match.group('braced') + if key is not None: + value, section = self._fetch(key) + return key, value, section + # Escaped delimiter (e.g., $$): return single delimiter + if match.group('escaped') is not None: + # Return None for key and section to indicate it's time to stop + return None, self._delimiter, None + # Anything else: ignore completely, just return it unchanged + return None, match.group(), None + +interpolation_engines = { + 'configparser': ConfigParserInterpolation, + 'template': TemplateInterpolation, +} + +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 __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 + # the sequence of scalar values in this Section + self.scalars = [] + # the sequence of sections in this Section + self.sections = [] + # purely for information + self.name = name + # for comments :-) + self.comments = {} + self.inline_comments = {} + # for the configspec + self.configspec = {} + self._order = [] + self._configspec_comments = {} + self._configspec_inline_comments = {} + self._cs_section_comments = {} + self._cs_section_inline_comments = {} + # for defaults + self.defaults = [] + # + # we do this explicitly so that __setitem__ is used properly + # (rather than just passing to ``dict.__init__``) + for entry in indict: + self[entry] = indict[entry] + + def _interpolate(self, key, value): + try: + # do we already have an interpolation engine? + engine = self._interpolation_engine + except AttributeError: + # not yet: first time running _interpolate(), so pick the engine + name = self.main.interpolation + if name == True: # note that "if name:" would be incorrect here + # backwards-compatibility: interpolation=True means use default + name = DEFAULT_INTERPOLATION + name = name.lower() # so that "Template", "template", etc. all work + class_ = interpolation_engines.get(name, None) + if class_ is None: + # invalid value for self.main.interpolation + self.main.interpolation = False + return value + else: + # save reference to engine so we don't have to do this again + engine = self._interpolation_engine = class_(self) + # let the engine do the actual work + return engine.interpolate(key, value) + + def __getitem__(self, key): + """Fetch the item and do string interpolation.""" + val = dict.__getitem__(self, key) + if self.main.interpolation and isinstance(val, StringTypes): + return self._interpolate(key, val) + return val + + def __setitem__(self, key, value, unrepr=False): + """ + Correctly set a value. + + Making dictionary values Section instances. + (We have to special case 'Section' instances - which are also dicts) + + Keys must be strings. + Values need only be strings (or lists of strings) if + ``main.stringify`` is set. + + `unrepr`` must be set when setting a value to a dictionary, without + creating a new sub-section. + """ + if not isinstance(key, StringTypes): + raise ValueError, 'The key "%s" is not a string.' % key + # add the comment + if not self.comments.has_key(key): + self.comments[key] = [] + self.inline_comments[key] = '' + # remove the entry from defaults + if key in self.defaults: + self.defaults.remove(key) + # + if isinstance(value, Section): + if not self.has_key(key): + self.sections.append(key) + dict.__setitem__(self, key, value) + elif isinstance(value, dict) and not unrepr: + # First create the new depth level, + # then create the section + if not self.has_key(key): + self.sections.append(key) + new_depth = self.depth + 1 + dict.__setitem__( + self, + key, + Section( + self, + new_depth, + self.main, + indict=value, + name=key)) + else: + if not self.has_key(key): + self.scalars.append(key) + if not self.main.stringify: + if isinstance(value, StringTypes): + pass + elif isinstance(value, (list, tuple)): + for entry in value: + if not isinstance(entry, StringTypes): + raise TypeError, ( + 'Value is not a string "%s".' % entry) + else: + raise TypeError, 'Value is not a string "%s".' % value + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + """Remove items from the sequence when deleting.""" + dict. __delitem__(self, key) + if key in self.scalars: + self.scalars.remove(key) + else: + self.sections.remove(key) + del self.comments[key] + del self.inline_comments[key] + + def get(self, key, default=None): + """A version of ``get`` that doesn't bypass string interpolation.""" + try: + return self[key] + except KeyError: + return default + + def update(self, indict): + """ + A version of update that uses our ``__setitem__``. + """ + for entry in indict: + self[entry] = indict[entry] + + def pop(self, key, *args): + """ """ + val = dict.pop(self, key, *args) + if key in self.scalars: + del self.comments[key] + del self.inline_comments[key] + self.scalars.remove(key) + elif key in self.sections: + del self.comments[key] + del self.inline_comments[key] + self.sections.remove(key) + if self.main.interpolation and isinstance(val, StringTypes): + return self._interpolate(key, val) + return val + + def popitem(self): + """Pops the first (key,val)""" + sequence = (self.scalars + self.sections) + if not sequence: + raise KeyError, ": 'popitem(): dictionary is empty'" + key = sequence[0] + val = self[key] + del self[key] + return key, val + + def clear(self): + """ + A version of clear that also affects scalars/sections + Also clears comments and configspec. + + Leaves other attributes alone : + depth/main/parent are not affected + """ + dict.clear(self) + self.scalars = [] + self.sections = [] + self.comments = {} + self.inline_comments = {} + self.configspec = {} + + 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): + """ """ + return zip((self.scalars + self.sections), self.values()) + + def keys(self): + """ """ + return (self.scalars + self.sections) + + def values(self): + """ """ + return [self[key] for key in (self.scalars + self.sections)] + + def iteritems(self): + """ """ + return iter(self.items()) + + def iterkeys(self): + """ """ + return iter((self.scalars + self.sections)) + + __iter__ = iterkeys + + def itervalues(self): + """ """ + return iter(self.values()) + + def __repr__(self): + return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(self[key]))) + for key in (self.scalars + self.sections)]) + + __str__ = __repr__ + + # Extra methods - not in a normal dictionary + + def dict(self): + """ + Return a deepcopy of self as a dictionary. + + All members that are ``Section`` instances are recursively turned to + ordinary dictionaries - by calling their ``dict`` method. + + >>> n = a.dict() + >>> n == a + 1 + >>> n is a + 0 + """ + newdict = {} + for entry in self: + this_entry = self[entry] + if isinstance(this_entry, Section): + this_entry = this_entry.dict() + elif isinstance(this_entry, list): + # create a copy rather than a reference + this_entry = list(this_entry) + elif isinstance(this_entry, tuple): + # create a copy rather than a reference + this_entry = tuple(this_entry) + newdict[entry] = this_entry + return newdict + + def merge(self, indict): + """ + A recursive update - useful for merging config files. + + >>> a = '''[section1] + ... option1 = True + ... [[subsection]] + ... more_options = False + ... # end of file'''.splitlines() + >>> b = '''# File is user.ini + ... [section1] + ... option1 = False + ... # end of file'''.splitlines() + >>> c1 = ConfigObj(b) + >>> c2 = ConfigObj(a) + >>> c2.merge(c1) + >>> c2 + {'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}} + """ + for key, val in indict.items(): + if (key in self and isinstance(self[key], dict) and + isinstance(val, dict)): + self[key].merge(val) + else: + self[key] = val + + def rename(self, oldkey, newkey): + """ + Change a keyname to another, without changing position in sequence. + + Implemented so that transformations can be made on keys, + as well as on values. (used by encode and decode) + + Also renames comments. + """ + if oldkey in self.scalars: + the_list = self.scalars + elif oldkey in self.sections: + the_list = self.sections + else: + raise KeyError, 'Key "%s" not found.' % oldkey + pos = the_list.index(oldkey) + # + val = self[oldkey] + dict.__delitem__(self, oldkey) + dict.__setitem__(self, newkey, val) + the_list.remove(oldkey) + the_list.insert(pos, newkey) + comm = self.comments[oldkey] + inline_comment = self.inline_comments[oldkey] + del self.comments[oldkey] + del self.inline_comments[oldkey] + self.comments[newkey] = comm + self.inline_comments[newkey] = inline_comment + + def walk(self, function, raise_errors=True, + call_on_sections=False, **keywargs): + """ + Walk every member and call a function on the keyword and value. + + Return a dictionary of the return values + + If the function raises an exception, raise the errror + unless ``raise_errors=False``, in which case set the return value to + ``False``. + + Any unrecognised keyword arguments you pass to walk, will be pased on + to the function you pass in. + + Note: if ``call_on_sections`` is ``True`` then - on encountering a + subsection, *first* the function is called for the *whole* subsection, + and then recurses into it's members. This means your function must be + able to handle strings, dictionaries and lists. This allows you + to change the key of subsections as well as for ordinary members. The + return value when called on the whole subsection has to be discarded. + + See the encode and decode methods for examples, including functions. + + .. caution:: + + You can use ``walk`` to transform the names of members of a section + but you mustn't add or delete members. + + >>> config = '''[XXXXsection] + ... XXXXkey = XXXXvalue'''.splitlines() + >>> cfg = ConfigObj(config) + >>> cfg + {'XXXXsection': {'XXXXkey': 'XXXXvalue'}} + >>> def transform(section, key): + ... val = section[key] + ... newkey = key.replace('XXXX', 'CLIENT1') + ... section.rename(key, newkey) + ... if isinstance(val, (tuple, list, dict)): + ... pass + ... else: + ... val = val.replace('XXXX', 'CLIENT1') + ... section[newkey] = val + >>> cfg.walk(transform, call_on_sections=True) + {'CLIENT1section': {'CLIENT1key': None}} + >>> cfg + {'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}} + """ + out = {} + # scalars first + for i in range(len(self.scalars)): + entry = self.scalars[i] + try: + val = function(self, entry, **keywargs) + # bound again in case name has changed + entry = self.scalars[i] + out[entry] = val + except Exception: + if raise_errors: + raise + else: + entry = self.scalars[i] + out[entry] = False + # then sections + for i in range(len(self.sections)): + entry = self.sections[i] + if call_on_sections: + try: + function(self, entry, **keywargs) + except Exception: + if raise_errors: + raise + else: + entry = self.sections[i] + out[entry] = False + # bound again in case name has changed + entry = self.sections[i] + # previous result is discarded + out[entry] = self[entry].walk( + function, + raise_errors=raise_errors, + call_on_sections=call_on_sections, + **keywargs) + return out + + def decode(self, encoding): + """ + Decode all strings and values to unicode, using the specified encoding. + + Works with subsections and list values. + + Uses the ``walk`` method. + + Testing ``encode`` and ``decode``. + >>> m = ConfigObj(a) + >>> m.decode('ascii') + >>> def testuni(val): + ... for entry in val: + ... if not isinstance(entry, unicode): + ... print >> sys.stderr, type(entry) + ... raise AssertionError, 'decode failed.' + ... if isinstance(val[entry], dict): + ... testuni(val[entry]) + ... elif not isinstance(val[entry], unicode): + ... raise AssertionError, 'decode failed.' + >>> testuni(m) + >>> m.encode('ascii') + >>> a == m + 1 + """ + warn('use of ``decode`` is deprecated.', DeprecationWarning) + def decode(section, key, encoding=encoding, warn=True): + """ """ + val = section[key] + if isinstance(val, (list, tuple)): + newval = [] + for entry in val: + newval.append(entry.decode(encoding)) + elif isinstance(val, dict): + newval = val + else: + newval = val.decode(encoding) + newkey = key.decode(encoding) + section.rename(key, newkey) + section[newkey] = newval + # using ``call_on_sections`` allows us to modify section names + self.walk(decode, call_on_sections=True) + + def encode(self, encoding): + """ + Encode all strings and values from unicode, + using the specified encoding. + + Works with subsections and list values. + Uses the ``walk`` method. + """ + warn('use of ``encode`` is deprecated.', DeprecationWarning) + def encode(section, key, encoding=encoding): + """ """ + val = section[key] + if isinstance(val, (list, tuple)): + newval = [] + for entry in val: + newval.append(entry.encode(encoding)) + elif isinstance(val, dict): + newval = val + else: + newval = val.encode(encoding) + newkey = key.encode(encoding) + section.rename(key, newkey) + section[newkey] = newval + self.walk(encode, call_on_sections=True) + + def istrue(self, key): + """A deprecated version of ``as_bool``.""" + warn('use of ``istrue`` is deprecated. Use ``as_bool`` method ' + 'instead.', DeprecationWarning) + return self.as_bool(key) + + def as_bool(self, key): + """ + Accepts a key as input. The corresponding value must be a string or + the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to + retain compatibility with Python 2.2. + + If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns + ``True``. + + If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns + ``False``. + + ``as_bool`` is not case sensitive. + + Any other input will raise a ``ValueError``. + + >>> a = ConfigObj() + >>> a['a'] = 'fish' + >>> a.as_bool('a') + Traceback (most recent call last): + ValueError: Value "fish" is neither True nor False + >>> a['b'] = 'True' + >>> a.as_bool('b') + 1 + >>> a['b'] = 'off' + >>> a.as_bool('b') + 0 + """ + val = self[key] + if val == True: + return True + elif val == False: + return False + else: + try: + if not isinstance(val, StringTypes): + raise KeyError + else: + return self.main._bools[val.lower()] + except KeyError: + raise ValueError('Value "%s" is neither True nor False' % val) + + def as_int(self, key): + """ + A convenience method which coerces the specified value to an integer. + + If the value is an invalid literal for ``int``, a ``ValueError`` will + be raised. + + >>> a = ConfigObj() + >>> a['a'] = 'fish' + >>> a.as_int('a') + Traceback (most recent call last): + ValueError: invalid literal for int(): fish + >>> a['b'] = '1' + >>> a.as_int('b') + 1 + >>> a['b'] = '3.2' + >>> a.as_int('b') + Traceback (most recent call last): + ValueError: invalid literal for int(): 3.2 + """ + return int(self[key]) + + def as_float(self, key): + """ + A convenience method which coerces the specified value to a float. + + If the value is an invalid literal for ``float``, a ``ValueError`` will + be raised. + + >>> a = ConfigObj() + >>> a['a'] = 'fish' + >>> a.as_float('a') + Traceback (most recent call last): + ValueError: invalid literal for float(): fish + >>> a['b'] = '1' + >>> a.as_float('b') + 1.0 + >>> a['b'] = '3.2' + >>> a.as_float('b') + 3.2000000000000002 + """ + return float(self[key]) + + +class ConfigObj(Section): + """An object to read, create, and write config files.""" + + _keyword = re.compile(r'''^ # line start + (\s*) # indentation + ( # keyword + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'"=].*?) # no quotes + ) + \s*=\s* # divider + (.*) # value (including list values and comments) + $ # line end + ''', + re.VERBOSE) + + _sectionmarker = re.compile(r'''^ + (\s*) # 1: indentation + ((?:\[\s*)+) # 2: section marker open + ( # 3: section name open + (?:"\s*\S.*?\s*")| # at least one non-space with double quotes + (?:'\s*\S.*?\s*')| # at least one non-space with single quotes + (?:[^'"\s].*?) # at least one non-space unquoted + ) # section name close + ((?:\s*\])+) # 4: section marker close + \s*(\#.*)? # 5: optional comment + $''', + re.VERBOSE) + + # this regexp pulls list values out as a single string + # or single values and comments + # FIXME: this regex adds a '' to the end of comma terminated lists + # workaround in ``_handle_value`` + _valueexp = re.compile(r'''^ + (?: + (?: + ( + (?: + (?: + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\#][^,\#]*?) # unquoted + ) + \s*,\s* # comma + )* # match all list items ending in a comma (if any) + ) + ( + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\#\s][^,]*?)| # unquoted + (?:(?<!,)) # Empty value + )? # last item in a list - or string value + )| + (,) # alternatively a single comma - empty list + ) + \s*(\#.*)? # optional comment + $''', + re.VERBOSE) + + # use findall to get the members of a list value + _listvalueexp = re.compile(r''' + ( + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'",\#].*?) # unquoted + ) + \s*,\s* # comma + ''', + re.VERBOSE) + + # this regexp is used for the value + # when lists are switched off + _nolistvalue = re.compile(r'''^ + ( + (?:".*?")| # double quotes + (?:'.*?')| # single quotes + (?:[^'"\#].*?)| # unquoted + (?:) # Empty value + ) + \s*(\#.*)? # optional comment + $''', + re.VERBOSE) + + # regexes for finding triple quoted values on one line + _single_line_single = re.compile(r"^'''(.*?)'''\s*(#.*)?$") + _single_line_double = re.compile(r'^"""(.*?)"""\s*(#.*)?$') + _multi_line_single = re.compile(r"^(.*?)'''\s*(#.*)?$") + _multi_line_double = re.compile(r'^(.*?)"""\s*(#.*)?$') + + _triple_quote = { + "'''": (_single_line_single, _multi_line_single), + '"""': (_single_line_double, _multi_line_double), + } + + # Used by the ``istrue`` Section method + _bools = { + 'yes': True, 'no': False, + 'on': True, 'off': False, + '1': True, '0': False, + 'true': True, 'false': False, + } + + def __init__(self, infile=None, options=None, **kwargs): + """ + Parse or create a config file object. + + ``ConfigObj(infile=None, options=None, **kwargs)`` + """ + if infile is None: + infile = [] + if options is None: + options = {} + else: + options = dict(options) + # keyword arguments take precedence over an options dictionary + options.update(kwargs) + # init the superclass + Section.__init__(self, self, 0, self) + # + defaults = OPTION_DEFAULTS.copy() + for entry in options.keys(): + if entry not in defaults.keys(): + raise TypeError, 'Unrecognised option "%s".' % entry + # TODO: check the values too. + # + # Add any explicit options to the defaults + defaults.update(options) + # + # initialise a few variables + self.filename = None + self._errors = [] + self.raise_errors = defaults['raise_errors'] + self.interpolation = defaults['interpolation'] + self.list_values = defaults['list_values'] + self.create_empty = defaults['create_empty'] + self.file_error = defaults['file_error'] + self.stringify = defaults['stringify'] + self.indent_type = defaults['indent_type'] + self.encoding = defaults['encoding'] + self.default_encoding = defaults['default_encoding'] + self.BOM = False + self.newlines = None + self.write_empty_values = defaults['write_empty_values'] + self.unrepr = defaults['unrepr'] + # + self.initial_comment = [] + self.final_comment = [] + # + self._terminated = False + # + if isinstance(infile, StringTypes): + self.filename = infile + if os.path.isfile(infile): + infile = open(infile).read() 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 + h = open(infile, 'w') + h.write('') + h.close() + infile = [] + elif isinstance(infile, (list, tuple)): + infile = list(infile) + elif isinstance(infile, dict): + # initialise self + # the Section class handles creating subsections + if isinstance(infile, ConfigObj): + # get a copy of our ConfigObj + infile = infile.dict() + for entry in infile: + self[entry] = infile[entry] + del self._errors + if defaults['configspec'] is not None: + self._handle_configspec(defaults['configspec']) + else: + self.configspec = None + return + elif hasattr(infile, 'read'): + # This supports file like objects + infile = infile.read() or [] + # needs splitting into lines - but needs doing *after* decoding + # in case it's not an 8 bit encoding + else: + raise TypeError, ('infile must be a filename,' + ' file like object, or list of lines.') + # + if infile: + # don't do it for the empty ConfigObj + infile = self._handle_bom(infile) + # infile is now *always* a list + # + # Set the newlines attribute (first line ending it finds) + # and strip trailing '\n' or '\r' from lines + for line in infile: + if (not line) or (line[-1] not in ('\r', '\n', '\r\n')): + continue + for end in ('\r\n', '\n', '\r'): + if line.endswith(end): + self.newlines = end + break + break + if infile[-1] and infile[-1] in ('\r', '\n', '\r\n'): + self._terminated = True + infile = [line.rstrip('\r\n') for line in infile] + # + self._parse(infile) + # if we had any errors, now is the time to raise them + if self._errors: + info = "at line %s." % self._errors[0].line_number + if len(self._errors) > 1: + msg = ("Parsing failed with several errors.\nFirst error %s" % + info) + error = ConfigObjError(msg) + else: + error = self._errors[0] + # set the errors attribute; it's a list of tuples: + # (error_type, message, line_number) + error.errors = self._errors + # set the config attribute + error.config = self + raise error + # delete private attributes + del self._errors + # + if defaults['configspec'] is None: + self.configspec = None + else: + self._handle_configspec(defaults['configspec']) + + def __repr__(self): + return 'ConfigObj({%s})' % ', '.join( + [('%s: %s' % (repr(key), repr(self[key]))) for key in + (self.scalars + self.sections)]) + + def _handle_bom(self, infile): + """ + Handle any BOM, and decode if necessary. + + If an encoding is specified, that *must* be used - but the BOM should + still be removed (and the BOM attribute set). + + (If the encoding is wrongly specified, then a BOM for an alternative + encoding won't be discovered or removed.) + + If an encoding is not specified, UTF8 or UTF16 BOM will be detected and + removed. The BOM attribute will be set. UTF16 will be decoded to + unicode. + + NOTE: This method must not be called with an empty ``infile``. + + Specifying the *wrong* encoding is likely to cause a + ``UnicodeDecodeError``. + + ``infile`` must always be returned as a list of lines, but may be + passed in as a single string. + """ + if ((self.encoding is not None) and + (self.encoding.lower() not in BOM_LIST)): + # No need to check for a BOM + # the encoding specified doesn't have one + # just decode + return self._decode(infile, self.encoding) + # + if isinstance(infile, (list, tuple)): + line = infile[0] + else: + line = infile + if self.encoding is not None: + # encoding explicitly supplied + # And it could have an associated BOM + # TODO: if encoding is just UTF16 - we ought to check for both + # TODO: big endian and little endian versions. + enc = BOM_LIST[self.encoding.lower()] + if enc == 'utf_16': + # For UTF16 we try big endian and little endian + for BOM, (encoding, final_encoding) in BOMS.items(): + if not final_encoding: + # skip UTF8 + continue + if infile.startswith(BOM): + ### BOM discovered + ##self.BOM = True + # Don't need to remove BOM + return self._decode(infile, encoding) + # + # If we get this far, will *probably* raise a DecodeError + # As it doesn't appear to start with a BOM + return self._decode(infile, self.encoding) + # + # Must be UTF8 + BOM = BOM_SET[enc] + if not line.startswith(BOM): + return self._decode(infile, self.encoding) + # + newline = line[len(BOM):] + # + # BOM removed + if isinstance(infile, (list, tuple)): + infile[0] = newline + else: + infile = newline + self.BOM = True + return self._decode(infile, self.encoding) + # + # No encoding specified - so we need to check for UTF8/UTF16 + for BOM, (encoding, final_encoding) in BOMS.items(): + if not line.startswith(BOM): + continue + else: + # BOM discovered + self.encoding = final_encoding + if not final_encoding: + self.BOM = True + # UTF8 + # remove BOM + newline = line[len(BOM):] + if isinstance(infile, (list, tuple)): + infile[0] = newline + else: + infile = newline + # UTF8 - don't decode + if isinstance(infile, StringTypes): + return infile.splitlines(True) + else: + return infile + # UTF16 - have to decode + return self._decode(infile, encoding) + # + # No BOM discovered and no encoding specified, just return + if isinstance(infile, StringTypes): + # infile read from a file will be a single string + return infile.splitlines(True) + else: + return infile + + def _a_to_u(self, aString): + """Decode ASCII strings to unicode if a self.encoding is specified.""" + if self.encoding: + return aString.decode('ascii') + else: + return aString + + def _decode(self, infile, encoding): + """ + Decode infile to unicode. Using the specified encoding. + + if is a string, it also needs converting to a list. + """ + if isinstance(infile, StringTypes): + # can't be unicode + # NOTE: Could raise a ``UnicodeDecodeError`` + return infile.decode(encoding).splitlines(True) + for i, line in enumerate(infile): + if not isinstance(line, unicode): + # NOTE: The isinstance test here handles mixed lists of unicode/string + # NOTE: But the decode will break on any non-string values + # NOTE: Or could raise a ``UnicodeDecodeError`` + infile[i] = line.decode(encoding) + return infile + + def _decode_element(self, line): + """Decode element to unicode if necessary.""" + if not self.encoding: + return line + if isinstance(line, str) and self.default_encoding: + return line.decode(self.default_encoding) + return line + + def _str(self, value): + """ + Used by ``stringify`` within validate, to turn non-string values + into strings. + """ + if not isinstance(value, StringTypes): + return str(value) + else: + return value + + def _parse(self, infile): + """Actually parse the config file.""" + temp_list_values = self.list_values + if self.unrepr: + self.list_values = False + comment_list = [] + done_start = False + this_section = self + maxline = len(infile) - 1 + cur_index = -1 + reset_comment = False + while cur_index < maxline: + if reset_comment: + comment_list = [] + cur_index += 1 + line = infile[cur_index] + sline = line.strip() + # do we have anything on the line ? + if not sline or sline.startswith('#'): + reset_comment = False + comment_list.append(line) + continue + if not done_start: + # preserve initial comment + self.initial_comment = comment_list + comment_list = [] + done_start = True + reset_comment = True + # first we check if it's a section marker + mat = self._sectionmarker.match(line) + if mat is not None: + # is a section line + (indent, sect_open, sect_name, sect_close, comment) = ( + mat.groups()) + if indent and (self.indent_type is None): + self.indent_type = indent + cur_depth = sect_open.count('[') + if cur_depth != sect_close.count(']'): + self._handle_error( + "Cannot compute the section depth at line %s.", + NestingError, infile, cur_index) + continue + # + if cur_depth < this_section.depth: + # the new section is dropping back to a previous level + try: + parent = self._match_depth( + this_section, + cur_depth).parent + except SyntaxError: + self._handle_error( + "Cannot compute nesting level at line %s.", + NestingError, infile, cur_index) + continue + elif cur_depth == this_section.depth: + # the new section is a sibling of the current section + parent = this_section.parent + elif cur_depth == this_section.depth + 1: + # the new section is a child the current section + parent = this_section + else: + self._handle_error( + "Section too nested at line %s.", + NestingError, infile, cur_index) + # + sect_name = self._unquote(sect_name) + if parent.has_key(sect_name): + self._handle_error( + 'Duplicate section name at line %s.', + DuplicateError, infile, cur_index) + continue + # create the new section + this_section = Section( + parent, + cur_depth, + self, + name=sect_name) + parent[sect_name] = this_section + parent.inline_comments[sect_name] = comment + parent.comments[sect_name] = comment_list + continue + # + # it's not a section... [truncated message content] |