# SPDX-License-Identifier: BSD-2-Clause # SPDX-FileCopyrightText: 2023 KDE e.V. # SPDX-FileContributor: Ingo Klöcker import logging from pathlib import Path from typing import Any, Dict, IO, List, Optional, Union import yaml log = logging.getLogger(__name__) class Error(Exception): pass def readProjectSettings(yamlStream: Union[str, IO]) -> None: global settings settings = ProjectSettings(yamlStream) class ProjectSettings: # Possible boolean values (same as configparser) BOOLEAN_STATES = { "1": True, "yes": True, "true": True, "on": True, "0": False, "no": False, "false": False, "off": False, } def __init__(self, yamlStream: Union[str, IO]): self._projects: Dict[str, Dict[str, Dict[str, str]]] = {} self._readSettings(yamlStream) def _readSettings(self, yamlStream): try: # Set "nosec B506" to silence false positive warning "Use of unsafe yaml load. # Allows instantiation of arbitrary objects. Consider yaml.safe_load()." # We use yaml.load with BaseLoader which is even safer than SafeLoader. d = yaml.load(yamlStream, yaml.BaseLoader) # nosec B506 except Exception as e: log.error("Error: Failed to read project settings.") log.debug("Error details:", exc_info=e) return if d is None: # PyYAML didn't find any documents in the stream return defaultSettings = d.get("defaults", {}) defaultBranches = [] if "branches" in defaultSettings: defaultBranches = defaultSettings["branches"] del defaultSettings["branches"] projects = {} for p in d: if p == "defaults": continue projects[p] = _readProject(d[p] or {}, defaultBranches, defaultSettings) self._projects = projects def exists(self, projectPath: str, branch: Optional[str] = None) -> bool: if branch is None: return projectPath in self._projects return branch in self._projects.get(projectPath, {}) def get(self, projectPath: str, branch: str, key: str, default: Any = None) -> Any: p = self._projects.get(projectPath) if p is None: raise Error(f"Unknown project '{projectPath}'") b = p.get(branch) if b is None: raise Error(f"Unknown branch '{branch}' of project '{projectPath}'") return b.get(key, default) def getBool(self, projectPath: str, branch: str, key: str, default: bool) -> bool: value = self.get(projectPath, branch, key, default) if isinstance(value, str): if value.lower() not in self.BOOLEAN_STATES: raise ValueError("Not a boolean: %s" % value) value = self.BOOLEAN_STATES[value.lower()] return value def getPath( self, projectPath: str, branch: str, key: str, default: Optional[Union[Path, str]] = None ) -> Optional[Path]: v = self.get(projectPath, branch, key, default) if isinstance(v, str): v = Path(v).expanduser() return v def _readProject(projectSettings: dict, defaultBranches: List[str], defaultSettings: dict): project = {} branches = projectSettings.get("branches") if branches is not None: del projectSettings["branches"] for b in branches: # initialize with the default settings branchSettings = defaultSettings.copy() # update with the project settings branchSettings.update(projectSettings.copy()) # update with the branch-specific settings branchSettings.update(branches[b] or {}) project[b] = branchSettings # add the default branches for b in defaultBranches: if b not in project: branchSettings = defaultSettings.copy() branchSettings.update(projectSettings.copy()) project[b] = branchSettings return project settings = ProjectSettings("")