Skip to content

Commit f870236

Browse files
committed
Add dependency updates to changelog
1 parent 2a50de1 commit f870236

File tree

6 files changed

+181
-40
lines changed

6 files changed

+181
-40
lines changed

exasol/toolbox/nox/_release.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ def prepare_release(session: Session) -> None:
119119
_ = _update_project_version(session, new_version)
120120

121121
changelogs = Changelogs(
122-
changes_path=PROJECT_CONFIG.doc / "changes", version=new_version
122+
changes_path=PROJECT_CONFIG.doc / "changes",
123+
source_path=PROJECT_CONFIG.root,
124+
version=new_version,
123125
)
124126
changelogs.update_changelogs_for_release()
125127
changed_files = changelogs.get_changed_files()

exasol/toolbox/util/dependencies/poetry_dependencies.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import subprocess
44
import tempfile
5+
from collections import OrderedDict
56
from pathlib import Path
67
from typing import Optional
78

@@ -105,8 +106,10 @@ def _extract_from_poetry_show(
105106
}
106107

107108
@property
108-
def direct_dependencies(self) -> dict[str, dict[NormalizedPackageStr, Package]]:
109-
dependencies = {}
109+
def direct_dependencies(
110+
self,
111+
) -> OrderedDict[str, dict[NormalizedPackageStr, Package]]:
112+
dependencies = OrderedDict()
110113
for group in self.groups:
111114
command = (
112115
"poetry",
@@ -127,7 +130,7 @@ def direct_dependencies(self) -> dict[str, dict[NormalizedPackageStr, Package]]:
127130
return dependencies
128131

129132
@property
130-
def all_dependencies(self) -> dict[str, dict[NormalizedPackageStr, Package]]:
133+
def all_dependencies(self) -> OrderedDict[str, dict[NormalizedPackageStr, Package]]:
131134
command = ("poetry", "show", "--no-truncate")
132135
output = subprocess.run(
133136
command,

exasol/toolbox/util/release/changelog.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,48 @@
22

33
from datetime import datetime
44
from inspect import cleandoc
5+
from itertools import chain
56
from pathlib import Path
67

8+
from exasol.toolbox.util.dependencies.poetry_dependencies import (
9+
get_dependencies,
10+
get_dependencies_from_latest_tag,
11+
)
12+
from exasol.toolbox.util.dependencies.track_changes import DependencyChanges
713
from exasol.toolbox.util.version import Version
814

915
UNRELEASED_INITIAL_CONTENT = "# Unreleased\n"
1016

1117

1218
class Changelogs:
13-
def __init__(self, changes_path: Path, version: Version) -> None:
19+
def __init__(self, changes_path: Path, source_path: Path, version: Version) -> None:
1420
self.version = version
1521
self.unreleased_md: Path = changes_path / "unreleased.md"
1622
self.versioned_changelog_md: Path = changes_path / f"changes_{version}.md"
1723
self.changelog_md: Path = changes_path / "changelog.md"
24+
self.source_path: Path = source_path
1825

1926
def _create_new_unreleased(self):
2027
"""
2128
Write a new unreleased changelog file.
2229
"""
2330
self.unreleased_md.write_text(UNRELEASED_INITIAL_CONTENT)
2431

25-
def _create_versioned_changelog(self, content: str) -> None:
32+
def _create_versioned_changelog(self, unreleased_content: str) -> None:
2633
"""
2734
Create a changelog entry for a specific version.
2835
2936
Args:
30-
content: The content of the changelog entry.
37+
unreleased_content: The content of the changelog entry.
3138
3239
"""
33-
template = cleandoc(
34-
f"""
35-
# {self.version} - {datetime.today().strftime("%Y-%m-%d")}
40+
template = f"# {self.version} - {datetime.today().strftime('%Y-%m-%d')}"
41+
template += f"\n{unreleased_content}"
3642

37-
{content}
38-
"""
39-
)
40-
self.versioned_changelog_md.write_text(template)
43+
if dependency_content := self._prepare_dependency_update():
44+
template += f"\n## Dependency Updates\n{dependency_content}"
45+
46+
self.versioned_changelog_md.write_text(cleandoc(template))
4147

4248
def _extract_unreleased_notes(self) -> str:
4349
"""
@@ -50,6 +56,36 @@ def _extract_unreleased_notes(self) -> str:
5056
unreleased_content += "\n"
5157
return unreleased_content
5258

59+
def _prepare_dependency_update(self) -> str:
60+
old_dependencies_in_groups = get_dependencies_from_latest_tag()
61+
current_dependencies_in_groups = get_dependencies(
62+
working_directory=self.source_path
63+
)
64+
65+
content = ""
66+
# preserve order of keys from old group
67+
groups = list(
68+
dict.fromkeys(
69+
chain(
70+
old_dependencies_in_groups.keys(),
71+
current_dependencies_in_groups.keys(),
72+
)
73+
)
74+
)
75+
for group in groups:
76+
old_dependencies = old_dependencies_in_groups.get(group, {})
77+
current_dependencies = current_dependencies_in_groups.get(group, {})
78+
changes = DependencyChanges(
79+
old_dependencies=old_dependencies,
80+
current_dependencies=current_dependencies,
81+
).changes
82+
if changes:
83+
# nicer group heading
84+
content += f"\n### `{group}`\n"
85+
content += "\n".join(str(change) for change in changes)
86+
content += "\n"
87+
return content
88+
5389
def _update_changelog_table_of_contents(self) -> None:
5490
"""
5591
Read in existing `changelog.md` and append to appropriate sections

test/unit/util/conftest.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from collections import OrderedDict
2+
3+
import pytest
4+
5+
from exasol.toolbox.util.dependencies.poetry_dependencies import PoetryGroup
6+
from exasol.toolbox.util.dependencies.shared_models import Package
7+
8+
9+
@pytest.fixture(scope="module")
10+
def main_group():
11+
return PoetryGroup(name="main", toml_section="project.dependencies")
12+
13+
14+
@pytest.fixture(scope="module")
15+
def dev_group():
16+
return PoetryGroup(name="dev", toml_section="tool.poetry.group.dev.dependencies")
17+
18+
19+
@pytest.fixture(scope="module")
20+
def old_dependencies(main_group, dev_group):
21+
deps = OrderedDict()
22+
deps[main_group.name] = {"package1": Package(name="package1", version="0.0.1")}
23+
return deps
24+
25+
26+
@pytest.fixture(scope="module")
27+
def dependencies(main_group, dev_group):
28+
deps = OrderedDict()
29+
deps[main_group.name] = {"package1": Package(name="package1", version="0.1.0")}
30+
deps[dev_group.name] = {"package2": Package(name="package2", version="0.2.0")}
31+
return deps

test/unit/util/dependencies/licenses_test.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,6 @@
99
_normalize,
1010
_packages_from_json,
1111
)
12-
from exasol.toolbox.util.dependencies.poetry_dependencies import PoetryGroup
13-
from exasol.toolbox.util.dependencies.shared_models import (
14-
Package,
15-
)
16-
17-
MAIN_GROUP = PoetryGroup(name="main", toml_section="project.dependencies")
18-
DEV_GROUP = PoetryGroup(name="dev", toml_section="tool.poetry.group.dev.dependencies")
1912

2013

2114
class TestPackageLicense:
@@ -101,15 +94,7 @@ def test_packages_from_json():
10194
}
10295

10396

104-
@pytest.fixture(scope="class")
105-
def dependencies():
106-
return {
107-
MAIN_GROUP.name: {"package1": Package(name="package1", version="0.1.0")},
108-
DEV_GROUP.name: {"package2": Package(name="package2", version="0.2.0")},
109-
}
110-
111-
112-
@pytest.fixture(scope="class")
97+
@pytest.fixture(scope="module")
11398
def package_license_report(dependencies):
11499
licenses = {
115100
"package1": PackageLicense(
@@ -131,9 +116,9 @@ def package_license_report(dependencies):
131116

132117
class TestPackageLicenseReport:
133118
@staticmethod
134-
def test_format_group_table_header(package_license_report):
119+
def test_format_group_table_header(package_license_report, main_group):
135120
result = package_license_report._format_group_table_header(
136-
group=MAIN_GROUP.name
121+
group=main_group.name
137122
)
138123

139124
assert (
@@ -142,11 +127,11 @@ def test_format_group_table_header(package_license_report):
142127
)
143128

144129
@staticmethod
145-
def test_format_group_table(package_license_report, dependencies):
146-
group_package_names = set(dependencies[MAIN_GROUP.name].keys())
130+
def test_format_group_table(package_license_report, dependencies, main_group):
131+
group_package_names = set(dependencies[main_group.name].keys())
147132

148133
result = package_license_report._format_group_table(
149-
group=MAIN_GROUP.name, group_package_names=group_package_names
134+
group=main_group.name, group_package_names=group_package_names
150135
)
151136

152137
assert result == (

test/unit/util/release/changelog_test.py

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from inspect import cleandoc
2+
from unittest import mock
23

34
import pytest
45

@@ -70,9 +71,35 @@ def unreleased_md(changelogs):
7071
)
7172

7273

74+
@pytest.fixture(scope="function")
75+
def mock_dependencies(dependencies, old_dependencies):
76+
with mock.patch.multiple(
77+
"exasol.toolbox.util.release.changelog",
78+
get_dependencies_from_latest_tag=lambda: old_dependencies,
79+
get_dependencies=lambda working_directory: dependencies,
80+
):
81+
yield
82+
83+
84+
@pytest.fixture(scope="function")
85+
def mock_no_dependencies():
86+
with mock.patch.multiple(
87+
"exasol.toolbox.util.release.changelog",
88+
get_dependencies_from_latest_tag=lambda: {},
89+
get_dependencies=lambda working_directory: {},
90+
):
91+
yield
92+
93+
7394
@pytest.fixture(scope="function")
7495
def changelogs(tmp_path) -> Changelogs:
75-
return Changelogs(changes_path=tmp_path, version=Version(major=1, minor=0, patch=0))
96+
changes_path = tmp_path / "doc/changes"
97+
changes_path.mkdir(parents=True)
98+
return Changelogs(
99+
changes_path=changes_path,
100+
source_path=tmp_path,
101+
version=Version(major=1, minor=0, patch=0),
102+
)
76103

77104

78105
class TestChangelogs:
@@ -90,7 +117,7 @@ def test_create_new_unreleased(changelogs):
90117
assert changelogs.unreleased_md.read_text() == UNRELEASED_INITIAL_CONTENT
91118

92119
@staticmethod
93-
def test_create_versioned_changelog(changelogs):
120+
def test_create_versioned_changelog(changelogs, mock_dependencies):
94121
changelogs._create_versioned_changelog(SampleContent.changelog)
95122
saved_text = changelogs.versioned_changelog_md.read_text()
96123

@@ -103,14 +130,28 @@ def test_extract_unreleased_notes(changelogs, unreleased_md):
103130

104131
assert result == SampleContent.changelog + "\n"
105132

133+
@staticmethod
134+
def test_prepare_dependency_update(changelogs, mock_dependencies):
135+
result = changelogs._prepare_dependency_update()
136+
assert result == (
137+
"\n"
138+
"### `main`\n"
139+
"* Updated dependency `package1:0.0.1` to `0.1.0`\n"
140+
"\n"
141+
"### `dev`\n"
142+
"* Added dependency `package2:0.2.0`\n"
143+
)
144+
106145
@staticmethod
107146
def test_update_changelog_table_of_contents(changelogs, changes_md):
108147
changelogs._update_changelog_table_of_contents()
109148

110149
assert changelogs.changelog_md.read_text() == SampleContent.altered_changes
111150

112151
@staticmethod
113-
def test_update_changelogs_for_release(changelogs, unreleased_md, changes_md):
152+
def test_update_changelogs_for_release(
153+
changelogs, mock_dependencies, unreleased_md, changes_md
154+
):
114155
changelogs.update_changelogs_for_release()
115156

116157
# changes.md
@@ -119,5 +160,48 @@ def test_update_changelogs_for_release(changelogs, unreleased_md, changes_md):
119160
assert changelogs.unreleased_md.read_text() == UNRELEASED_INITIAL_CONTENT
120161
# versioned.md
121162
saved_text = changelogs.versioned_changelog_md.read_text()
122-
assert "1.0.0" in saved_text
123-
assert SampleContent.changelog in saved_text
163+
assert saved_text == cleandoc(
164+
"""# 1.0.0 - 2025-07-24
165+
## Added
166+
* Added Awesome feature
167+
168+
## Changed
169+
* Some behaviour
170+
171+
## Fixed
172+
* Fixed nasty bug
173+
174+
## Dependency Updates
175+
176+
### `main`
177+
* Updated dependency `package1:0.0.1` to `0.1.0`
178+
179+
### `dev`
180+
* Added dependency `package2:0.2.0`
181+
"""
182+
)
183+
184+
@staticmethod
185+
def test_update_changelogs_for_release_with_no_dependencies(
186+
changelogs, mock_no_dependencies, unreleased_md, changes_md
187+
):
188+
changelogs.update_changelogs_for_release()
189+
190+
# changes.md
191+
assert changelogs.changelog_md.read_text() == SampleContent.altered_changes
192+
# unreleased.md
193+
assert changelogs.unreleased_md.read_text() == UNRELEASED_INITIAL_CONTENT
194+
# versioned.md
195+
saved_text = changelogs.versioned_changelog_md.read_text()
196+
assert saved_text == cleandoc(
197+
"""# 1.0.0 - 2025-07-24
198+
## Added
199+
* Added Awesome feature
200+
201+
## Changed
202+
* Some behaviour
203+
204+
## Fixed
205+
* Fixed nasty bug
206+
"""
207+
)

0 commit comments

Comments
 (0)