view configmix/ini.py @ 654:0d6673d06c2c

Add support for using "tomllib" (in Python's stdlib since 3.11) and "tomli" TOML packages. They are preferred if they are found to be installed. But note that the declared dependency for the "toml" extra nevertheless is the "toml" package. Because it is available for all supported Python versions. So use Python 3.11+ or install "tomli" manually if you want to use the alternate packages.
author Franz Glasner <fzglas.hg@dom66.de>
date Thu, 19 May 2022 22:10:59 +0200
parents 301cf2337fde
children
line wrap: on
line source

# -*- coding: utf-8 -*-
# :-
# :Copyright: (c) 2015-2022, 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 dot ``.``.

    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