Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
46b0bb4
feat: add get_str_or_str_list function and update FileConfig with cha…
97gamjak Jan 15, 2026
6a5e8c3
feat: refactor _get_type function and enhance parsing for changelog p…
97gamjak Jan 16, 2026
a32d1a6
feat: update FileConfig to support multiple changelog paths and defau…
97gamjak Jan 16, 2026
8839253
docs: update changelog for changelog_paths config
97gamjak Jan 16, 2026
ea74811
Update src/devops/config/config_file.py
97gamjak Jan 17, 2026
7e9317f
Update src/devops/config/base.py
97gamjak Jan 17, 2026
860f600
Update src/devops/config/config_file.py
97gamjak Jan 17, 2026
6f020e1
Update src/devops/config/config_file.py
97gamjak Jan 17, 2026
f6434ac
Update src/devops/config/config_file.py
97gamjak Jan 17, 2026
51c0c57
Update src/devops/config/config_file.py
97gamjak Jan 17, 2026
8dea888
Initial plan
Copilot Jan 17, 2026
6432e89
Initial plan
Copilot Jan 17, 2026
6511ae9
Initial plan
Copilot Jan 17, 2026
26a70ba
Initial plan
Copilot Jan 17, 2026
4549482
Initial plan
Copilot Jan 17, 2026
db0aad0
Add comprehensive test coverage for parse_changelog_path function
Copilot Jan 17, 2026
fae9e5a
test: add comprehensive test coverage for parse_changelog_path function
Copilot Jan 17, 2026
20be204
Add comprehensive tests for parse_default_changelog_path function
Copilot Jan 17, 2026
fc72103
Remove duplicate test case
Copilot Jan 17, 2026
e6ae286
fix: simplify type annotation to match function signature
Copilot Jan 17, 2026
efed2d6
Fix type annotation for empty dict in test
Copilot Jan 17, 2026
ca2f022
Add comprehensive tests for parse_default_changelog_path function
Copilot Jan 17, 2026
510dc32
Fix type annotation in test_parse_default_changelog_path_with_missing…
Copilot Jan 17, 2026
24f4e11
feat: add comprehensive test coverage for get_str_or_str_list function
Copilot Jan 17, 2026
94c8d53
Merge pull request #75 from 97gamjak/copilot/sub-pr-74
97gamjak Jan 17, 2026
72ec272
test: add comprehensive tests for get_str_or_str_list function
Copilot Jan 17, 2026
8d86f9c
Merge branch 'feature/DEV-75-add-changelog-path-to-toml-config' into …
97gamjak Jan 17, 2026
a9f9100
Merge pull request #76 from 97gamjak/copilot/sub-pr-74-again
97gamjak Jan 17, 2026
9d1a6e0
Merge branch 'feature/DEV-75-add-changelog-path-to-toml-config' into …
97gamjak Jan 17, 2026
4ba6c17
Merge pull request #77 from 97gamjak/copilot/sub-pr-74-another-one
97gamjak Jan 17, 2026
aafde8f
Merge branch 'feature/DEV-75-add-changelog-path-to-toml-config' into …
97gamjak Jan 17, 2026
2a22334
Merge pull request #78 from 97gamjak/copilot/sub-pr-74-yet-again
97gamjak Jan 17, 2026
24a763d
Merge pull request #79 from 97gamjak/copilot/sub-pr-74-one-more-time
97gamjak Jan 17, 2026
c73860c
Merge branch 'feature/DEV-75-add-changelog-path-to-toml-config' into …
97gamjak Jan 17, 2026
d1c43c9
Merge pull request #80 from 97gamjak/copilot/sub-pr-74-please-work
97gamjak Jan 17, 2026
d0a13b3
fix: ruff check errors
97gamjak Jan 17, 2026
759b8af
Initial plan
Copilot Jan 17, 2026
9ec6ec2
fix: avoid re-fetching value from mapping in get_str_or_str_list
Copilot Jan 17, 2026
140d0f5
Merge pull request #81 from 97gamjak/copilot/sub-pr-74-one-more-time
97gamjak Jan 17, 2026
d578f44
Merge pull request #74 from 97gamjak/feature/DEV-75-add-changelog-pat…
97gamjak Jan 17, 2026
c7b288d
feat: add changelog_path argument to update_changelog function and up…
97gamjak Jan 17, 2026
52210fd
Initial plan
Copilot Jan 17, 2026
aea121a
feat: add optional changelog_path argument to main function in update…
97gamjak Jan 17, 2026
783d7ce
feat: add CLI tests for update_changelog and fix Path conversion
Copilot Jan 17, 2026
24974d2
fix: use Path objects in test mocks to match actual implementation
Copilot Jan 17, 2026
665eb88
Merge branch 'feature/DEV-72-add-path-cli-argument-to-update-changelo…
97gamjak Jan 17, 2026
f8e9fcd
Merge pull request #83 from 97gamjak/copilot/sub-pr-82
97gamjak Jan 17, 2026
6f0a472
fix: update changelog function call to use changelog_path argument
97gamjak Jan 17, 2026
cd008b4
Merge pull request #82 from 97gamjak/feature/DEV-72-add-path-cli-argu…
97gamjak Jan 17, 2026
70b6d0c
feat: add support for updating multiple changelogs with new version e…
97gamjak Jan 18, 2026
545e818
Update CHANGELOG.md
97gamjak Jan 18, 2026
b033141
Update src/devops/scripts/update_changelog.py
97gamjak Jan 18, 2026
366c929
Initial plan
Copilot Jan 18, 2026
c0271e6
Initial plan
Copilot Jan 18, 2026
fc2e66f
Fix breaking change: update test imports for refactored update_change…
Copilot Jan 18, 2026
bed0b62
Merge pull request #86 from 97gamjak/copilot/sub-pr-84-again
97gamjak Jan 18, 2026
744f047
Add comprehensive test coverage for _update_changelogs function
Copilot Jan 18, 2026
87063d9
Remove redundant Path conversion for config.file.changelog_paths
Copilot Jan 18, 2026
f350ffd
Merge branch 'feature/DEV-76-make-it-possible-to-use-update-changelog…
97gamjak Jan 18, 2026
998c9bf
Merge pull request #85 from 97gamjak/copilot/sub-pr-84
97gamjak Jan 18, 2026
ea66959
fix: ruff linting
97gamjak Jan 18, 2026
f79cdc7
Merge pull request #84 from 97gamjak/feature/DEV-76-make-it-possible-…
97gamjak Jan 18, 2026
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: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ 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: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ 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:app"
update_changelog = "devops.scripts.update_changelog:update_changelog"
update_changelogs = "devops.scripts.update_changelog:update_changelogs"
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 @@ -24,4 +25,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: 62 additions & 10 deletions src/devops/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -136,7 +137,52 @@ 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 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)


def get_str_enum(
Expand Down Expand Up @@ -165,7 +211,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
Expand Down Expand Up @@ -205,7 +254,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 []
Expand Down
112 changes: 109 additions & 3 deletions src/devops/config/config_file.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
"""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)
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 @@ -23,6 +45,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


Expand All @@ -46,12 +71,93 @@ 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.
"""
# 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
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 = __CHANGELOG_PATH__) -> None:
def update_changelog(version: str, changelog_path: Path) -> None:
"""Update the changelog file with a new version entry.

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

import sys
from pathlib import Path

import typer

from devops.files import update_changelog
from devops.config import config
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

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

# Alias for tests and backwards compatibility
app = update_changelog

@app.command()
def main(version: str) -> None:

@update_changelog.command()
def _update_changelog(version: str, changelog_path: str | None = None) -> 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.update_changelog(version)
update_changelog_func(version, changelog_path)
mstd_print(f"✅ CHANGELOG.md updated for version {version}")
except DevOpsChangelogError as e:
mstd_print(f"❌ Error updating changelog: {e}")
sys.exit(1)


if __name__ == "__main__":
app()
@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)
Loading
Loading