From 46b0bb4a641b5f401787398c98316eb47d2eccfc Mon Sep 17 00:00:00 2001 From: Jakob Gamper <97gamjak@gmail.com> Date: Thu, 15 Jan 2026 22:55:11 +0100 Subject: [PATCH 1/3] feat: add get_str_or_str_list function and update FileConfig with changelog_path - to make this work we will need a rework of the default handling in config base --- src/devops/config/base.py | 36 ++++++++++++++++++++++++++++++++ src/devops/config/config_file.py | 4 +++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/devops/config/base.py b/src/devops/config/base.py index e3e3634..6f1394a 100644 --- a/src/devops/config/base.py +++ b/src/devops/config/base.py @@ -139,6 +139,42 @@ def get_str( return _get_type(mapping, key, default, str) +def get_str_or_str_list( + mapping: dict[str, Any], key: str, default: str | list[str] | None = None +) -> str | list[str] | None: + """Get a string or list of strings from a mapping. + + Parameters + ---------- + mapping: dict[str, Any] + The mapping to extract the value from. + key: str + The key of the value. + default: str | list[str] | None + The default value to return if the key is not found. + + Returns + ------- + str | list[str] | None + The extracted string or list of strings value or None if the key is not found. + """ + value = mapping.get(key, default) + + if value is None: + return None + + if isinstance(value, str): + return get_str(mapping, key, default) + + if isinstance(value, list) and all(isinstance(item, str) for item in value): + return get_str_list( + mapping, key, default if isinstance(default, list) else None + ) + + msg = f"Expected str or list of str for key '{key}', got {type(value).__name__}" + raise ConfigError(msg) + + def get_str_enum( mapping: dict[str, Any], key: str, enum_type: type, default: str | None = None ) -> StrEnum | None: diff --git a/src/devops/config/config_file.py b/src/devops/config/config_file.py index 0b80bd5..ab7ae3c 100644 --- a/src/devops/config/config_file.py +++ b/src/devops/config/config_file.py @@ -1,6 +1,6 @@ """Module to parse file configuration values.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from .base import ConfigError, get_str, get_table @@ -11,6 +11,7 @@ class FileConfig: """Dataclass to hold file configuration values.""" encoding: str = "utf-8" + changelog_path: list[Path] = field(default_factory=lambda: [Path("CHANGELOG.md")]) def to_toml_lines(self) -> list[str]: """Convert the FileConfig to TOML lines. @@ -48,6 +49,7 @@ def parse_file_config(raw_config: dict) -> FileConfig: encoding = get_str(table, "encoding", default=FileConfig.encoding) + ### Validate encoding try: Path(__file__).open("r", encoding=encoding).close() except LookupError as e: From 6a5e8c386f72976835348dafd1d30e2e5f64f876 Mon Sep 17 00:00:00 2001 From: Jakob Gamper <97gamjak@gmail.com> Date: Fri, 16 Jan 2026 20:31:53 +0100 Subject: [PATCH 2/3] feat: refactor _get_type function and enhance parsing for changelog paths in FileConfig --- src/devops/config/base.py | 36 ++++++++++++++-------- src/devops/config/config_file.py | 53 ++++++++++++++++++++++++++++++-- tests/config/test_config.py | 4 +-- 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/devops/config/base.py b/src/devops/config/base.py index 6f1394a..5400985 100644 --- a/src/devops/config/base.py +++ b/src/devops/config/base.py @@ -54,9 +54,7 @@ def get_table(mapping: dict[str, Any], key: str) -> dict[str, Any]: return value -def _get_type( - mapping: dict[str, Any], key: str, default: Any, expected_type: type -) -> Any: +def _get_type(mapping: dict[str, Any], key: str, expected_type: type) -> Any: """Get a value of expected type from a mapping. Parameters @@ -65,8 +63,6 @@ def _get_type( The mapping to extract the value from. key: str The key of the value. - default: Any - The default value to return if the key is not found. expected_type: type The expected type of the value. @@ -80,7 +76,7 @@ def _get_type( ConfigError If the value associated with the key is not of the expected type. """ - value = mapping.get(key, default) + value = mapping.get(key) if value is None: return None @@ -114,7 +110,12 @@ def get_bool( bool | None The extracted boolean value or None if the key is not found. """ - return _get_type(mapping, key, default, bool) + value = _get_type(mapping, key, bool) + + if value is None: + return default + + return value def get_str( @@ -136,7 +137,12 @@ def get_str( str | None The extracted string value or None if the key is not found. """ - return _get_type(mapping, key, default, str) + value = _get_type(mapping, key, str) + + if value is None: + return default + + return value def get_str_or_str_list( @@ -167,9 +173,7 @@ def get_str_or_str_list( return get_str(mapping, key, default) if isinstance(value, list) and all(isinstance(item, str) for item in value): - return get_str_list( - mapping, key, default if isinstance(default, list) else None - ) + return get_str_list(mapping, key, default) msg = f"Expected str or list of str for key '{key}', got {type(value).__name__}" raise ConfigError(msg) @@ -201,7 +205,10 @@ def get_str_enum( ConfigError If the value associated with the key is not a valid enum value. """ - value = _get_type(mapping, key, default, str) + value = _get_type(mapping, key, str) + + if value is None: + value = default if value is None: return None @@ -241,7 +248,10 @@ def get_str_list( If the value associated with the key is not a list of strings. """ - value = mapping.get(key, default) + value = _get_type(mapping, key, list) + + if value is None: + value = default if value is None: return [] diff --git a/src/devops/config/config_file.py b/src/devops/config/config_file.py index ab7ae3c..5e36059 100644 --- a/src/devops/config/config_file.py +++ b/src/devops/config/config_file.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from pathlib import Path -from .base import ConfigError, get_str, get_table +from .base import ConfigError, get_str, get_str_or_str_list, get_table @dataclass(frozen=True) @@ -24,6 +24,8 @@ def to_toml_lines(self) -> list[str]: """ lines = ["[file]\n"] lines.append(f'#encoding = "{self.encoding}"\n') + paths_str = '", "'.join(str(p) for p in self.changelog_path) + lines.append(f'#changelog_path = ["{paths_str}"]\n') return lines @@ -47,6 +49,30 @@ def parse_file_config(raw_config: dict) -> FileConfig: """ table = get_table(raw_config, "file") + encoding = parse_encoding(table) + changelog_paths = parse_changelog_path(table) + + return FileConfig(encoding=encoding, changelog_path=changelog_paths) + + +def parse_encoding(table: dict) -> str: + """Parse the encoding from a configuration table. + + Parameters + ---------- + table: dict + The configuration table. + + Returns + ------- + str + The parsed encoding. + + Raises + ------ + ConfigError + If the specified encoding is invalid. + """ encoding = get_str(table, "encoding", default=FileConfig.encoding) ### Validate encoding @@ -56,4 +82,27 @@ def parse_file_config(raw_config: dict) -> FileConfig: msg = f"Invalid file encoding specified in configuration: {encoding}" raise ConfigError(msg) from e - return FileConfig(encoding=encoding) + return encoding + + +def parse_changelog_path(table: dict) -> list[Path]: + """Parse the changelog paths from a configuration table. + + Parameters + ---------- + table: dict + The configuration table. + + Returns + ------- + list[Path] + The parsed list of changelog paths. + """ + changelog_paths = get_str_or_str_list( + table, "changelog_path", FileConfig.changelog_path + ) + + if isinstance(changelog_paths, str): + changelog_paths = [changelog_paths] + + return [Path(p) for p in changelog_paths] diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 5e41bac..a915789 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -92,7 +92,7 @@ def test_parse_config_buggy_cpp_macros_not_list() -> None: with pytest.raises(ConfigError) as exc_info: parse_config(raw_config) - assert "Expected list of strings for key" in str(exc_info.value) + assert "Expected list for key" in str(exc_info.value) assert "buggy_cpp_macros" in str(exc_info.value) @@ -122,7 +122,7 @@ def test_parse_config_buggy_cpp_macros_is_dict() -> None: with pytest.raises(ConfigError) as exc_info: parse_config(raw_config) - assert "Expected list of strings for key" in str(exc_info.value) + assert "Expected list for key" in str(exc_info.value) assert "buggy_cpp_macros" in str(exc_info.value) From a32d1a6896a90cdcc2efed1166c9cfb258b2704c Mon Sep 17 00:00:00 2001 From: Jakob Gamper <97gamjak@gmail.com> Date: Fri, 16 Jan 2026 23:22:14 +0100 Subject: [PATCH 3/3] feat: update FileConfig to support multiple changelog paths and default path handling --- src/devops/config/config_file.py | 50 +++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/devops/config/config_file.py b/src/devops/config/config_file.py index 5e36059..19c337f 100644 --- a/src/devops/config/config_file.py +++ b/src/devops/config/config_file.py @@ -11,7 +11,22 @@ class FileConfig: """Dataclass to hold file configuration values.""" encoding: str = "utf-8" - changelog_path: list[Path] = field(default_factory=lambda: [Path("CHANGELOG.md")]) + changelog_paths: list[Path] = field(default_factory=lambda: [Path("CHANGELOG.md")]) + _default_changelog_path: Path | None = None + + @property + def default_changelog_path(self) -> Path: + """Get the default changelog path. + + Returns + ------- + Path + The default changelog path. + """ + if self._default_changelog_path is not None: + return self._default_changelog_path + + return self.changelog_paths[0] def to_toml_lines(self) -> list[str]: """Convert the FileConfig to TOML lines. @@ -24,8 +39,9 @@ def to_toml_lines(self) -> list[str]: """ lines = ["[file]\n"] lines.append(f'#encoding = "{self.encoding}"\n') - paths_str = '", "'.join(str(p) for p in self.changelog_path) - lines.append(f'#changelog_path = ["{paths_str}"]\n') + paths_str = '", "'.join(str(p) for p in self.changelog_paths) + lines.append(f'#changelog_paths = ["{paths_str}"]\n') + lines.append(f'#_default_changelog_path = "{self.default_changelog_path}"\n') return lines @@ -51,8 +67,13 @@ def parse_file_config(raw_config: dict) -> FileConfig: encoding = parse_encoding(table) changelog_paths = parse_changelog_path(table) + default_changelog_path = parse_default_changelog_path(table) - return FileConfig(encoding=encoding, changelog_path=changelog_paths) + return FileConfig( + encoding=encoding, + changelog_paths=changelog_paths, + _default_changelog_path=default_changelog_path, + ) def parse_encoding(table: dict) -> str: @@ -106,3 +127,24 @@ def parse_changelog_path(table: dict) -> list[Path]: changelog_paths = [changelog_paths] return [Path(p) for p in changelog_paths] + + +def parse_default_changelog_path(table: dict) -> Path | None: + """Parse the default changelog path from a configuration table. + + Parameters + ---------- + table: dict + The configuration table. + + Returns + ------- + Path | None + The parsed default changelog path, or None if not specified. + """ + default_path_str = get_str(table, "default_changelog_path", default=None) + + if default_path_str is not None: + return Path(default_path_str) + + return None