Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save amendoncabh/07847badb0336a4d02439fc7dbcb4c9b to your computer and use it in GitHub Desktop.
Save amendoncabh/07847badb0336a4d02439fc7dbcb4c9b to your computer and use it in GitHub Desktop.
A simple abstraction of Python’s ConfigParser
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# A simple abstraction of Python’s ConfigParser.
# It features implicit type conversion and defaults through prior
# registration of settings. It can be used to save and read settings
# without bothering about the specifics of ConfigParser or the INI files
# themselves. It could also serve as a starting point to abstract
# platform-specific saving methods through its general API.
# Standard modules
import collections
import collections.abc
import re
import configparser
from urllib import parse
class Configuration(collections.abc.MutableMapping):
"""INI file based configuration manager"""
__slots__ = ("_defaults", "_parser", "_section")
_key_chars = re.compile("\\w*", re.ASCII)
_types = frozenset((bool, int, float, str, bytes))
def __init__(self, product: str) -> None:
"""Initialize the configuration manager."""
self._defaults = {}
self._parser = configparser.RawConfigParser(
None, dict, False,
delimiters=("=",),
comment_prefixes=(";",),
inline_comment_prefixes=None,
strict=True,
empty_lines_in_values=False,
default_section=None,
interpolation=None
)
self._section = str(product)
self._parser.add_section(self._section)
def __getitem__(self, identifier: str):
"""Return value of `identifier` or the registered default on errors.
KeyError is raised if there is no such identifier.
"""
default = self._defaults[identifier]
if self._parser.has_option(self._section, identifier):
dtype = type(default)
try:
if dtype is bool:
return self._parser.getboolean(self._section, identifier, raw=True)
if dtype is int:
return self._parser.getint(self._section, identifier, raw=True)
if dtype is float:
return self._parser.getfloat(self._section, identifier, raw=True)
if dtype is str:
return parse.unquote(self._parser.get(self._section, identifier, raw=True), errors="strict")
if dtype is bytes:
return parse.unquote_to_bytes(self._parser.get(self._section, identifier, raw=True))
except ValueError:
self._parser.remove_option(self._section, identifier)
return default
def __setitem__(self, identifier: str, value) -> None:
"""Set `identifier` to `value`.
KeyError is raised if `identifier` was not registered.
TypeError is raised if `value` does not match the registered type.
"""
dtype = type(self._defaults[identifier])
if dtype is bool is type(value):
self._parser.set(self._section, identifier, "yes" if value else "no")
elif dtype is int is type(value) \
or dtype is float is type(value):
self._parser.set(self._section, identifier, str(value))
elif dtype is str is type(value):
self._parser.set(self._section, identifier, parse.quote(value))
elif dtype is bytes is type(value):
self._parser.set(self._section, identifier, parse.quote_from_bytes(value))
else:
raise TypeError("Not matching registered type")
def __delitem__(self, identifier: str) -> None:
"""Remove customized value of `identifier`.
Nothing is done if the value was not customized,
but KeyError is raised if `identifier` was not registered."""
if identifier in self._defaults:
self._parser.remove_option(self._section, identifier)
else:
raise KeyError(identifier)
def __iter__(self):
"""Return an iterator over all registered identifiers."""
return iter(self._defaults.keys())
def __len__(self) -> int:
"""Return the number of registered settings."""
return len(self._defaults)
def __contains__(self, identifier) -> bool:
"""Return True if `identifier` is registered, else False."""
return identifier in self._defaults
def keys(self):
"""Return a set-like object providing a view on registered identifiers."""
return self._defaults.keys()
def clear(self) -> None:
"""Remove all customized values, reverting to the registered defaults."""
for identifier in self._defaults.keys():
self._parser.remove_option(self._section, identifier)
def register(self, identifier: str, default) -> None:
"""Register a setting and its default value.
Identifiers must consist of only letters, digits and underscores.
The type of `default` also specifies the type returned later
and what can be assigned.
Supported types are bool, int, float, str, and bytes.
"""
if type(identifier) is not str:
raise TypeError("Identifiers must be strings")
if not identifier:
raise ValueError("Identifiers must not be empty")
if not self._key_chars.fullmatch(identifier):
raise ValueError("Identifier contains invalid characters")
if identifier in self._defaults:
raise ValueError("Identifier already registered")
if type(default) not in self._types:
raise TypeError("Unsupported type")
self._defaults[identifier] = default
def get_default(self, identifier: str):
"""Return the default value of `identifier`.
KeyError is raised if there is no such identifier.
"""
return self._defaults[identifier]
def load(self, file: str) -> None:
"""Read and parse a configuration file."""
with open(file, encoding="ascii") as stream:
self._parser.read_file(stream)
def save(self, file: str) -> None:
"""Save the configuration."""
with open(file, "w", encoding="ascii") as stream:
print("; This file is written by", self._section, "when it quits.", file=stream)
print("; It is not recommended to do manual changes.", file=stream)
self._parser.write(stream, False)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment