Source code for configfile

# ConfigFile class - Dynamically parse and edit configuration files.
# Copyright (C) 2011-2015 Dario Giovannetti <dev@dariogiovannetti.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
This library provides the :py:class:`ConfigFile` class, whose goal is to
provide an interface for parsing, modifying and writing configuration files.

Main features:

* Support for subsections. Support for sectionless options (root options).
* Read from multiple sources (files, file-like objects, dictionaries or special
  compatible objects) and compose them in a single :py:class:`ConfigFile`
  object.
* When importing and exporting it is possible to choose what to do with
  options only existing in the source, only existing in the destination, or
  existing in both with different values.
* Import a configuration source into a particular subsection of an existing
  object. Export only a particular subsection of an existing object.
* Preserve the order of sections and options when exporting. Try the best to
  preserve any comments too.
* Access sections and options with the
  ``root('Section', 'Subsection')['option']`` syntax or the
  ``root('Section')('Subsection')['option']`` syntax.
* save references to subsections with e.g.
  ``subsection = section('Section', 'Subsection')``.
* Interpolation of option values between sections when importing.

Author: Dario Giovannetti <dev@dariogiovannetti.net>

License: GPLv3

GitHub: https://www.github.com/kynikos/lib.py.configfile

Issue tracker: https://www.github.com/kynikos/lib.py.configfile/issues

**Note:** as it is clear by reading this page, the documentation is still in a
poor state. If you manage to understand how this library works and want to help
documenting it, you are welcome to fork the GitHub repository and request to
pull your improvements. Everything is written in docstrings in the only
python module of the package.

Also, if you have any questions, do not hesitate to ask in the issue tracker,
or write the author an email!

Examples
========

Basic usage
-----------

Suppose you have these two files:

``/path/to/file``:

.. code-block:: cfg

    root_option = demo

    [Section1]
    test = ok
    retest = no
    test3 = yes

    [Section2.Section2A]
    foo = fooo

    [Section3]
    bar = yay

``/path/to/other_file``:

.. code-block:: cfg

    [Section2C]
    an_option = 2

Now run this script:

::

    from configfile import ConfigFile

    conf = ConfigFile("/path/to/file")

    conf("Section2").upgrade("path/to/other_file")

    option = conf("Section2", "Section2C")["an_option"]
    print(option, type(option))  # 2 <class 'str'>

    option = conf("Section2")("Section2C").get_int("an_option")
    print(option, type(option))  # 2 <class 'int'>

    conf.export_add("/path/to/file")

    conf["root_option"] = "value"

    conf("Section3").export_reset("/path/to/another_file")

You will end up with these files (``/path/to/other_file`` is left
untouched):

``/path/to/file``:

.. code-block:: cfg

    root_option = demo

    [Section1]
    test = ok
    retest = no
    test3 = yes

    [Section2.Section2A]
    foo = fooo

    [Section2.Section2C]
    an_option = 2

    [Section3]
    bar = yay

``/path/to/another_file``:

.. code-block:: cfg

    bar = yay

Interpolation
-------------

Suppose you have this file:

``/path/to/file``:

.. code-block:: cfg

    [Section1]
    option = foo ${$:Section2$:optionA$}

    [Section1.Section2]
    optionA = some value
    optionB = ${optionA$} test
    optionC = test ${$:optionA$}

    [Section3]
    option = ${Section1$:Section2$:optionA$} bar

Now run this script:

::

    from configfile import ConfigFile

    conf = ConfigFile("/path/to/file", interpolation=True)

    print(conf('Section1')['option'])  # foo some value
    print(conf('Section1', 'Section2')['optionA'])  # some value
    print(conf('Section1', 'Section2')['optionB'])  # some value test
    print(conf('Section1', 'Section2')['optionC'])  # test some value
    print(conf('Section3')['option'])  # some value bar

Module contents
===============
"""

import errno
import re as re_
import collections
import io


[docs]class Section(object): """ The class for a section in the configuration file, including the root section. You should never need to instantiate this class directly, use :py:class:`ConfigFile` instead. """ # TODO: Compile only once (bug #20) _PARSE_SECTION = r'^\s*\[(.+)\]\s*$' _PARSE_OPTION = r'^\s*([^\=]+?)\s*\=\s*(.*?)\s*$' _PARSE_COMMENT = r'^\s*[#;]{1}\s*(.*?)\s*$' _PARSE_IGNORE = r'^\s*$' _SECTION_SUB = r'^[a-zA-Z_]+(?:\.?[a-zA-Z0-9_]+)*$' _SECTION_PLAIN = r'^[a-zA-Z_]+[a-zA-Z0-9_]*$' _OPTION = r'^[a-zA-Z_]+[a-zA-Z0-9_]*$' _VALUE = r'^.*$' _SECTION_SEP = r'.' _OPTION_SEP = r' = ' # "{}" will be replaced with the section name by str.format _SECTION_MARKERS = r'[{}]' _COMMENT_MARKER = r'# ' _INTERPOLATION_SPECIAL = '$' _INTERPOLATION_SPECIAL_ESC = _INTERPOLATION_SPECIAL * 2 _INTERPOLATION_START = _INTERPOLATION_SPECIAL + '{' _INTERPOLATION_SEP = _INTERPOLATION_SPECIAL + ':' _INTERPOLATION_END = _INTERPOLATION_SPECIAL + '}' _INTERPOLATION_SPLIT = (r'(' + r'|'.join(re_.escape(mark) for mark in ( _INTERPOLATION_SPECIAL_ESC, _INTERPOLATION_START, _INTERPOLATION_SEP, _INTERPOLATION_END)) + r')') _GET_BOOLEAN_TRUE = ('true', '1', 'yes', 'on', 'enabled') _GET_BOOLEAN_FALSE = ('false', '0', 'no', 'off', 'disabled') _GET_BOOLEAN_DEFAULT = None _DICT_CLASS = collections.OrderedDict # Use lambda to create a new object every time _EMPTY_SECTION = lambda self: (self._DICT_CLASS(), self._DICT_CLASS())
[docs] def __init__(self, name=None, parent=None, safe_calls=False, inherit_options=False, subsections=True, ignore_case=True): """ Constructor. :param str name: The name of the section. :param Section parent: A reference to the parent section object. :param bool safe_calls: If True, when calling a non-existent subsection, its closest existing ancestor is returned. :param bool inherit_options: Whether the section will inherit the options from its ancestors. :param bool subsections: If True, subsections are enabled; otherwise they are disabled. :param bool ignore_case: If True, section and option names will be compared ignoring case differences; regular expressions will use ``re.I`` flag. """ self._NAME = name self._PARENT = parent # TODO: Move constant settings to a Settings class (bug #19) self._SAFE_CALLS = safe_calls self._INHERIT_OPTIONS = inherit_options self._ENABLE_SUBSECTIONS = subsections self._IGNORE_CASE = ignore_case self._RE_I = re_.I if self._IGNORE_CASE else 0 self._SECTION = self._SECTION_SUB if self._ENABLE_SUBSECTIONS else \ self._SECTION_PLAIN self._options = self._DICT_CLASS() self._subsections = self._DICT_CLASS()
### DATA MODEL ###
[docs] def __call__(self, *path, **kwargs): """ Enables calling directly the object with a string or sequence of strings, returning the corresponding subsection object, if existent. :param path: A sequence of strings, representing a relative path of section names to the target descendant subsection, whose name is the last item. :type path: str :param bool safe: If True, when calling a non-existent subsection, its closest existing ancestor is returned. """ # The Python 3 definition was: #def __call__(self, *path, safe=None): # But to keep compatibility with Python 2 it has been changed to the # current safe = kwargs.get('safe') section = self for sname in path: try: lsname = sname.lower() except AttributeError: raise TypeError('Section name must be a string: {}'.format( sname)) if self._IGNORE_CASE: for subname in section._subsections: if lsname == subname.lower(): section = section._subsections[subname] break else: self._finalize_call(safe, sname) break else: try: section = section._subsections[sname] except KeyError: self._finalize_call(safe, sname) break return section
[docs] def _finalize_call(self, safe, sname): """ Auxiliary method for :py:meth:`__call__`. Process a not-found section name. """ if safe not in (True, False): if self._SAFE_CALLS: return elif safe: return raise KeyError('Section not found: {}'.format(sname))
[docs] def __getitem__(self, opt): """ Returns the value for the option specified. :param str opt: The name of the option whose value must be returned. """ item = self.get(opt, fallback=None, inherit_options=self._INHERIT_OPTIONS) # self.get returns None as a fallback value if opt is not found: # however, for compatibility with usual dictionary operations, # __getitem__ should better raise KeyError in this case if item is None: raise KeyError('Option not found: {}'.format(opt)) else: return item
[docs] def __setitem__(self, opt, val): """ Stores the provided value in the specified option. :param str opt: The name of the option. :param str val: The new value for the option. """ if isinstance(opt, str): if isinstance(val, str): if self._IGNORE_CASE: for o in self._options: if opt.lower() == o.lower(): self._options[o] = val break else: self._options[opt] = val else: self._options[opt] = val else: raise TypeError('Value must be a string: {}'.format(val)) else: raise TypeError('Option name must be a string: {}'.format(opt))
[docs] def __delitem__(self, opt): """ Deletes the specified option. :param str opt: The name of the option that must be deleted. """ try: lopt = opt.lower() except AttributeError: raise TypeError('Option name must be a string: {}'.format(opt)) else: if self._IGNORE_CASE: for o in self._options: if opt.lower() == o.lower(): del self._options[o] break else: raise KeyError('Option not found: {}'.format(opt)) else: try: del self._options[opt] except KeyError: raise KeyError('Option not found: {}'.format(opt))
[docs] def __iter__(self): """ Lets iterate over the options of the section (for example with a for loop). """ return iter(self._options)
[docs] def __contains__(self, item): """ If item is a :py:class:`Section` object, this method returns True if item (the object, not its name) is a subsection of self; otherwise this returns True if item is the name of an option in self. :param item: A :py:class:`Section` object or the name of an option. :type item: Section or str """ if isinstance(item, Section): return item in self._subsections.values() elif self._IGNORE_CASE: for o in self._options: if item.lower() == o.lower(): return True else: return False else: return item in self._options
### IMPORTING DATA ###
[docs] def set(self, opt, val): """ This is an alias for :py:meth:`__setitem__`. """ self[opt] = val
[docs] def make_subsection(self, name): """ Create an empty subsection under the current section if it does not exist. :param str name: The name of the new subsection. """ # TODO: Use this method, where possible, when creating new sections in # the other methods sub = self._EMPTY_SECTION() sub[1][name] = self._EMPTY_SECTION() self._import_object(sub, overwrite=False)
[docs] def delete(self): """ Delete the current section. """ del self._PARENT._subsections[self._NAME]
[docs] def upgrade(self, *sources, **kwargs): """ Import sections and options from a file, file-like object, dictionary or special object with upgrade mode. If an option already exists, change its value; if it does not exist, create it and store its value. For example: *{A:a,B:b,C:c} upgrade {A:d,D:e} => {A:d,B:b,C:c,D:e}* See :py:meth:`_import_object` for object compatibility. :param sources: A sequence of files, file-like objects, dictionaries and/or special objects. :param bool interpolation: Enable/disable value interpolation. """ # Necessary for Python 2 compatibility # The Python 3 definition was: #def upgrade(self, *sources, interpolation=False): interpolation = kwargs.get('interpolation', False) self._import(sources, interpolation=interpolation)
[docs] def update(self, *sources, **kwargs): """ Import sections and options from a file, file-like object, dictionary or special object with update mode. If an option already exists, change its value; if it does not exist, do not do anything. For example: *{A:a,B:b,C:c} update {A:d,D:e} => {A:d,B:b,C:c}* See :py:meth:`_import_object` for object compatibility. :param sources: A sequence of files, file-like objects, dictionaries and/or special objects. :param bool interpolation: Enable/disable value interpolation. """ # Necessary for Python 2 compatibility # The Python 3 definition was: #def upgrade(self, *sources, interpolation=False): interpolation = kwargs.get('interpolation', False) self._import(sources, add=False, interpolation=interpolation)
[docs] def reset(self, *sources, **kwargs): """ Import sections and options from a file, file-like object, dictionary or special object with reset mode. Delete all options and subsections and recreate everything from the importing object. For example: *{A:a,B:b,C:c} reset {A:d,D:e} => {A:d,D:e}* See :py:meth:`_import_object` for object compatibility. :param sources: A sequence of files, file-like objects, dictionaries and/or special objects. :param bool interpolation: Enable/disable value interpolation. """ # Necessary for Python 2 compatibility # The Python 3 definition was: #def upgrade(self, *sources, interpolation=False): interpolation = kwargs.get('interpolation', False) self._import(sources, reset=True, interpolation=interpolation)
[docs] def add(self, *sources, **kwargs): """ Import sections and options from a file, file-like object, dictionary or special object with add mode. If an option already exists, do not do anything; if it does not exist, create it and store its value. For example: *{A:a,B:b,C:c} add {A:d,D:e} => {A:a,B:b,C:c,D:e}* See :py:meth:`_import_object` for object compatibility. :param sources: A sequence of files, file-like objects, dictionaries and/or special objects. :param bool interpolation: Enable/disable value interpolation. """ # Necessary for Python 2 compatibility # The Python 3 definition was: #def upgrade(self, *sources, interpolation=False): interpolation = kwargs.get('interpolation', False) self._import(sources, overwrite=False, interpolation=interpolation)
[docs] def _import(self, sources, overwrite=True, add=True, reset=False, interpolation=False): """ Parse some files, file-like objects, dictionaries or special objects and add their configuration to the existing one. Distinction between the various source types is done automatically. :param sources: A sequence of all the file names, file-like objects, dictionaries or special objects to be parsed; a value of None will be ignored (useful for creating empty objects that will be populated programmatically). :param bool overwrite: This sets whether the next source in the chain overwrites already imported sections and options; see :py:meth:`_import_object` for more details. :param bool add: This sets whether the next source in the chain adds non-pre-existing sections and options; see _import_object for more details. :param bool reset: This sets whether the next source in the chain removes all the data added by the previous sources. :param bool interpolation: If True, option values will be interpolated using values from other options through the special syntax ``${section$:section$:option$}``. Options will be interpolated only once at importing: all links among options will be lost after importing. """ for source in sources: if source is None: continue elif isinstance(source, str): obj = self._parse_file(self._open_file(source)) elif isinstance(source, io.IOBase): obj = self._parse_file(source) elif isinstance(source, dict): obj = (source, {}) else: obj = source self._import_object(obj, overwrite=overwrite, add=add, reset=reset) if interpolation: self._interpolate()
[docs] def _open_file(self, cfile): """ Open config file for reading. :param str cfile: The name of the file to be parsed. """ try: return open(cfile, 'r') except EnvironmentError as e: if e.errno == errno.ENOENT: raise NonExistentFileError('Cannot find {} ({})'.format( e.filename, e.strerror)) else: raise InvalidFileError('Cannot import configuration from {} ' '({})'.format(e.filename, e.strerror))
[docs] def _parse_file(self, stream): """ Parse a text file and translate it into a compatible object, thus making it possible to import it. :param stream: a file-like object to be read from. """ with stream: cdict = self._EMPTY_SECTION() lastsect = cdict for lno, line in enumerate(stream): # Note that the order the various types are evaluated # matters! # TODO: Really? What about sorting the tests according # to their likelihood to pass? if re_.match(self._PARSE_IGNORE, line, self._RE_I): continue if re_.match(self._PARSE_COMMENT, line, self._RE_I): continue re_option = re_.match(self._PARSE_OPTION, line, self._RE_I) if re_option: lastsect[0][re_option.group(1)] = re_option.group(2) continue re_section = re_.match(self._PARSE_SECTION, line, self._RE_I) if re_section: subs = self._parse_subsections(re_section) d = cdict for s in subs: if s not in d[1]: d[1][s] = self._EMPTY_SECTION() d = d[1][s] lastsect = d continue raise ParsingError('Invalid line in {}: {} (line {})' ''.format(cfile, line, lno + 1)) return cdict
[docs] def _parse_subsections(self, re): """ Parse the sections hierarchy in a section line of a text file and return them in a list. :param re: regular expression object. """ if self._ENABLE_SUBSECTIONS: return re.group(1).split(self._SECTION_SEP) else: return (re.group(1), )
[docs] def _import_object(self, cobj, overwrite=True, add=True, reset=False): """ Import sections and options from a compatible object. :param cobj: A special object composed of dictionaries (or compatible mapping object) and tuples to be imported; a section is represented by a 2-tuple: its first value is a mapping object that associates the names of options to their values; its second value is a mapping object that associates the names of subsections to their 2-tuples. For example:: cobj = ( { 'option1': 'value', 'option2': 'value' }, { 'sectionA': ( { 'optionA1': 'value', 'optionA2': 'value', }, { 'sectionC': ( { 'optionC1': 'value', 'optionC2': 'value', }, {}, ), }, ), 'sectionB': ( { 'optionB1': 'value', 'optionB2': 'value' }, {}, ), }, ) :param bool overwrite: Whether imported data will overwrite pre-existing data. :param bool add: Whether non-pre-existing data will be imported. :param bool reset: Whether pre-existing data will be cleared. """ # TODO: Change "reset" mode to "remove" (complementing "overwrite" and # "add") (bug #25) if reset: self._options = self._DICT_CLASS() self._subsections = self._DICT_CLASS() for o in cobj[0]: if isinstance(o, str) and isinstance(cobj[0][o], str) and \ re_.match(self._OPTION, o, self._RE_I) and \ re_.match(self._VALUE, cobj[0][o], self._RE_I): self._import_object_option(overwrite, add, reset, o, cobj[0][o]) else: raise InvalidObjectError('Invalid option or value: {}: {}' ''.format(o, cobj[0][o])) for s in cobj[1]: if isinstance(s, str) and re_.match(self._SECTION, s, self._RE_I): self._import_object_subsection(overwrite, add, reset, s, cobj[1][s]) else: raise InvalidObjectError('Invalid section name: {}'.format(s))
[docs] def _import_object_option(self, overwrite, add, reset, opt, val): """ Auxiliary method for :py:meth:`_import_object`. Import the currently-examined option. """ if reset: self._options[opt] = val return True if self._IGNORE_CASE: for o in self._options: if opt.lower() == o.lower(): # Don't even think of merging these two tests if overwrite: self._options[o] = val return True break else: # Going through the loop above makes sure the option is not yet # in the section if add: self._options[opt] = val return True elif opt in self._options: # Don't even think of merging these two tests if overwrite: self._options[opt] = val return True elif add: self._options[opt] = val return True return False
[docs] def _import_object_subsection(self, overwrite, add, reset, sec, secd): """ Auxiliary method for :py:meth:`_import_object`. Import the currently-examined subsection. """ if reset: self._import_object_subsection_create(overwrite, add, sec, secd) return True if self._IGNORE_CASE: for ss in self._subsections: if sec.lower() == ss.lower(): # Don't test overwrite here self._subsections[ss]._import_object(secd, overwrite=overwrite, add=add) return True else: # Going through the loop above makes sure the section is not # yet a subsection of the visited section if add: self._import_object_subsection_create(overwrite, add, sec, secd) return True elif sec in self._subsections: # Don't test overwrite here self._subsections[sec]._import_object(secd, overwrite=overwrite, add=add) return True elif add: self._import_object_subsection_create(overwrite, add, sec, secd) return True return False
[docs] def _import_object_subsection_create(self, overwrite, add, sec, secd): """ Auxiliary method for :py:meth:`_import_object_subsection`. Import the currently-examined subsection. """ subsection = Section(name=sec, parent=self, safe_calls=self._SAFE_CALLS, inherit_options=self._INHERIT_OPTIONS, subsections=self._ENABLE_SUBSECTIONS, ignore_case=self._IGNORE_CASE) subsection._import_object(secd, overwrite=overwrite, add=add) self._subsections[sec] = subsection
[docs] def _interpolate(self): """ Interpolate values among different options. The ``$`` sign is a special character: a ``$`` not followed by ``$``, ``{``, ``:`` or ``}`` will be left ``$``; ``$$`` will be translated as ``$`` both inside or outside an interpolation path; ``${`` will be considered as the beginning of an interpolation path, unless it is found inside another interpolation path, and in the latter case it will be left ``${``; ``$:`` will be considered as a separator between sections of an interpolation path, unless it is found outside of an interpolation path, and in the latter case it will be left ``$:``; ``$}`` will be considered as the end of an interpolation path, unless it is found outside of an interpolation path, and in the latter case it will be left ``$}``. Normally all paths will be resolved based on the root section of the file; anyway, if the interpolation path has only one item, it will be resolved as an option relative to the current section; otherwise, if the path starts with ``$:``, the first item will be considered as a section (or an option, if last in the list) relative to the current section. """ try: root = self._get_ancestors()[-1] except IndexError: root = self for optname in self._options: split = re_.split(self._INTERPOLATION_SPLIT, self._options[optname]) value = '' resolve = None for chunk in split: if resolve is None: if chunk == self._INTERPOLATION_SPECIAL_ESC: value += self._INTERPOLATION_SPECIAL elif chunk == self._INTERPOLATION_START: resolve = [''] else: value += chunk else: if chunk == self._INTERPOLATION_SPECIAL_ESC: resolve[-1] += self._INTERPOLATION_SPECIAL elif chunk == self._INTERPOLATION_SEP: resolve.append('') elif chunk == self._INTERPOLATION_END: intoptname = resolve.pop() if len(resolve) == 0: # TODO: It's currently not possible to write a # reference to a root option?!? intsection = self else: if resolve[0] == '': intsection = self resolve.pop(0) else: intsection = root for s in resolve: intsection = intsection._subsections[s] # Use get(intoptname) instead of _options[intoptname] # so that options are properly inherited if the object # is configured to do so value += intsection.get(intoptname) resolve = None else: resolve[-1] += chunk if resolve is not None: # The last interpolation wasn't closed, so interpret it as a # normal string value += self._INTERPOLATION_START + \ self._INTERPOLATION_SEP.join(resolve) self._options[optname] = value for secname in self._subsections: self._subsections[secname]._interpolate()
### EXPORTING DATA ###
[docs] def get(self, opt, fallback=None, inherit_options=None): """ Returns the value for the option specified. :param str opt: The name of the option whose value must be returned. :param fallback: If set to a string, and the option is not found, this method returns that string; if set to None (default) it returns KeyError. :type fallback: str or None :param bool inherit_options: If True, if the option is not found in the current section, it is searched in the parent sections; note that this can be set as a default for the object, but this setting overwrites it only for this call. """ if inherit_options not in (True, False): inherit_options = self._INHERIT_OPTIONS if isinstance(opt, str): slist = [self, ] if inherit_options: slist.extend(self._get_ancestors()) for s in slist: for o in s._options: if (self._IGNORE_CASE and opt.lower() == o.lower()) or \ (not self._IGNORE_CASE and opt == o): return s._options[o] else: # Note that if fallback is not specified, this returns None # which is not a string as expected return fallback else: raise TypeError('Option name must be a string: {}'.format(opt))
[docs] def get_str(self, opt, fallback=None, inherit_options=None): """ This is an alias for :py:meth:`get`. This will always return a string. :param str opt: The name of the option whose value must be returned. :param fallback: If set to a string, and the option is not found, this method returns that string; if set to None (default) it returns KeyError. :type fallback: str or None :param bool inherit_options: If True, if the option is not found in the current section, it is searched in the parent sections; note that this can be set as a default for the object, but this setting overwrites it only for this call. """ if inherit_options not in (True, False): inherit_options = self._INHERIT_OPTIONS return self.get(opt, fallback=fallback, inherit_options=inherit_options)
[docs] def get_int(self, opt, fallback=None, inherit_options=None): """ This method tries to return an integer from the value of an option. :param str opt: The name of the option whose value must be returned. :param fallback: If set to a string, and the option is not found, this method returns that string; if set to None (default) it returns KeyError. :type fallback: str or None :param bool inherit_options: If True, if the option is not found in the current section, it is searched in the parent sections; note that this can be set as a default for the object, but this setting overwrites it only for this call. """ if inherit_options not in (True, False): inherit_options = self._INHERIT_OPTIONS return int(self.get(opt, fallback=fallback, inherit_options=inherit_options))
[docs] def get_float(self, opt, fallback=None, inherit_options=None): """ This method tries to return a float from the value of an option. :param str opt: The name of the option whose value must be returned. :param fallback: If set to a string, and the option is not found, this method returns that string; if set to None (default) it returns KeyError. :type fallback: str or None :param bool inherit_options: If True, if the option is not found in the current section, it is searched in the parent sections; note that this can be set as a default for the object, but this setting overwrites it only for this call. """ if inherit_options not in (True, False): inherit_options = self._INHERIT_OPTIONS return float(self.get(opt, fallback=fallback, inherit_options=inherit_options))
[docs] def get_bool(self, opt, true=(), false=(), default=None, fallback=None, inherit_options=None): """ This method tries to return a boolean status (True or False) from the value of an option. :param str opt: The name of the option whose value must be returned. :param tuple true: A tuple with the strings to be recognized as True. :param tuple false: A tuple with the strings to be recognized as False. :param default: If the value is neither in true nor in false tuples, return this boolean status; if set to None, it raises a ValueError exception. :param fallback: If set to None (default), and the option is not found, it raises KeyError; otherwise this value is evaluated with the true and false tuples, or the default value. :param bool inherit_options: If True, if the option is not found in the current section, it is searched in the parent sections; note that this can be set as a default for the object, but this setting overwrites it only for this call. Note that the characters in the strings are compared in lowercase, so there is no need to specify all casing variations of a string. """ # TODO: Use default values in definition with Settings class (bug #19) if true == (): true = self._GET_BOOLEAN_TRUE if false == (): false = self._GET_BOOLEAN_FALSE if default not in (True, False): default = self._GET_BOOLEAN_DEFAULT if inherit_options not in (True, False): inherit_options = self._INHERIT_OPTIONS v = str(self.get(opt, fallback=fallback, inherit_options=inherit_options)).lower() if v in true: return True elif v in false: return False elif default in (True, False): return default else: raise ValueError('Unrecognized boolean status: {}'.format( self[opt]))
[docs] def _get_ancestors(self): """ Return a list with the ancestors of the current section, but not the current section itself. """ slist = [] p = self._PARENT while p: slist.append(p) p = p._PARENT return slist
[docs] def _get_descendants(self): """ Return a list with the descendants of the current section, but not the current section itself. """ # Don't do `slist = self._subsections.values()` because the descendants # for each subsection must be appended after the proper subsection, # not at the end of the list slist = [] for section in self._subsections.values(): slist.append(section) slist.extend(section._get_descendants()) return slist
[docs] def get_options(self, ordered=True, inherit_options=None): """ Return a dictionary with a copy of option names as keys and their values as values. :param bool ordered: If True, return an ordered dictionary; otherwise return a normal dictionary. :param bool inherit_options: If True, options are searched also in the parent sections; note that this can be set as a default for the object, but this setting overwrites it only for this call. """ if inherit_options not in (True, False): inherit_options = self._INHERIT_OPTIONS if ordered: d = self._DICT_CLASS() else: d = {} slist = [self, ] if inherit_options: slist.extend(self._get_ancestors()) for s in slist: for o in s._options: d.setdefault(o, s._options[o][:]) # There should be no need to check _IGNORE_CASE, in fact it has # already been done at importing time return d
[docs] def get_sections(self): """ Return a view of the names of the child sections. """ return self._subsections.keys()
[docs] def get_tree(self, ordered=True, path=False): """ Return a compatible object with options and subsections. :param bool ordered: If True, the object uses ordered dictionaries; otherwise it uses normal dictionaries. :param bool path: If True, return the current section as a subsection of the parent sections. """ d = self._recurse_tree(ordered=ordered) if path: p = self._PARENT n = self._NAME while p: if ordered: e = self._EMPTY_SECTION() else: e = ({}, {}) e[1][n] = d d = e n = p._NAME p = p._PARENT return d
[docs] def _recurse_tree(self, ordered=True): """ Auxiliary recursor for :py:meth:`get_tree`. """ options = self.get_options(ordered=ordered, inherit_options=False) if ordered: d = (options, self._DICT_CLASS()) else: d = (options, {}) for s in self._subsections: d[1][s] = self._subsections[s]._recurse_tree(ordered=ordered) return d
[docs] def _export(self, targets, overwrite=True, add=True, reset=False, path=True): """ Export the configuration to one or more files. :param targets: A sequence with the target file names. :param bool overwrite: This sets whether sections and options in the file are overwritten; see _import_object for more details. :param bool add: This sets whether non-pre-existing sections and option are added; see _import_object for more details. :param bool path: If True, section names are exported with their full path. """ # TODO: Change "reset" mode to "remove" (complementing "overwrite" and # "add") (bug #25) for f in targets: self._export_file(f, overwrite=overwrite, add=add, reset=reset, path=path)
[docs] def export_upgrade(self, *targets, **kwargs): """ Export sections and options to one or more files with upgrade mode. If an option already exists, change its value; if it does not exist, create it and store its value. For example: *{A:d,D:e} upgrade {A:a,B:b,C:c} => {A:d,B:b,C:c,D:e}* See :py:meth:`_export_file` for object compatibility. :param targets: A sequence with the target file names. :param bool path: If True, section names are exported with their full path. """ # Necessary for Python 2 compatibility # The Python 3 definition was: #def export_upgrade(self, *targets, path=True): path = kwargs.get('path', True) self._export(targets, path=path)
[docs] def export_update(self, *targets, **kwargs): """ Export sections and options to one or more files with update mode. If an option already exists, change its value; if it does not exist, do not do anything. For example: *{A:d,D:e} update {A:a,B:b,C:c} => {A:d,B:b,C:c}* See :py:meth:`_export_file` for object compatibility. :param targets: A sequence with the target file names. :param bool path: If True, section names are exported with their full path. """ # Necessary for Python 2 compatibility # The Python 3 definition was: #def export_upgrade(self, *targets, path=True): path = kwargs.get('path', True) self._export(targets, add=False, path=path)
[docs] def export_reset(self, *targets, **kwargs): """ Export sections and options to one or more files with reset mode. Delete all options and subsections and recreate everything from the importing object. For example: *{A:d,D:e} reset {A:a,B:b,C:c} => {A:d,D:e}* See :py:meth:`_export_file` for object compatibility. :param targets: A sequence with the target file names. :param bool path: If True, section names are exported with their full path. """ # Necessary for Python 2 compatibility # The Python 3 definition was: #def export_upgrade(self, *targets, path=True): path = kwargs.get('path', True) self._export(targets, reset=True, path=path)
[docs] def export_add(self, *targets, **kwargs): """ Export sections and options to one or more files with add mode. If an option already exists, do not do anything; if it does not exist, create it and store its value. For example: *{A:d,D:e} add {A:a,B:b,C:c} => {A:a,B:b,C:c,D:e}* See :py:meth:`_export_file` for object compatibility. :param targets: A sequence with the target file names. :param bool path: If True, section names are exported with their full path. """ # Necessary for Python 2 compatibility # The Python 3 definition was: #def export_upgrade(self, *targets, path=True): path = kwargs.get('path', True) self._export(targets, overwrite=False, path=path)
[docs] def _export_file(self, cfile, overwrite=True, add=True, reset=False, path=True): """ Export the sections tree to a file. :param str efile: The target file name. :param bool overwrite: Whether sections and options already existing in the file are overwritten. :param bool add: Whether non-pre-existing data will be exported. :param bool path: If True, section names are exported with their full path. """ try: with open(cfile, 'r') as stream: lines = stream.readlines() except IOError: lines = [] else: # Exclude leading blank lines for lineN, line in enumerate(lines): if not re_.match(self._PARSE_IGNORE, line, self._RE_I): lines = lines[lineN:] break else: lines = [] with open(cfile, 'w') as stream: BASE_SECTION = self try: ROOT_SECTION = self._get_ancestors()[-1] except IndexError: ROOT_SECTION = self readonly_section = False remaining_descendants = [] else: if path: readonly_section = True remaining_descendants = [BASE_SECTION, ] else: # The options without a section (i.e. at the top of the # file) must be considered part of the current section if # path is False readonly_section = False remaining_descendants = [] remaining_options = BASE_SECTION.get_options(inherit_options=False) remaining_descendants.extend(BASE_SECTION._get_descendants()) other_lines = [] for line in lines: re_option = re_.match(self._PARSE_OPTION, line, self._RE_I) if re_option: # This also changes other_lines in place self._export_other_lines(stream, other_lines, readonly_section, reset) self._export_file_existing_option(stream, line, re_option, readonly_section, remaining_options, overwrite, reset) continue re_section = re_.match(self._PARSE_SECTION, line, self._RE_I) if re_section: if add: self._export_file_remaining_options(stream, readonly_section, remaining_options) # This also changes other_lines in place self._export_other_lines_before_existing_section(stream, other_lines, readonly_section, reset) # This also changes remaining_descendants in place (readonly_section, remaining_options) = \ self._export_file_existing_section( stream, line, re_section, ROOT_SECTION, BASE_SECTION, remaining_descendants, path) continue # Comments, ignored/invalid lines other_lines.append(line) if add: self._export_file_remaining_options(stream, readonly_section, remaining_options) # Don't use _export_other_lines_before_existing_section here # because any pre-existing unrecognized lines must be restored in # any case, and since they're at the end of the original file, # they weren't meant to separate any further sections, so let # _export_file_remaining_sections handle the addition of a blank # line # This also changes other_lines in place self._export_other_lines(stream, other_lines, readonly_section, reset) if add: self._export_file_remaining_sections(stream, BASE_SECTION, remaining_descendants, path)
[docs] def _export_file_existing_option(self, stream, line, re_option, readonly_section, remaining_options, overwrite, reset): """ Auxiliary method for :py:meth:`_export_file`. Write the option currently examined from the destination file. """ if readonly_section: stream.write(line) return True if self._IGNORE_CASE: for option in remaining_options: fkey = re_option.group(1) fvalue = re_option.group(2) if fkey.lower() == option.lower(): if overwrite and fvalue != remaining_options[option]: stream.write(''.join((fkey, self._OPTION_SEP, remaining_options[option], '\n'))) else: stream.write(line) del remaining_options[option] # There shouldn't be more occurrences of this option (even # with different casing) return True else: fkey = re_option.group(1) fvalue = re_option.group(2) if fkey in remaining_options: if overwrite and remaining_options[fkey] != fvalue: stream.write(''.join((fkey, self._OPTION_SEP, remaining_options[fkey], '\n'))) else: stream.write(line) del remaining_options[fkey] return True if not reset: stream.write(line) return True return False
[docs] def _export_file_remaining_options(self, stream, readonly_section, remaining_options): """ Auxiliary method for :py:meth:`_export_file`. Write the options from the origin object that were not found in the destination file. """ if not readonly_section: for option in remaining_options: stream.write(''.join((option, self._OPTION_SEP, remaining_options[option], '\n')))
[docs] def _export_file_existing_section(self, stream, line, re_section, ROOT_SECTION, BASE_SECTION, remaining_descendants, path): """ Auxiliary method for :py:meth:`_export_file`. Write the section currently examined from the destination file. """ if self._ENABLE_SUBSECTIONS: names = re_section.group(1).split(self._SECTION_SEP) else: names = (re_section.group(1), ) current_section = ROOT_SECTION if path else BASE_SECTION for name in names: try: current_section = current_section(name) except KeyError: # The currently parsed section is not in the configuration # object readonly_section = True remaining_options = self._DICT_CLASS() break else: alist = [current_section, ] alist.extend(current_section._get_ancestors()) if BASE_SECTION in alist: readonly_section = False remaining_options = current_section.get_options( inherit_options=False) remaining_descendants.remove(current_section) else: readonly_section = True remaining_options = self._DICT_CLASS() # TODO: If reset (which for all the other modes by default is "deep", # i.e. it must affect the subsections too) this section and all # the other "old" subsections must be removed from the file # (bug #22) stream.write(line) return (readonly_section, remaining_options)
[docs] def _export_file_remaining_sections(self, stream, BASE_SECTION, remaining_descendants, path): """ Auxiliary method for :py:meth:`_export_file`. Write the sections and their options from the origin object that were not found in the destination file. """ # Do not add an empty line if at the start of the file BR = "\n" if stream.tell() > 0 else "" for section in remaining_descendants: if len(section._options) > 0: ancestors = [section._NAME, ] for ancestor in section._get_ancestors()[:-1]: if not path and ancestor is BASE_SECTION: break ancestors.append(ancestor._NAME) ancestors.reverse() stream.write("".join((BR, self._SECTION_MARKERS, "\n") ).format(self._SECTION_SEP.join(ancestors))) for option in section._options: stream.write("".join((option, self._OPTION_SEP, section[option], "\n"))) # All the subsequent sections will need a blank line in any # case (do not add a double line break after the last option # because the last option of the last section must have only # one break) BR = "\n"
[docs] def _export_other_lines(self, stream, other_lines, readonly_section, reset): """ Auxiliary method for :py:meth:`_export_file`. """ if readonly_section or not reset: stream.writelines(other_lines) other_lines[:] = []
[docs] def _export_other_lines_before_existing_section(self, stream, other_lines, readonly_section, reset): """ Auxiliary method for :py:meth:`_export_file`. """ if readonly_section or not reset: stream.writelines(other_lines) elif stream.tell() > 0: stream.write("\n") other_lines[:] = []
[docs]class ConfigFile(Section): """ The main configuration object. """
[docs] def __init__(self, *sources, **kwargs): """ Constructor. :param sources: A sequence of all the files, file-like objects, dictionaries and special objects to be parsed. :type sources: str, dict or special object (see :py:meth:`Section._import_object`) :param str mode: This sets if and how the next source in the chain overwrites already imported sections and options; available choices are ``'upgrade'``, ``'update'``, ``'reset'`` and ``'add'`` (see the respective methods for more details). :param bool safe_calls: If True, when calling a non-existent subsection, its closest existing ancestor is returned. :param bool inherit_options: If True, if an option is not found in a section, it is searched in the parent sections. :param bool ignore_case: If True, section and option names will be compared ignoring case differences; regular expressions will use ``re.I`` flag. :param bool subsections: If True (default) subsections are allowed. :param bool interpolation: If True, option values will be interpolated using values from other options through the special syntax ``${section$:section$:option$}``. Options will be interpolated only once at importing: all links among options will be lost after importing. """ # The Python 3 definition was: #def __init__(self, # *sources, # mode='upgrade', # safe_calls=False, # inherit_options=False, # subsections=True, # ignore_case=True, # interpolation=False): # But to keep compatibility with Python 2 it has been changed to the # current mode = kwargs.get('mode', 'upgrade') safe_calls = kwargs.get('safe_calls', False) inherit_options = kwargs.get('inherit_options', False) subsections = kwargs.get('subsections', True) ignore_case = kwargs.get('ignore_case', True) interpolation = kwargs.get('interpolation', False) # Root section Section.__init__(self, name=None, parent=None, safe_calls=safe_calls, inherit_options=inherit_options, subsections=subsections, ignore_case=ignore_case) try: overwrite, add, reset = { "upgrade": (True, True, False), "update": (True, False, False), "reset": (True, True, True), "add": (False, True, False), }[mode] except KeyError: raise ValueError('Unrecognized importing mode: {}'.format(mode)) self._import(sources, overwrite=overwrite, add=add, reset=reset, interpolation=interpolation)
### EXCEPTIONS ###
[docs]class ConfigFileError(Exception): """ The root exception, useful for catching generic errors from this module. """ pass
[docs]class ParsingError(ConfigFileError): """ An error, overcome at parse time, due to bad file formatting. """ pass
[docs]class NonExistentFileError(ConfigFileError): """ A non-existent configuration file. """ pass
[docs]class InvalidFileError(ConfigFileError): """ An invalid configuration file. """ pass
[docs]class InvalidObjectError(ConfigFileError): """ An invalid key found in an importing object. """ pass