Source code for easydev.config_tools

# -*- python -*-
# -*- coding: utf-8 -*-
#
#  This file is part of the easydev software
#
#  Copyright (c) 2011-2024
#
#  File author(s): Thomas Cokelaer <cokelaer@gmail.com>
#
#  Distributed under BSD3 license
#
#  Website: https://github.com/cokelaer/easydev
#  Documentation: http://packages.python.org/easydev
#
##############################################################################
import os
from configparser import ConfigParser

__all__ = ["CustomConfig", "DynamicConfigParser"]


class _DictSection:
    """Dictionary section.

    Reference: https://gist.github.com/dangoakachan/3855920

    """

    def __init__(self, config, section):
        object.__setattr__(self, "_config", config)
        object.__setattr__(self, "_section", section)

    def __getattr__(self, attr):
        return self.get(attr, None)

    __getitem__ = __getattr__

    def get(self, attr, default=None):
        if attr in self:
            return self._config.get(self._section, attr)
        else:  # pragma: no cover
            return default

    def __setattr__(self, attr, value):
        if attr.startswith("_"):
            object.__setattr__(self, attr, value)
        else:  # pragma: no cover
            self.__setitem__(attr, value)

    def __setitem__(self, attr, value):
        if self._section not in self._config:  # pragma: no cover
            self._config.add_section(self._section)

        self._config.set(self._section, attr, str(value))

    def __delattr__(self, attr):
        if attr in self:
            self._config.remove_option(self._section, attr)

    __delitem__ = __delattr__

    def __contains__(self, attr):
        config = self._config
        section = self._section

        return config.has_section(section) and config.has_option(section, attr)


[docs]class DynamicConfigParser(ConfigParser): """Enhanced version of Config Parser Provide some aliases to the original ConfigParser class and new methods such as :meth:`save` to save the config object in a file. .. code-block:: python >>> from easydev.config_tools import ConfigExample >>> standard_config_file = ConfigExample().config >>> c = DynamicConfigParser(standard_config_file) >>> >>> # then to get the sections, simply type as you would normally do with ConfigParser >>> c.sections() >>> # or for the options of a specific sections: >>> c.get_options('General') You can now also directly access to an option as follows:: c.General.tag Then, you can add or remove sections (:meth:`remove_section`, :meth:`add_section`), or option from a section :meth:`remove_option`. You can save the instance into a file or print it:: print(c) .. warning:: if you set options manually (e.G. self.GA.test =1 if GA is a section and test one of its options), then the save/write does not work at the moment even though if you typoe self.GA.test, it has the correct value Methods inherited from ConfigParser are available: :: # set value of an option in a section c.set(section, option, value=None) # but with this class, you can also use the attribute c.section.option = value # set value of an option in a section c.remove_option(section, option) c.remove_section(section) """ def __init__(self, config_or_filename=None, *args, **kargs): object.__setattr__(self, "_filename", config_or_filename) # why not a super usage here ? Maybe there were issues related # to old style class ? ConfigParser.__init__(self, *args, **kargs) if isinstance(self._filename, str) and os.path.isfile(self._filename): self.read(self._filename) elif isinstance(config_or_filename, ConfigParser): self._replace_config(config_or_filename) elif config_or_filename == None: pass else: raise TypeError("config_or_filename must be a valid filename or valid ConfigParser instance")
[docs] def read(self, filename): """Load a new config from a filename (remove all previous sections)""" if os.path.isfile(filename) == False: raise IOError("filename {0} not found".format(filename)) config = ConfigParser() config.read(filename) self._replace_config(config)
def _replace_config(self, config): """Remove all sections and add those from the input config file :param config: """ for section in self.sections(): self.remove_section(section) for section in config.sections(): self.add_section(section) for option in config.options(section): data = config.get(section, option) self.set(section, option, data)
[docs] def get_options(self, section): """Alias to get all options of a section in a dictionary One would normally need to extra each option manually:: for option in config.options(section): config.get(section, option, raw=True)# then, populate a dictionary and finally take care of the types. .. warning:: types may be changed .For instance the string "True" is interpreted as a True boolean. .. seealso:: internally, this method uses :meth:`section2dict` """ return self.section2dict(section)
[docs] def section2dict(self, section): """utility that extract options of a ConfigParser section into a dictionary :param ConfigParser config: a ConfigParser instance :param str section: the section to extract :returns: a dictionary where key/value contains all the options/values of the section required Let us build up a standard config file: .. code-block:: python >>> # Python 3 code >>> from configparser import ConfigParser >>> c = ConfigParser() >>> c.add_section('general') >>> c.set('general', 'step', str(1)) >>> c.set('general', 'verbose', 'True') To access to the step options, you would write:: >>> c.get('general', 'step') this function returns a dictionary that may be manipulated as follows:: >>> d_dict.general.step .. note:: a value (string) found to be True, Yes, true, yes is transformed to True .. note:: a value (string) found to be False, No, no, false is transformed to False .. note:: a value (string) found to be None; none, "" (empty string) is set to None .. note:: an integer is cast into an int """ options = {} for option in self.options(section): # pragma no cover data = self.get(section, option, raw=True) if data.lower() in ["true", "yes"]: options[option] = True elif data.lower() in ["false", "no"]: options[option] = False elif data in ["None", None, "none", ""]: options[option] = None else: try: # numbers try: options[option] = self.getint(section, option) except: options[option] = self.getfloat(section, option) except: # string options[option] = self.get(section, option, raw=True) return options
[docs] def save(self, filename): """Save all sections/options to a file. :param str filename: a valid filename :: config = ConfigParams('config.ini') #doctest: +SKIP config.save('config2.ini') #doctest: +SKIP """ try: if os.path.exists(filename) == True: print("Warning: over-writing %s " % filename) fp = open(filename, "w") except Exception as err: # pragma: no cover print(err) raise Exception("filename could not be opened") self.write(fp) fp.close()
[docs] def add_option(self, section, option, value=None): """add an option to an existing section (with a value) .. code-block:: python >>> c = DynamicConfigParser() >>> c.add_section("general") >>> c.add_option("general", "verbose", True) """ assert section in self.sections(), "unknown section" # TODO I had to cast to str with DictSection self.set(section, option, value=str(value))
def __str__(self): str_ = "" for section in self.sections(): str_ += "[" + section + "]\n" for option in self.options(section): data = self.get(section, option, raw=True) str_ += option + " = " + str(data) + "\n" str_ += "\n\n" return str_ def __getattr__(self, key): return _DictSection(self, key) __getitem__ = __getattr__ def __setattr__(self, attr, value): if attr.startswith("_") or attr: object.__setattr__(self, attr, value) else: # pragma: no cover self.__setitem__(attr, value) # def __setitem__(self, attr, value): # if isinstance(value, dict): # section = self[attr] # for k, v in value.items(): # section[k] = v # else: # raise TypeError("value must be a valid dictionary") def __delattr__(self, attr): if attr in self: self.remove_section(attr) def __contains__(self, attr): return self.has_section(attr) def __eq__(self, data): # FIXME if you read file, the string "True" is a string # but you may want it to be converted to a True boolean value if sorted(data.sections()) != sorted(self.sections()): print("Sections differ") return False for section in self.sections(): for option in self.options(section): try: if str(self.get(section, option, raw=True)) != str(data.get(section, option, raw=True)): print("option %s in section %s differ" % (option, section)) return False except: # pragma: no cover return False return True
[docs]class CustomConfig(object): """Base class to manipulate a config directory""" def __init__(self, name, verbose=False): self.verbose = verbose from easydev import appdirs self.appdirs = appdirs.AppDirs(name) def init(self): sdir = self.appdirs.user_config_dir self._get_and_create(sdir) def _get_config_dir(self): sdir = self.appdirs.user_config_dir return self._get_and_create(sdir) user_config_dir = property(_get_config_dir, doc="return directory of this configuration file") def _get_and_create(self, sdir): if not os.path.exists(sdir): print("Creating directory %s " % sdir) try: self._mkdirs(sdir) except Exception: # pragma: no cover print("Could not create the path %s " % sdir) return None return sdir def _mkdirs(self, newdir, mode=0o777): """See :func:`easydev.tools.mkdirs`""" from easydev.tools import mkdirs mkdirs(newdir, mode) def remove(self): try: sdir = self.appdirs.user_config_dir os.rmdir(sdir) except Exception as err: # pragma: no cover raise Exception(err)