Compare commits

...

3 Commits

Author SHA1 Message Date
dw-0
56ea43ccb6 refactor: improve typesafety KiauhSettings
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-04-14 21:19:38 +02:00
dw-0
25e22c993f chore(scp): update SimpleConfigParser
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-04-14 21:15:12 +02:00
dw-0
ead521b377 refactor: replace mypy with pyright
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-04-14 21:07:56 +02:00
6 changed files with 83 additions and 77 deletions

View File

@@ -10,7 +10,7 @@ from __future__ import annotations
import shutil
from dataclasses import dataclass, field
from typing import Any, List, Literal
from typing import Any, Callable, List, TypeVar
from components.klipper import KLIPPER_REPO_URL
from components.moonraker import MOONRAKER_REPO_URL
@@ -27,6 +27,8 @@ from kiauh import PROJECT_ROOT
DEFAULT_CFG = PROJECT_ROOT.joinpath("default.kiauh.cfg")
CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg")
T = TypeVar("T")
class InvalidValueError(Exception):
"""Raised when a value is invalid for an option"""
@@ -152,8 +154,7 @@ class KiauhSettings:
self.kiauh.backup_before_update = self.__read_from_cfg(
"kiauh",
"backup_before_update",
"bool",
False,
self.config.getboolean,
False,
)
@@ -161,16 +162,15 @@ class KiauhSettings:
self.klipper.use_python_binary = self.__read_from_cfg(
"klipper",
"use_python_binary",
"str",
self.config.getval,
None,
True,
)
kl_repos: List[str] = self.__read_from_cfg(
"klipper",
"repositories",
"list",
self.config.getvals,
[KLIPPER_REPO_URL],
False,
)
self.klipper.repositories = self.__set_repo_state("klipper", kl_repos)
@@ -178,23 +178,21 @@ class KiauhSettings:
self.moonraker.use_python_binary = self.__read_from_cfg(
"moonraker",
"use_python_binary",
"str",
self.config.getval,
None,
True,
)
self.moonraker.optional_speedups = self.__read_from_cfg(
"moonraker",
"optional_speedups",
"bool",
self.config.getboolean,
True,
False,
)
mr_repos: List[str] = self.__read_from_cfg(
"moonraker",
"repositories",
"list",
self.config.getvals,
[MOONRAKER_REPO_URL],
False,
)
self.moonraker.repositories = self.__set_repo_state("moonraker", mr_repos)
@@ -202,15 +200,13 @@ class KiauhSettings:
self.mainsail.port = self.__read_from_cfg(
"mainsail",
"port",
"int",
self.config.getint,
80,
False,
)
self.mainsail.unstable_releases = self.__read_from_cfg(
"mainsail",
"unstable_releases",
"bool",
False,
self.config.getboolean,
False,
)
@@ -218,49 +214,52 @@ class KiauhSettings:
self.fluidd.port = self.__read_from_cfg(
"fluidd",
"port",
"int",
self.config.getint,
80,
False,
)
self.fluidd.unstable_releases = self.__read_from_cfg(
"fluidd",
"unstable_releases",
"bool",
False,
self.config.getboolean,
False,
)
def __check_option_exists(
self, section: str, option: str, fallback: Any, silent: bool = False
) -> bool:
has_section = self.config.has_section(section)
has_option = self.config.has_option(section, option)
if not (has_section and has_option):
if not silent:
Logger.print_warn(
f"Option '{option}' in section '{section}' not defined. Falling back to '{fallback}'."
)
return False
return True
def __read_bool_from_cfg(
self,
section: str,
option: str,
fallback: bool | None = None,
silent: bool = False,
) -> bool | None:
if not self.__check_option_exists(section, option, fallback, silent):
return fallback
return self.config.getboolean(section, option, fallback)
def __read_from_cfg(
self,
section: str,
option: str,
value_type: Literal["str", "int", "bool", "list"] = "str",
fallback: str | int | bool | List[str] | None = None,
silent_fallback: bool = False,
) -> str | int | bool | List[str] | None:
has_section = self.config.has_section(section)
has_option = self.config.has_option(section, option)
if not (has_section and has_option):
if not silent_fallback:
Logger.print_warn(
f"Option '{option}' in section '{section}' not defined. Falling back to '{fallback}'."
)
return fallback
try:
return {
"int": self.config.getint,
"bool": self.config.getboolean,
"list": self.config.getval,
"str": self.config.getval,
}[value_type](section, option)
except (ValueError, TypeError) as e:
if not silent_fallback:
Logger.print_warn(
f"Failed to parse value of option '{option}' in section '{section}' as {value_type}. Falling back to '{fallback}'. Error: {e}"
)
getter: Callable[[str, str, T | None], T],
fallback: T = None,
silent: bool = False,
) -> T:
if not self.__check_option_exists(section, option, fallback, silent):
return fallback
return getter(section, option, fallback)
def __set_repo_state(self, section: str, repos: List[str]) -> List[Repository]:
_repos: List[Repository] = []

View File

@@ -314,9 +314,7 @@ class SimpleConfigParser:
elements.pop(i)
break
def getval(
self, section: str, option: str, fallback: str | _UNSET = _UNSET
) -> str | List[str]:
def getval(self, section: str, option: str, fallback: str | _UNSET = _UNSET) -> str:
"""
Return the value of the given option in the given section
@@ -329,22 +327,34 @@ class SimpleConfigParser:
if option not in self.get_options(section):
raise NoOptionError(option, section)
# Find the option in the elements list
for element in self.config[section]["elements"]:
if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value] and element["name"] == option:
raw_value = element["value"]
if isinstance(raw_value, str) and raw_value.endswith("\n"):
return raw_value[:-1].strip()
elif isinstance(raw_value, list):
values: List[str] = []
for i, val in enumerate(raw_value):
val = val.strip().strip("\n")
if len(val) < 1:
continue
values.append(val.strip())
return values
return str(raw_value)
raise NoOptionError(option, section)
if element["type"] is LineType.OPTION.value and element["name"] == option:
return str(element["value"].strip().replace("\n", ""))
return ""
except (NoSectionError, NoOptionError):
if fallback is _UNSET:
raise
return fallback
def getvals(self, section: str, option: str, fallback: List[str] | _UNSET = _UNSET) -> List[str]:
"""
Return the values of the given multi-line option in the given section
If the key is not found and 'fallback' is provided, it is used as
a fallback value.
"""
try:
if section not in self.get_sections():
raise NoSectionError(section)
if option not in self.get_options(section):
raise NoOptionError(option, section)
for element in self.config[section]["elements"]:
if element["type"] is LineType.OPTION_BLOCK.value and element["name"] == option:
return [val.strip() for val in element["value"] if val.strip()]
return []
except (NoSectionError, NoOptionError):
if fallback is _UNSET:
raise

View File

@@ -51,7 +51,7 @@ def test_getval(parser):
assert parser.getval("section_2", "option_2") == "value_2"
# test multiline option values
ml_val = parser.getval("section number 5", "multi_option")
ml_val = parser.getvals("section number 5", "multi_option")
assert isinstance(ml_val, list)
assert len(ml_val) > 0
@@ -164,7 +164,7 @@ def test_set_new_option(parser):
assert parser.getval("new_section", "very_new_option") == "very_new_value"
parser.set_option("section_2", "array_option", ["value_1", "value_2", "value_3"])
assert parser.getval("section_2", "array_option") == [
assert parser.getvals("section_2", "array_option") == [
"value_1",
"value_2",
"value_3",

View File

@@ -2,7 +2,7 @@
requires-python = ">=3.8"
[project.optional-dependencies]
dev=["ruff", "mypy"]
dev=["ruff", "pyright"]
[tool.ruff]
required-version = ">=0.9.10"
@@ -20,14 +20,3 @@ quote-style = "double"
[tool.ruff.lint]
extend-select = ["I"]
[tool.mypy]
python_version = "3.8"
platform = "linux"
# strict = true # TODO: enable this once everything is else is handled
check_untyped_defs = true
ignore_missing_imports = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
warn_unreachable = true

6
pyrightconfig.json Normal file
View File

@@ -0,0 +1,6 @@
{
"pythonVersion": "3.8",
"pythonPlatform": "Linux",
"typeCheckingMode": "standard",
"venvPath": "./.kiauh-env"
}

2
requirements-dev.txt Normal file
View File

@@ -0,0 +1,2 @@
ruff (>=0.9.10)
pyright