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

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 

8 

9import os 

10import re 

11from collections.abc import Iterable 

12 

13import univention.info_tools as uit 

14 

15 

16# default locale 

17_locale = 'de' 

18 

19 

20class Variable(uit.LocalizedDictionary): 

21 """UCR variable description.""" 

22 

23 def __init__(self, registered: bool = True) -> None: 

24 uit.LocalizedDictionary.__init__(self) 

25 self.value: str | None = None 

26 self._registered = registered 

27 

28 def check(self) -> list[str]: 

29 """ 

30 Check description for completeness. 

31 

32 :returns: List of missing settings. 

33 """ 

34 missing: list[str] = [] 

35 if not self._registered: 

36 return missing 

37 

38 for key in ('description', 'type', 'categories'): 

39 if not self.get(key, None): 

40 missing.append(key) 

41 return missing 

42 

43 

44class Category(uit.LocalizedDictionary): 

45 """UCR category description.""" 

46 

47 def __init__(self) -> None: 

48 uit.LocalizedDictionary.__init__(self) 

49 

50 def check(self) -> list[str]: 

51 """ 

52 Check description for completeness. 

53 

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 

61 

62 

63class ConfigRegistryInfo: 

64 """UCR variable and category descriptions.""" 

65 

66 BASE_DIR = '/etc/univention/registry.info' 

67 CATEGORIES = 'categories' 

68 VARIABLES = 'variables' 

69 CUSTOMIZED = '_customized' 

70 FILE_SUFFIX = '.cfg' 

71 

72 def __init__(self, install_mode: bool = False, registered_only: bool = True, load_customized: bool = True) -> None: 

73 """ 

74 Initialize variable and category descriptions. 

75 

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 

91 

92 def check_categories(self) -> dict[str, list[str]]: 

93 """ 

94 Check all categories for completeness. 

95 

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 

104 

105 def check_variables(self) -> dict[str, list[str]]: 

106 """ 

107 Check variables. 

108 

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 

117 

118 def read_categories(self, filename: str) -> None: 

119 """ 

120 Load a single category description file. 

121 

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 

135 

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)) 

142 

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) 

148 

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 

171 

172 def describe_search_term(self, term: str) -> dict[str, Variable]: 

173 """ 

174 Try to apply a description to a search term. 

175 

176 This is not complete, because it would require a complete "intersect 

177 two regular languages" algorithm. 

178 

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 

196 

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) 

201 

202 def _write_variables(self, filename: str | None = None, package: str | None = None) -> bool: 

203 """ 

204 Persist the variable descriptions into a file. 

205 

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") 

217 

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) 

227 

228 cfg.write(fd) 

229 

230 return True 

231 except OSError: 

232 return False 

233 

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) 

238 

239 def read_variables(self, filename: str | None = None, package: str | None = None, override: bool = False) -> None: 

240 """ 

241 Read variable descriptions. 

242 

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 

271 

272 def _load_variables(self, registered_only: bool = True, load_customized: bool = True) -> None: 

273 """ 

274 Read default and customized variable descriptions. 

275 

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() 

296 

297 def get_categories(self) -> Iterable[str]: 

298 """ 

299 Return a list of category names. 

300 

301 :returns: List if categories. 

302 """ 

303 return self.categories.keys() 

304 

305 def get_category(self, name: str) -> Category | None: 

306 """ 

307 Returns a category object associated with the given name or None. 

308 

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 

315 

316 def get_variables(self, category: str | None = None) -> dict[str, Variable]: 

317 """ 

318 Return dictionary of variable info blocks belonging to given category. 

319 

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 

333 

334 def get_variable(self, key: str) -> Variable | None: 

335 """ 

336 Return the description of a variable. 

337 

338 :param key: Variable name. 

339 :returns: description object or `None`. 

340 """ 

341 return self.variables.get(key, None) 

342 

343 def add_variable(self, key: str, variable: Variable) -> None: 

344 """ 

345 Add a new variable information item or overrides an old entry. 

346 

347 :param key: Variable name. 

348 :param variable: :py:class:`Variable` instance. 

349 """ 

350 self.variables[key] = variable 

351 

352 def match_pattern(self, key: str) -> Variable | None: 

353 """ 

354 Searches the variable info, whichs regex pattern patches the given key. 

355 

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 

367 

368 return None 

369 

370 

371def set_language(lang: str) -> None: 

372 """Set the default language.""" 

373 global _locale 

374 _locale = lang 

375 uit.set_language(lang)