Coverage for python/univention/config_registry_info.py: 100%
203 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-09-11 14:11 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-09-11 14:11 +0000
1#
2# Univention Configuration Registry
3# Config Registry information: read information about registered Config Registry
4# variables
5#
6# SPDX-FileCopyrightText: 2007-2026 Univention GmbH
7# SPDX-License-Identifier: AGPL-3.0-only
9import os
10import re
11from collections.abc import Iterable
13import univention.info_tools as uit
16# default locale
17_locale = 'de'
20class Variable(uit.LocalizedDictionary):
21 """UCR variable description."""
23 def __init__(self, registered: bool = True) -> None:
24 uit.LocalizedDictionary.__init__(self)
25 self.value: str | None = None
26 self._registered = registered
28 def check(self) -> list[str]:
29 """
30 Check description for completeness.
32 :returns: List of missing settings.
33 """
34 missing: list[str] = []
35 if not self._registered:
36 return missing
38 for key in ('description', 'type', 'categories'):
39 if not self.get(key, None):
40 missing.append(key)
41 return missing
44class Category(uit.LocalizedDictionary):
45 """UCR category description."""
47 def __init__(self) -> None:
48 uit.LocalizedDictionary.__init__(self)
50 def check(self) -> list[str]:
51 """
52 Check description for completeness.
54 :returns: List of missing settings.
55 """
56 missing: list[str] = []
57 for key in ('name', 'icon'):
58 if not self.get(key, None):
59 missing.append(key)
60 return missing
63class ConfigRegistryInfo:
64 """UCR variable and category descriptions."""
66 BASE_DIR = '/etc/univention/registry.info'
67 CATEGORIES = 'categories'
68 VARIABLES = 'variables'
69 CUSTOMIZED = '_customized'
70 FILE_SUFFIX = '.cfg'
72 def __init__(self, install_mode: bool = False, registered_only: bool = True, load_customized: bool = True) -> None:
73 """
74 Initialize variable and category descriptions.
76 :param install_mode: `True` deactivates the use of an UCR instance.
77 :param registered_only: `False` creates synthetic entries for all undescribed but set variables.
78 :param load_customized: `False` deactivates loading customized descriptions.
79 """
80 self.categories: dict[str, Category] = {}
81 self.variables: dict[str, Variable] = {}
82 self._patterns: dict[str, list[tuple[str, str]]] = {}
83 if not install_mode:
84 import univention.config_registry as ucr # circular import
85 self._configRegistry: ucr.ConfigRegistry | None = ucr.ConfigRegistry()
86 self._configRegistry.load()
87 self.load_categories()
88 self._load_variables(registered_only, load_customized)
89 else:
90 self._configRegistry = None
92 def check_categories(self) -> dict[str, list[str]]:
93 """
94 Check all categories for completeness.
96 :returns: dictionary of incomplete category descriptions.
97 """
98 incomplete: dict[str, list[str]] = {}
99 for name, cat in self.categories.items():
100 miss = cat.check()
101 if miss:
102 incomplete[name] = miss
103 return incomplete
105 def check_variables(self) -> dict[str, list[str]]:
106 """
107 Check variables.
109 :returns: dictionary of incomplete variable descriptions.
110 """
111 incomplete: dict[str, list[str]] = {}
112 for name, var in self.variables.items():
113 miss = var.check()
114 if miss:
115 incomplete[name] = miss
116 return incomplete
118 def read_categories(self, filename: str) -> None:
119 """
120 Load a single category description file.
122 :param filename: File to load.
123 """
124 cfg = uit.UnicodeConfig()
125 cfg.read(filename)
126 for sec in cfg.sections():
127 # category already known?
128 cat_name = sec.lower()
129 if cat_name in self.categories:
130 continue
131 cat = Category()
132 for name, value in cfg.items(sec):
133 cat[name] = value
134 self.categories[cat_name] = cat
136 def load_categories(self) -> None:
137 """Load all category description files."""
138 path = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.CATEGORIES)
139 if os.path.exists(path):
140 for filename in os.listdir(path):
141 self.read_categories(os.path.join(path, filename))
143 @staticmethod
144 def _pattern_sorter(args: tuple) -> tuple[tuple[int, str], str]:
145 """Sort more specific (longer) regular expressions first."""
146 pattern, data = args
147 return ((len(pattern), pattern), data)
149 def check_patterns(self) -> None:
150 """Match descriptions agains currently defined UCR variables."""
151 # in install mode
152 if self._configRegistry is None:
153 return
154 # Try more specific (longer) regular expressions first
155 for pattern, data in sorted(self._patterns.items(), key=ConfigRegistryInfo._pattern_sorter, reverse=True):
156 regex = re.compile(pattern)
157 # find config registry variables that match this pattern and are
158 # not already listed in self.variables
159 for key, value in self._configRegistry.items():
160 if key in self.variables:
161 continue
162 if not regex.match(key):
163 continue
164 # create variable object with values
165 var = Variable()
166 var.value = value
167 # var.update() does not use __setitem__()
168 for name, value in data:
169 var[name] = value
170 self.variables[key] = var
172 def describe_search_term(self, term: str) -> dict[str, Variable]:
173 """
174 Try to apply a description to a search term.
176 This is not complete, because it would require a complete "intersect
177 two regular languages" algorithm.
179 :param term: Search term.
180 :returns: Dictionary mapping variable pattern to Variable info blocks.
181 """
182 patterns: dict[str, Variable] = {}
183 for pattern, data in sorted(self._patterns.items(), key=ConfigRegistryInfo._pattern_sorter, reverse=True):
184 regex = re.compile(pattern)
185 match = regex.search(term)
186 if not match:
187 regex = re.compile(term)
188 match = regex.search(pattern)
189 if match:
190 var = Variable()
191 # var.update() does not use __setitem__()
192 for name, value in data:
193 var[name] = value
194 patterns[pattern] = var
195 return patterns
197 def write_customized(self) -> None:
198 """Persist the customized variable descriptions."""
199 filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, ConfigRegistryInfo.CUSTOMIZED)
200 self._write_variables(filename)
202 def _write_variables(self, filename: str | None = None, package: str | None = None) -> bool:
203 """
204 Persist the variable descriptions into a file.
206 :param filename: Explicit filename for saving.
207 :param package: Explicit package name.
208 :raises AttributeError: if neither `filename` nor `package` are given.
209 :returns: `True` on success, `False` otherwise.
210 """
211 if filename:
212 pass
213 elif package:
214 filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, package + ConfigRegistryInfo.FILE_SUFFIX)
215 else:
216 raise AttributeError("neither 'filename' nor 'package' is specified")
218 try:
219 with open(filename, 'w') as fd:
220 cfg = uit.UnicodeConfig()
221 for name, var in self.variables.items():
222 cfg.add_section(name)
223 for key in var.keys():
224 items = var.normalize(key)
225 for item, value in items.items():
226 cfg.set(name, item, value)
228 cfg.write(fd)
230 return True
231 except OSError:
232 return False
234 def read_customized(self) -> None:
235 """Read customized variable descriptions."""
236 filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, ConfigRegistryInfo.CUSTOMIZED)
237 self.read_variables(filename, override=True)
239 def read_variables(self, filename: str | None = None, package: str | None = None, override: bool = False) -> None:
240 """
241 Read variable descriptions.
243 :param filename: Explicit filename for loading.
244 :param package: Explicit package name.
245 :param override: `True` to overwrite already loaded descriptions.
246 :raises AttributeError: if neither `filename` nor `package` are given.
247 """
248 if filename:
249 pass
250 elif package:
251 filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, package + ConfigRegistryInfo.FILE_SUFFIX)
252 else:
253 raise AttributeError("neither 'filename' nor 'package' is specified")
254 cfg = uit.UnicodeConfig()
255 cfg.read(filename)
256 for sec in cfg.sections():
257 # is a pattern?
258 if sec.find('.*') != -1:
259 self._patterns[sec] = cfg.items(sec)
260 continue
261 # variable already known?
262 if not override and sec in self.variables:
263 continue
264 var = Variable()
265 for name, value in cfg.items(sec):
266 var[name] = value
267 # get current value
268 if self._configRegistry is not None:
269 var.value = self._configRegistry.get(sec, None)
270 self.variables[sec] = var
272 def _load_variables(self, registered_only: bool = True, load_customized: bool = True) -> None:
273 """
274 Read default and customized variable descriptions.
276 :param registered_only: With default `True` only variables for which a description exists are loaded, otherwise all currently set variables are also included.
277 :param load_customized: Load customized variable descriptions.
278 """
279 path = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES)
280 if os.path.exists(path):
281 for entry in os.listdir(path):
282 cfgfile = os.path.join(path, entry)
283 if os.path.isfile(cfgfile) and cfgfile.endswith(ConfigRegistryInfo.FILE_SUFFIX) and entry != ConfigRegistryInfo.CUSTOMIZED:
284 self.read_variables(cfgfile)
285 self.check_patterns()
286 if not registered_only and self._configRegistry is not None:
287 for key, value in self._configRegistry.items():
288 if key in self.variables:
289 continue
290 var = Variable(registered=False)
291 var.value = value
292 self.variables[key] = var
293 # read customized infos afterwards to override existing entries
294 if load_customized:
295 self.read_customized()
297 def get_categories(self) -> Iterable[str]:
298 """
299 Return a list of category names.
301 :returns: List if categories.
302 """
303 return self.categories.keys()
305 def get_category(self, name: str) -> Category | None:
306 """
307 Returns a category object associated with the given name or None.
309 :param name: Name of the category.
310 :returns:
311 """
312 if name.lower() in self.categories:
313 return self.categories[name.lower()]
314 return None
316 def get_variables(self, category: str | None = None) -> dict[str, Variable]:
317 """
318 Return dictionary of variable info blocks belonging to given category.
320 :param category: Name of the category. `None` defaults to all variables.
321 :returns: Dictionary mapping variable-name to :py:class:`Variable` instance.
322 """
323 if not category:
324 return self.variables
325 temp: dict[str, Variable] = {}
326 for name, var in self.variables.items():
327 categories = var.get('categories')
328 if not categories:
329 continue
330 if category in [_.lower() for _ in categories.split(',')]:
331 temp[name] = var
332 return temp
334 def get_variable(self, key: str) -> Variable | None:
335 """
336 Return the description of a variable.
338 :param key: Variable name.
339 :returns: description object or `None`.
340 """
341 return self.variables.get(key, None)
343 def add_variable(self, key: str, variable: Variable) -> None:
344 """
345 Add a new variable information item or overrides an old entry.
347 :param key: Variable name.
348 :param variable: :py:class:`Variable` instance.
349 """
350 self.variables[key] = variable
352 def match_pattern(self, key: str) -> Variable | None:
353 """
354 Searches the variable info, whichs regex pattern patches the given key.
356 :param key: search key which should be matched by the regex pattern
357 :returns: corresponding variable info block
358 """
359 for pattern, data in sorted(self._patterns.items(), key=ConfigRegistryInfo._pattern_sorter, reverse=True):
360 regex = re.compile(pattern)
361 if not regex.match(key):
362 continue
363 var = Variable()
364 for name, value in data:
365 var[name] = value
366 return var
368 return None
371def set_language(lang: str) -> None:
372 """Set the default language."""
373 global _locale
374 _locale = lang
375 uit.set_language(lang)