Source code for univention.appcenter.ini_parser

#!/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 TypedIniSectionObjectMetaClass(UniventionMetaClass): @classmethod def _add_class_type(mcs, name, base, klass): if hasattr(base, '_class_types'): base._class_types[name] = klass for _base in base.__bases__: mcs._add_class_type(name, _base, klass) def __new__(mcs, name, bases, attrs): new_cls = super().__new__(mcs, name, bases, attrs) new_cls._class_types = {} for base in bases: mcs._add_class_type(name, base, new_cls) return new_cls
[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)