Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@ All notable changes to this project will be documented in this file.

## Next Release

#### Config

- Add `changelog_paths` to config toml approach
- Add `default_changelog_path` to config toml approach

#### API

- Add `changelog_path` input to `update_changelog`
- Add `update_changelogs` to update multiple changelogs at once

<!-- insertion marker -->
## [0.0.4](https://github.com/repo/owner/releases/tag/0.0.4) - 2025-12-20

Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ test = ["pytest>=9.0.1", "pytest-cov", "coverage", "docstr-coverage"]

[project.scripts]
cpp_checks = "devops.scripts.cpp_checks:app"
update_changelog = "devops.scripts.update_changelog:update_changelog"
update_changelogs = "devops.scripts.update_changelog:update_changelogs"
update_changelog = "devops.scripts.update_changelog:app"
get_latest_tag = "devops.scripts.get_latest_git_tag:latest_tag"
increase_latest_tag = "devops.scripts.get_latest_git_tag:increase_tag"
generate_toml_template = "devops.scripts.generate_toml_template:app"
Expand All @@ -25,4 +24,4 @@ add_license_headers = "devops.scripts.add_license_header:add_multiple_headers"
filter_buggy_cpp_files = "devops.scripts.cpp_files:app"

[tool.setuptools_scm]
version_file = "src/devops/__version__.py"
version_file = "src/devops/__version__.py"
72 changes: 10 additions & 62 deletions src/devops/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def get_table(mapping: dict[str, Any], key: str) -> dict[str, Any]:
return value


def _get_type(mapping: dict[str, Any], key: str, expected_type: type) -> Any:
def _get_type(
mapping: dict[str, Any], key: str, default: Any, expected_type: type
) -> Any:
"""Get a value of expected type from a mapping.

Parameters
Expand All @@ -63,6 +65,8 @@ def _get_type(mapping: dict[str, Any], key: str, expected_type: type) -> Any:
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.

Expand All @@ -76,7 +80,7 @@ def _get_type(mapping: dict[str, Any], key: str, expected_type: type) -> Any:
ConfigError
If the value associated with the key is not of the expected type.
"""
value = mapping.get(key)
value = mapping.get(key, default)

if value is None:
return None
Expand Down Expand Up @@ -110,12 +114,7 @@ def get_bool(
bool | None
The extracted boolean value or None if the key is not found.
"""
value = _get_type(mapping, key, bool)

if value is None:
return default

return value
return _get_type(mapping, key, default, bool)


def get_str(
Expand All @@ -137,52 +136,7 @@ def get_str(
str | None
The extracted string value or None if the key is not found.
"""
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 value

if isinstance(value, list):
if all(isinstance(item, str) for item in value):
return value
msg = (
f"Expected str or list of str for key '{key}', "
"got list with non-string items"
)
raise ConfigError(msg)

msg = f"Expected str or list of str for key '{key}', got {type(value).__name__}"
raise ConfigError(msg)
return _get_type(mapping, key, default, str)


def get_str_enum(
Expand Down Expand Up @@ -211,10 +165,7 @@ def get_str_enum(
ConfigError
If the value associated with the key is not a valid enum value.
"""
value = _get_type(mapping, key, str)

if value is None:
value = default
value = _get_type(mapping, key, default, str)

if value is None:
return None
Expand Down Expand Up @@ -254,10 +205,7 @@ def get_str_list(
If the value associated with the key is not a list of strings.

"""
value = _get_type(mapping, key, list)

if value is None:
value = default
value = mapping.get(key, default)

if value is None:
return []
Expand Down
112 changes: 3 additions & 109 deletions src/devops/config/config_file.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
"""Module to parse file configuration values."""

from dataclasses import dataclass, field
from dataclasses import dataclass
from pathlib import Path

from .base import ConfigError, get_str, get_str_or_str_list, get_table
from .base import ConfigError, get_str, get_table


@dataclass(frozen=True)
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

if not self.changelog_paths:
msg = (
"No changelog paths configured; "
"cannot determine default changelog path."
)
raise ConfigError(msg)
return self.changelog_paths[0]

def to_toml_lines(self) -> list[str]:
"""Convert the FileConfig to TOML lines.
Expand All @@ -45,9 +23,6 @@ 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


Expand All @@ -71,93 +46,12 @@ 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 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.
"""
# Use the same default as FileConfig, but expressed as a list of strings.
default_paths = [str(p) for p in FileConfig().changelog_paths]
changelog_paths = get_str_or_str_list(
table,
"changelog_paths",
default=default_paths,
)

# If the configuration explicitly provides no value, fall back to the default.
if changelog_paths is None:
changelog_paths = default_paths
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
return FileConfig(encoding=encoding)
2 changes: 1 addition & 1 deletion src/devops/files/update_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(self, message: str) -> None:
self.message = message


def update_changelog(version: str, changelog_path: Path) -> None:
def update_changelog(version: str, changelog_path: Path = __CHANGELOG_PATH__) -> None:
"""Update the changelog file with a new version entry.

Parameters
Expand Down
55 changes: 8 additions & 47 deletions src/devops/scripts/update_changelog.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,33 @@
"""Script for updating the changelog file."""

import sys
from pathlib import Path

import typer

from devops.config import config
from devops.files import update_changelog
from devops.files.update_changelog import DevOpsChangelogError
from devops.files.update_changelog import update_changelog as update_changelog_func
from devops.utils import mstd_print

update_changelog = typer.Typer()
update_changelogs = typer.Typer()
app = typer.Typer()

# Alias for tests and backwards compatibility
app = update_changelog


@update_changelog.command()
def _update_changelog(version: str, changelog_path: str | None = None) -> None:
@app.command()
def main(version: str) -> None:
"""Update the changelog file with a new version entry.

Parameters
----------
version: str
The new version to add to the changelog.
changelog_path: str | None
The path to the changelog file. If None, it defaults
to the default_changelog_path from the configuration file.

"""
if changelog_path is None:
changelog_path = config.file.default_changelog_path
else:
changelog_path = Path(changelog_path)
try:
update_changelog_func(version, changelog_path)
update_changelog.update_changelog(version)
mstd_print(f"✅ CHANGELOG.md updated for version {version}")
except DevOpsChangelogError as e:
mstd_print(f"❌ Error updating changelog: {e}")
sys.exit(1)


@update_changelogs.command()
def _update_changelogs(version: str, changelog_paths: list[str] | None = None) -> None:
"""Update multiple changelog files with a new version entry.

Parameters
----------
version: str
The new version to add to the changelogs.
changelog_paths: list[str] | None
The list of paths to the changelog files. If None, it defaults
to the changelog_paths from the configuration file.
"""
if changelog_paths is None:
changelog_paths = config.file.changelog_paths
else:
changelog_paths = [Path(p) for p in changelog_paths]

failed_updates = []

for changelog_path in changelog_paths:
try:
update_changelog_func(version, changelog_path)
mstd_print(f"✅ {changelog_path} updated for version {version}")
except DevOpsChangelogError as e:
mstd_print(f"❌ Error updating {changelog_path}: {e}")
failed_updates.append(changelog_path)

if failed_updates:
sys.exit(1)
if __name__ == "__main__":
app()
Loading
Loading