diff --git a/src/devops/config/base.py b/src/devops/config/base.py index e3e3634..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,46 @@ 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( + 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) + + msg = f"Expected str or list of str for key '{key}', got {type(value).__name__}" + raise ConfigError(msg) def get_str_enum( @@ -165,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 @@ -205,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 0b80bd5..19c337f 100644 --- a/src/devops/config/config_file.py +++ b/src/devops/config/config_file.py @@ -1,9 +1,9 @@ """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 +from .base import ConfigError, get_str, get_str_or_str_list, get_table @dataclass(frozen=True) @@ -11,6 +11,22 @@ class FileConfig: """Dataclass to hold file configuration values.""" encoding: str = "utf-8" + 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. @@ -23,6 +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_paths) + lines.append(f'#changelog_paths = ["{paths_str}"]\n') + lines.append(f'#_default_changelog_path = "{self.default_changelog_path}"\n') return lines @@ -46,12 +65,86 @@ def parse_file_config(raw_config: dict) -> FileConfig: """ table = get_table(raw_config, "file") + encoding = parse_encoding(table) + changelog_paths = parse_changelog_path(table) + default_changelog_path = parse_default_changelog_path(table) + + return FileConfig( + encoding=encoding, + changelog_paths=changelog_paths, + _default_changelog_path=default_changelog_path, + ) + + +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 try: Path(__file__).open("r", encoding=encoding).close() except LookupError as e: 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] + + +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 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)