view configmix/ini.py @ 428:090a25f36a3d

FIX: Allow jailed configurations to use correctly use base configurations that use a different "default" marker object. Jailed configurations assumed that their "default" marker object is identical to the "default" marker object in the unjailed base configuration. This is not always true, especially if "_JailedConfiguration.rebind()" is used. Removed the explicit "default" keyword argument and passed the complete keywords argument dictionary to the base instead. This triggers correct default handling in the base.
author Franz Glasner <f.glasner@feldmann-mg.com>
date Thu, 09 Dec 2021 13:02:17 +0100
parents eed16a1ec8f3
children f454889e41fa
line wrap: on
line source

# -*- coding: utf-8 -*-
# :-
# :Copyright: (c) 2015-2020, Franz Glasner. All rights reserved.
# :License:   BSD-3-Clause. See LICENSE.txt for details.
# :-
"""Read INI-style configuration files.

"""

from __future__ import division, absolute_import, print_function


__all__ = ["INIConfigParser", "NoSectionError", "NoOptionError",
           "load"]


import sys
import os
import io
import locale
try:
    from configparser import ConfigParser, NoSectionError, NoOptionError
    # SafeConfigParser is deprecated in Python 3.2+ (together with "readfp()")
    if hasattr(ConfigParser, "read_file"):
        _ConfigParserBase = ConfigParser
    else:
        from configparser import SafeConfigParser
        _ConfigParserBase = SafeConfigParser
except ImportError:
    from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError
    _ConfigParserBase = SafeConfigParser
try:
    from collections import OrderedDict as DictImpl
except ImportError:
    try:
        from ordereddict import OrderedDict as DictImpl
    except ImportError:
        DictImpl = dict

from .compat import u, u2fs


class INIConfigParser(_ConfigParserBase):

    """A case sensitive config parser that returns all-unicode string
    values.

    """

    def __init__(self, filename, executable=None, encoding=None):
        _ConfigParserBase.__init__(self)
        if executable is None:
            executable = sys.argv[0]
        filename = u(filename, locale.getpreferredencoding())
        executable = u(executable, locale.getpreferredencoding())
        self.executable = os.path.normpath(os.path.abspath(executable))
        if encoding is None:
            encoding = locale.getpreferredencoding()
        self.encoding = encoding
        with io.open(u2fs(filename), mode="rt", encoding=self.encoding) as cf:
            self.read_file(cf, filename)

    def optionxform(self, option):
        return option

    def get_path_list(self, section, option):
        v = self.get(section, option)
        return v.split(os.pathsep)

    def read(self, filenames):
        """Not implemented. Use :meth:`read_file` instead."""
        raise NotImplementedError("use `read_file()' instead")

    def readfp(self, fp, filename):
        """Compatibility for older Python versions.

        Use :meth:`.read_file` instead.

        """
        return self.read_file(fp, filename)

    def read_file(self, fp, filename):
        """Read from a file-like object `fp`.

        The `fp` argument must be iterable (Python 3.2+) or have a
        `readline()` method (Python 2, <3.2).

        """
        if hasattr(self, "filename"):
            raise RuntimeError("already initialized")
        filename = os.path.normpath(os.path.abspath(filename))
        filename = u(filename, locale.getpreferredencoding())
        # self.set(None, u("self"), filename)
        # self.set(None, u("here"), os.path.dirname(filename))
        # self.set(None, u("root"), os.path.dirname(self.executable))
        if hasattr(_ConfigParserBase, "read_file"):
            _ConfigParserBase.read_file(self, fp, source=filename)
        else:
            _ConfigParserBase.readfp(self, fp, filename=filename)
        self.filename = filename
        # self.root = os.path.dirname(self.executable)

    def getx(self, section, option):
        """Extended `get()` with some automatic type conversion support.

        Default: Fetch as string (like :meth:`get`).

        If annotated with ``:bool:`` fetch as bool, if annotated with
        ``:int:`` fetch as int, if annotated with ``:float:`` fetch as
        float.

        """
        v = self.get(section, option)
        if v.startswith(u(":bool:")):
            v = v[6:].lower()
            if v not in self._BOOL_CVT:
                raise ValueError("Not a boolean: %r" % v)
            return self._BOOL_CVT[v]
        elif v.startswith(u(":int:")):
            return int(v[5:], 0)
        elif v.startswith(u(":float:")):
            return float(v[7:])
        else:
            return v

    _BOOL_CVT = {u('1'): True,
                 u('yes'): True,
                 u('true'): True,
                 u('on'): True,
                 u('0'): False,
                 u('no'): False,
                 u('false'): False,
                 u('off'): False}

    def itemsx(self, section, options):
        """Get all the options given in `options` of section `section`.
        Fetch them with :meth:`getx` in the order given.

        Return a list of ``(name, value)`` pairs for each option in
        `options` in the given `section`.

        """
        d = []
        for option in options:
            try:
                val = self.getx(section, option)
            except (NoSectionError, NoOptionError):
                pass
            else:
                d.append((option, val, ))
        return d

    def items_as_dictx(self, section, options):
        """Similar to :meth:`itemsx` but return a (possibly ordered)
        dict instead of a list of key-value pairs.

        """
        return DictImpl(self.itemsx(section, options))


def load(filename, extract=["config"],
         encoding="utf-8"):
    """Load a single INI file and read/interpolate the sections given in
    `extract`.

    Flattens the given sections into the resulting dictionary.

    Then build a tree out of sections which start with any of the `extract`
    content value and a point ``.``.

    The encoding of the file is given in `encoding`.

    """
    conf = DictImpl()
    ini = INIConfigParser(filename, encoding=encoding)
    for sect in extract:
        try:
            cfg = ini.options(sect)
        except NoSectionError:
            pass
        else:
            for option in cfg:
                value = ini.getx(sect, option)
                conf[option] = value
    # try to read "<extract>.xxx" sections as tree
    for treemarker in [e + '.' for e in extract]:
        sections = list(ini.sections())
        sections.sort()
        for section in sections:
            cur_cfg = conf
            if section.startswith(treemarker):
                treestr = section[len(treemarker):]
                for treepart in treestr.split('.'):
                    cur_cfg = cur_cfg.setdefault(treepart, DictImpl())
                for option in ini.options(section):
                    value = ini.getx(section, option)
                    cur_cfg[option] = value
    return conf