#!/usr/bin/python3
#
# Univention App Center
# Tools for reading ini files
#
# SPDX-FileCopyrightText: 2017-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
#
import codecs
import re
from configparser import DuplicateSectionError, NoOptionError, NoSectionError, ParsingError, RawConfigParser
from copy import deepcopy
from univention.appcenter.log import get_base_logger
from univention.appcenter.meta import UniventionMetaClass, UniventionMetaInfo
from univention.appcenter.utils import get_locale
ini_logger = get_base_logger().getChild('ini')
[docs]
class NoValueError(Exception):
def __init__(self, name, section):
self.name = name
self.section = section
def __str__(self):
return 'Missing %s in %s' % (self.name, self.section)
[docs]
class ParseError(Exception):
def __init__(self, name, section, message):
self.name = name
self.section = section
self.message = message
def __str__(self):
return 'Cannot parse %s in %s: %s' % (self.name, self.section, self.message)
[docs]
def read_ini_file(filename, parser_class=RawConfigParser):
parser = parser_class()
try:
with codecs.open(filename, 'r', 'utf-8') as f:
parser.read_file(f)
except TypeError:
pass
except OSError:
pass
except (DuplicateSectionError, ParsingError) as exc:
ini_logger.warning('Could not parse %s', filename)
ini_logger.warning(str(exc))
else:
return parser
# in case of error return empty parser
return parser_class()
[docs]
class IniSectionAttribute(UniventionMetaInfo):
save_as_dict = '_attrs'
pop = True
auto_set_name = True
def __init__(self, required=False, default=None, localisable=False, choices=None):
self.required = required
self.default = deepcopy(default)
self.localisable = localisable
self.choices = choices
def _canonical_name(self):
return self.name.replace('_', '')
@classmethod
def _fetch_from_parser(cls, parser, section, name):
return parser.get(section, name)
[docs]
def get(self, parser, section, locale):
name = self._canonical_name()
names = [name]
if self.localisable and locale:
names.insert(0, '%s[%s]' % (name, locale))
for name in names:
try:
value = self._fetch_from_parser(parser, section, name)
except (NoSectionError, NoOptionError):
pass
else:
try:
return self.parse(value)
except ValueError as exc:
raise ParseError(name, section, str(exc))
if self.required:
raise NoValueError(self.name, section)
return self.default
[docs]
def parse(self, value):
if self.choices and value not in self.choices:
raise ValueError('%r not in %r' % (value, self.choices))
return value
[docs]
class IniSectionBooleanAttribute(IniSectionAttribute):
def _fetch_from_parser(self, parser, section, name):
try:
return parser.getboolean(section, name)
except ValueError:
raise ParseError(name, section, 'Not a Boolean')
[docs]
class IniSectionListAttribute(IniSectionAttribute):
def __init__(self, required=False, default=[], localisable=False, choices=None):
super().__init__(required, default, localisable, choices)
[docs]
def parse(self, value):
'''
Returns a list; splits on "," (stripped, whitespaces before
and after are removed). If a single value needs to contain a
",", it can be escaped with backslash: "My \\, value".
'''
if value is None:
return []
value = re.split(r'(?<=[^\\])\s*,\s*', value)
values = [re.sub(r'\\,', ',', val) for val in value]
if self.choices:
for val in values:
if val not in self.choices:
raise ValueError('%r not in %r' % (val, self.choices))
return values
[docs]
class IniSectionObject(metaclass=UniventionMetaClass):
_main_attr_name = 'name'
def __init__(self, **kwargs):
for attr in self._attrs.values():
setattr(self, attr.name, kwargs.get(attr.name, attr.default))
setattr(self, self._main_attr_name, kwargs.get(self._main_attr_name))
def __repr__(self):
return '%s(%s=%r)' % (self.__class__.__name__, self._main_attr_name, getattr(self, self._main_attr_name))
[docs]
def to_dict(self):
ret = {self._main_attr_name: getattr(self, self._main_attr_name)}
for key, value in self._attrs.items():
ret[key] = getattr(self, value.name)
return ret
[docs]
@classmethod
def from_parser(cls, parser, section, locale):
return cls.build(parser, section, locale)
[docs]
@classmethod
def build(cls, parser, section, locale):
kwargs = {cls._main_attr_name: section}
for attr in cls._attrs.values():
kwargs[attr.name] = attr.get(parser, section, locale)
return cls(**kwargs)
[docs]
@classmethod
def all_from_file(cls, fname, locale=None):
if locale is None:
locale = get_locale()
ret = []
parser = read_ini_file(fname)
for section in parser.sections():
try:
obj = cls.from_parser(parser, section, locale)
except (NoValueError, ParseError) as exc:
ini_logger.warning('%s: %s', fname, exc)
else:
ret.append(obj)
return ret
[docs]
class TypedIniSectionObject(IniSectionObject, metaclass=TypedIniSectionObjectMetaClass):
_type_attr = 'type'
[docs]
@classmethod
def get_class(cls, name):
return cls._class_types.get(name, cls)
[docs]
@classmethod
def from_parser(cls, parser, section, locale):
try:
value = parser.get(section, cls._type_attr)
except (NoSectionError, NoOptionError):
attr = cls._attrs.get(cls._type_attr)
value = attr.default if attr else None
klass = cls.get_class(value)
return klass.build(parser, section, locale)