Source code for univention.config_registry_info

#
# Univention Configuration Registry
#  Config Registry information: read information about registered Config Registry
#  variables
#
# SPDX-FileCopyrightText: 2007-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

import os
import re
from collections.abc import Iterable

import univention.info_tools as uit


# default locale
_locale = 'de'


[docs] class Variable(uit.LocalizedDictionary): """UCR variable description.""" def __init__(self, registered: bool = True) -> None: uit.LocalizedDictionary.__init__(self) self.value: str | None = None self._registered = registered
[docs] def check(self) -> list[str]: """ Check description for completeness. :returns: List of missing settings. """ missing: list[str] = [] if not self._registered: return missing for key in ('description', 'type', 'categories'): if not self.get(key, None): missing.append(key) return missing
[docs] class Category(uit.LocalizedDictionary): """UCR category description.""" def __init__(self) -> None: uit.LocalizedDictionary.__init__(self)
[docs] def check(self) -> list[str]: """ Check description for completeness. :returns: List of missing settings. """ missing: list[str] = [] for key in ('name', 'icon'): if not self.get(key, None): missing.append(key) return missing
[docs] class ConfigRegistryInfo: """UCR variable and category descriptions.""" BASE_DIR = '/etc/univention/registry.info' CATEGORIES = 'categories' VARIABLES = 'variables' CUSTOMIZED = '_customized' FILE_SUFFIX = '.cfg' def __init__(self, install_mode: bool = False, registered_only: bool = True, load_customized: bool = True) -> None: """ Initialize variable and category descriptions. :param install_mode: `True` deactivates the use of an UCR instance. :param registered_only: `False` creates synthetic entries for all undescribed but set variables. :param load_customized: `False` deactivates loading customized descriptions. """ self.categories: dict[str, Category] = {} self.variables: dict[str, Variable] = {} self._patterns: dict[str, list[tuple[str, str]]] = {} if not install_mode: import univention.config_registry as ucr # circular import self._configRegistry: ucr.ConfigRegistry | None = ucr.ConfigRegistry() self._configRegistry.load() self.load_categories() self._load_variables(registered_only, load_customized) else: self._configRegistry = None
[docs] def check_categories(self) -> dict[str, list[str]]: """ Check all categories for completeness. :returns: dictionary of incomplete category descriptions. """ incomplete: dict[str, list[str]] = {} for name, cat in self.categories.items(): miss = cat.check() if miss: incomplete[name] = miss return incomplete
[docs] def check_variables(self) -> dict[str, list[str]]: """ Check variables. :returns: dictionary of incomplete variable descriptions. """ incomplete: dict[str, list[str]] = {} for name, var in self.variables.items(): miss = var.check() if miss: incomplete[name] = miss return incomplete
[docs] def read_categories(self, filename: str) -> None: """ Load a single category description file. :param filename: File to load. """ cfg = uit.UnicodeConfig() cfg.read(filename) for sec in cfg.sections(): # category already known? cat_name = sec.lower() if cat_name in self.categories: continue cat = Category() for name, value in cfg.items(sec): cat[name] = value self.categories[cat_name] = cat
[docs] def load_categories(self) -> None: """Load all category description files.""" path = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.CATEGORIES) if os.path.exists(path): for filename in os.listdir(path): self.read_categories(os.path.join(path, filename))
@staticmethod def _pattern_sorter(args: tuple) -> tuple[tuple[int, str], str]: """Sort more specific (longer) regular expressions first.""" pattern, data = args return ((len(pattern), pattern), data)
[docs] def check_patterns(self) -> None: """Match descriptions agains currently defined UCR variables.""" # in install mode if self._configRegistry is None: return # Try more specific (longer) regular expressions first for pattern, data in sorted(self._patterns.items(), key=ConfigRegistryInfo._pattern_sorter, reverse=True): regex = re.compile(pattern) # find config registry variables that match this pattern and are # not already listed in self.variables for key, value in self._configRegistry.items(): if key in self.variables: continue if not regex.match(key): continue # create variable object with values var = Variable() var.value = value # var.update() does not use __setitem__() for name, value in data: var[name] = value self.variables[key] = var
[docs] def describe_search_term(self, term: str) -> dict[str, Variable]: """ Try to apply a description to a search term. This is not complete, because it would require a complete "intersect two regular languages" algorithm. :param term: Search term. :returns: Dictionary mapping variable pattern to Variable info blocks. """ patterns: dict[str, Variable] = {} for pattern, data in sorted(self._patterns.items(), key=ConfigRegistryInfo._pattern_sorter, reverse=True): regex = re.compile(pattern) match = regex.search(term) if not match: regex = re.compile(term) match = regex.search(pattern) if match: var = Variable() # var.update() does not use __setitem__() for name, value in data: var[name] = value patterns[pattern] = var return patterns
[docs] def write_customized(self) -> None: """Persist the customized variable descriptions.""" filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, ConfigRegistryInfo.CUSTOMIZED) self._write_variables(filename)
def _write_variables(self, filename: str | None = None, package: str | None = None) -> bool: """ Persist the variable descriptions into a file. :param filename: Explicit filename for saving. :param package: Explicit package name. :raises AttributeError: if neither `filename` nor `package` are given. :returns: `True` on success, `False` otherwise. """ if filename: pass elif package: filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, package + ConfigRegistryInfo.FILE_SUFFIX) else: raise AttributeError("neither 'filename' nor 'package' is specified") try: with open(filename, 'w') as fd: cfg = uit.UnicodeConfig() for name, var in self.variables.items(): cfg.add_section(name) for key in var.keys(): items = var.normalize(key) for item, value in items.items(): cfg.set(name, item, value) cfg.write(fd) return True except OSError: return False
[docs] def read_customized(self) -> None: """Read customized variable descriptions.""" filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, ConfigRegistryInfo.CUSTOMIZED) self.read_variables(filename, override=True)
[docs] def read_variables(self, filename: str | None = None, package: str | None = None, override: bool = False) -> None: """ Read variable descriptions. :param filename: Explicit filename for loading. :param package: Explicit package name. :param override: `True` to overwrite already loaded descriptions. :raises AttributeError: if neither `filename` nor `package` are given. """ if filename: pass elif package: filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, package + ConfigRegistryInfo.FILE_SUFFIX) else: raise AttributeError("neither 'filename' nor 'package' is specified") cfg = uit.UnicodeConfig() cfg.read(filename) for sec in cfg.sections(): # is a pattern? if sec.find('.*') != -1: self._patterns[sec] = cfg.items(sec) continue # variable already known? if not override and sec in self.variables: continue var = Variable() for name, value in cfg.items(sec): var[name] = value # get current value if self._configRegistry is not None: var.value = self._configRegistry.get(sec, None) self.variables[sec] = var
def _load_variables(self, registered_only: bool = True, load_customized: bool = True) -> None: """ Read default and customized variable descriptions. :param registered_only: With default `True` only variables for which a description exists are loaded, otherwise all currently set variables are also included. :param load_customized: Load customized variable descriptions. """ path = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES) if os.path.exists(path): for entry in os.listdir(path): cfgfile = os.path.join(path, entry) if os.path.isfile(cfgfile) and cfgfile.endswith(ConfigRegistryInfo.FILE_SUFFIX) and entry != ConfigRegistryInfo.CUSTOMIZED: self.read_variables(cfgfile) self.check_patterns() if not registered_only and self._configRegistry is not None: for key, value in self._configRegistry.items(): if key in self.variables: continue var = Variable(registered=False) var.value = value self.variables[key] = var # read customized infos afterwards to override existing entries if load_customized: self.read_customized()
[docs] def get_categories(self) -> Iterable[str]: """ Return a list of category names. :returns: List if categories. """ return self.categories.keys()
[docs] def get_category(self, name: str) -> Category | None: """ Returns a category object associated with the given name or None. :param name: Name of the category. :returns: """ if name.lower() in self.categories: return self.categories[name.lower()] return None
[docs] def get_variables(self, category: str | None = None) -> dict[str, Variable]: """ Return dictionary of variable info blocks belonging to given category. :param category: Name of the category. `None` defaults to all variables. :returns: Dictionary mapping variable-name to :py:class:`Variable` instance. """ if not category: return self.variables temp: dict[str, Variable] = {} for name, var in self.variables.items(): categories = var.get('categories') if not categories: continue if category in [_.lower() for _ in categories.split(',')]: temp[name] = var return temp
[docs] def get_variable(self, key: str) -> Variable | None: """ Return the description of a variable. :param key: Variable name. :returns: description object or `None`. """ return self.variables.get(key, None)
[docs] def add_variable(self, key: str, variable: Variable) -> None: """ Add a new variable information item or overrides an old entry. :param key: Variable name. :param variable: :py:class:`Variable` instance. """ self.variables[key] = variable
[docs] def match_pattern(self, key: str) -> Variable | None: """ Searches the variable info, whichs regex pattern patches the given key. :param key: search key which should be matched by the regex pattern :returns: corresponding variable info block """ for pattern, data in sorted(self._patterns.items(), key=ConfigRegistryInfo._pattern_sorter, reverse=True): regex = re.compile(pattern) if not regex.match(key): continue var = Variable() for name, value in data: var[name] = value return var return None
[docs] def set_language(lang: str) -> None: """Set the default language.""" global _locale _locale = lang uit.set_language(lang)