Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Bachsau
Last active March 2, 2023 15:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Bachsau/2feade1c2663d97b7b7fcadb923674c0 to your computer and use it in GitHub Desktop.
Save Bachsau/2feade1c2663d97b7b7fcadb923674c0 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)
@Bachsau
Copy link
Author

Bachsau commented Mar 1, 2020

If anyone asks why I built this around RawConfigParser and the "legacy" API: It is an abstraction layer itself, which already cares for typing, encoding and correct naming of identifiers, so there is no need to add additional overhead by routing access through another abstraction layer, and ConfigParser is nothing more than that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment