124 lines
4 KiB
Python
124 lines
4 KiB
Python
# SPDX-License-Identifier: BSD-2-Clause
|
|
# SPDX-FileCopyrightText: 2023 KDE e.V.
|
|
# SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
|
|
|
|
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("")
|