Skip to content

Commit 51b254d

Browse files
authored
GH28: Switch semver string to semver info (#29)
1 parent fd85edd commit 51b254d

11 files changed

Lines changed: 222 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,21 @@
88
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1010

11+
<!-- BEGIN RELEASE NOTES -->
1112
### [Unreleased]
1213

14+
### [0.6.0] - 2023-02-08
15+
16+
#### Changed
17+
- Pattern recognition of partitioned sections within the changelog file.
18+
- Semantic Version strings with old semver API to use the new semver.VersionInfo class.
19+
20+
#### Removed
21+
- The LINKS section of the changelog file.
22+
23+
#### Fixed
24+
- Incomplete semantic version parsing. Prerelease and Build sections are now allowed.
25+
1326
### [0.5.0] - 2023-02-04
1427

1528
#### Added
@@ -64,10 +77,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6477
- `unreleased` command, with unreleased subcommands.
6578
- `unreleased content`, which lists the unreleased content.
6679
- `unreleased add`, which allows inline or prompted adding of unreleased changes.
67-
68-
### [LINKS]
69-
70-
[Unreleased]: https://github.com/award28/changelogger/compare/0.5.0...HEAD
80+
<!-- END RELEASE NOTES -->
81+
<!-- BEGIN LINKS -->
82+
[Unreleased]: https://github.com/award28/changelogger/compare/0.6.0...HEAD
83+
[0.6.0]: https://github.com/award28/changelogger/compare/0.5.0...0.6.0
7184
[0.5.0]: https://github.com/award28/changelogger/compare/0.4.0...0.5.0
7285
[0.4.0]: https://github.com/award28/changelogger/compare/0.3.4...0.4.0
7386
[0.3.4]: https://github.com/award28/changelogger/compare/0.3.3...0.3.4
@@ -78,3 +91,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7891
[0.2.1]: https://github.com/award28/changelogger/compare/0.2.0...0.2.1
7992
[0.2.0]: https://github.com/award28/changelogger/compare/0.1.0...0.2.0
8093
[0.1.0]: https://github.com/award28/changelogger/commit/fc688488620df4fe014c9d1b55782b75a674fa15
94+
<!-- END LINKS -->

changelogger/app/manage/commands/check.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ def _check() -> None:
3535

3636
# Validate all release notes are parseable for all versions
3737
# Point of Failure 1
38-
changelog_versions = ["Unreleased", *all_versions, "LINKS"]
39-
for version, prev_version in zip(
38+
changelog_versions = ["Unreleased", *all_versions, None]
39+
for new_version, old_version in zip(
4040
changelog_versions, changelog_versions[1:]
4141
):
4242
try:
43-
changelog.get_release_notes(version, prev_version)
44-
except:
43+
changelog.get_release_notes(new_version, old_version) # type: ignore
44+
except Exception as e:
4545
raise ValidationException(
46-
f"Failed to validate notes for version {version}"
46+
f"Failed to validate notes for version {new_version}: {str(e)}."
4747
)
4848

4949
# Validate there are links in the expected format for all versions

changelogger/app/manage/commands/content.py

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

44
from changelogger import changelog
55
from changelogger.exceptions import CommandException
6+
from changelogger.models.domain_models import VersionInfo
67

78

89
def content(
910
version: str,
1011
pretty: bool = True,
1112
) -> None:
13+
version_info = VersionInfo.parse(version)
1214
"""Retrieves the changelog content for the specified version."""
1315
all_versions = changelog.get_all_versions()
14-
if version not in all_versions:
16+
if version_info not in all_versions:
1517
raise CommandException(f"Could not find version {version}.")
1618

17-
i = all_versions.index(version)
18-
prev_version = (
19-
all_versions[i + 1] if i + 1 < len(all_versions) else "LINKS"
20-
)
21-
release_notes = changelog.get_release_notes(version, prev_version)
19+
i = all_versions.index(version_info)
20+
prev_version = all_versions[i + 1] if i + 1 < len(all_versions) else None
21+
release_notes = changelog.get_release_notes(version_info, prev_version)
2222

2323
md = release_notes.markdown()
2424
if pretty:

changelogger/app/manage/commands/upgrade.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ def upgrade(
2525
) -> None:
2626
"""Upgrades all versioned files, as specified in the changelogger config file."""
2727
old_version = changelog.get_latest_version()
28-
bump = getattr(semver, f"bump_{version_to_bump.value}")
29-
new_version = bump(old_version)
28+
bump = getattr(old_version, f"bump_{version_to_bump.value}")
29+
new_version = bump()
3030

3131
release_notes = changelog.get_release_notes("Unreleased", old_version)
3232
update = ChangelogUpdate(

changelogger/app/unreleased/commands/add.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def add(
3333
release_notes = changelog.get_release_notes("Unreleased", topmost_version)
3434

3535
update = ChangelogUpdate(
36-
new_version="",
36+
new_version=None,
3737
old_version=topmost_version,
3838
release_notes=release_notes,
3939
)

changelogger/changelog.py

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,128 @@
11
from pathlib import Path
2+
from typing import Literal
23

34
from changelogger.conf import settings
45
from changelogger.conf.models import VersionedFile
5-
from changelogger.exceptions import RollbackException, UpgradeException
6-
from changelogger.models.domain_models import ChangelogUpdate, ReleaseNotes
6+
from changelogger.exceptions import (
7+
CommandException,
8+
RollbackException,
9+
UpgradeException,
10+
)
11+
from changelogger.models.domain_models import (
12+
ChangelogUpdate,
13+
ReleaseNotes,
14+
VersionInfo,
15+
)
716
from changelogger.templating import update_with_jinja
817
from changelogger.utils import cached_compile
918

19+
CHANGELOG_PARTITION_RELEASE_NOTES = "RELEASE NOTES"
20+
CHANGELOG_PARTITION_LINKS = "LINKS"
1021

11-
def get_all_links() -> dict[str, str]:
12-
lines = settings.CHANGELOG_PATH.read_text().split("\n")
22+
23+
def _get_changelog_parition(partition: str) -> str:
24+
changelog_content = settings.CHANGELOG_PATH.read_text()
25+
26+
start_partition = f"<!-- BEGIN {partition} -->"
27+
end_partition = f"<!-- END {partition} -->"
28+
partition_re = cached_compile(
29+
rf"{start_partition}([\s\S]*){end_partition}"
30+
)
31+
32+
match = partition_re.search(changelog_content)
33+
if not match:
34+
raise CommandException(
35+
f"Expected partition for `{partition}`; None found."
36+
)
37+
return match[1]
38+
39+
40+
def _get_release_notes_parition() -> str:
41+
return _get_changelog_parition(CHANGELOG_PARTITION_RELEASE_NOTES)
42+
43+
44+
def _get_links_parition() -> str:
45+
return _get_changelog_parition(CHANGELOG_PARTITION_LINKS)
46+
47+
48+
def get_all_links() -> dict[VersionInfo | str, str]:
49+
lines = _get_links_parition().split("\n")
1350

1451
links = {}
1552
for line in lines:
1653
match = cached_compile(
17-
r"\[([\d.]+|Unreleased)]: (.*)",
54+
r"\[(.*)]: (.*)",
1855
).search(
1956
line,
2057
)
2158

2259
if not match:
2360
continue
2461

25-
links[match[1]] = match[2]
62+
version_str = match[1]
63+
link = match[2]
2664

27-
return links
65+
if version_str == "Unreleased":
66+
links[version_str] = link
67+
continue
68+
69+
match = VersionInfo._REGEX.fullmatch(version_str)
70+
if not match:
71+
continue
72+
73+
links[VersionInfo.parse(version_str)] = link
2874

75+
return links
2976

30-
def get_all_versions() -> list[str]:
31-
lines = settings.CHANGELOG_PATH.read_text().split("\n")
3277

78+
def get_all_versions() -> list[VersionInfo]:
79+
lines = _get_release_notes_parition().split("\n")
3380
versions = []
3481
for line in lines:
3582
match = cached_compile(
36-
r"### \[([\d.]+)]",
83+
r"### \[(.*)]",
3784
).search(
3885
line,
3986
)
4087

4188
if not match:
4289
continue
4390

44-
versions.append(match[1])
91+
version_str = match[1]
92+
93+
match = VersionInfo._REGEX.fullmatch(version_str)
94+
if not match:
95+
continue
96+
97+
versions.append(VersionInfo.parse(version_str))
4598
return versions
4699

47100

48-
def get_sorted_versions() -> list[str]:
49-
versions = get_all_versions()
50-
sorted_versions = sorted(
51-
(tuple(map(int, version.split("."))) for version in versions)
52-
)
53-
return [".".join(map(str, v)) for v in sorted_versions]
101+
def get_sorted_versions() -> list[VersionInfo]:
102+
return sorted(get_all_versions())
54103

55104

56-
def get_latest_version() -> str:
105+
def get_latest_version() -> VersionInfo:
57106
versions = get_sorted_versions()
58107
if not versions:
59108
raise UpgradeException(f"This changelog has no versions currently.")
60109
return versions[-1]
61110

62111

63-
def get_release_notes(version: str, prev_version: str) -> ReleaseNotes:
64-
version = version.replace(".", r"\.")
112+
def get_release_notes(
113+
new_version: VersionInfo | Literal["Unreleased"],
114+
old_version: VersionInfo | None,
115+
) -> ReleaseNotes:
116+
new_version_pattern = str(new_version).replace(".", r"\.")
65117

66-
content = settings.CHANGELOG_PATH.read_text()
118+
content = _get_release_notes_parition()
67119

120+
pattern = rf"### \[{new_version_pattern}\]( - \d+-\d+-\d+)?([\s\S]*)"
121+
if old_version:
122+
old_version_pattern = str(old_version).replace(".", r"\.")
123+
pattern += rf"### \[{old_version_pattern}\]"
68124
match = cached_compile(
69-
rf"### \[{version}\]( - \d+-\d+-\d+)?([\s\S]*)### \[{prev_version}\]",
125+
pattern,
70126
).search(
71127
content,
72128
)

changelogger/models/domain_models.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from enum import Enum
2+
from typing import Union
23

3-
from pydantic import BaseModel
4+
import semver
5+
from pydantic import BaseModel, validator
46

57

68
class SemVerType(Enum):
@@ -53,7 +55,19 @@ def markdown(self) -> str:
5355
return "\n".join(s.lstrip() for s in md.split("\n"))
5456

5557

58+
class VersionInfo(semver.VersionInfo):
59+
@classmethod
60+
def __get_validators__(cls):
61+
yield cls.validate
62+
63+
@validator("*")
64+
def validate(cls, v: Union[str, "VersionInfo"]) -> "VersionInfo":
65+
if isinstance(v, VersionInfo):
66+
return v
67+
return cls.parse(v)
68+
69+
5670
class ChangelogUpdate(BaseModel):
57-
new_version: str
58-
old_version: str
71+
new_version: VersionInfo | None
72+
old_version: VersionInfo | None
5973
release_notes: ReleaseNotes

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "changelogged"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
description = "Automated management of your CHANGELOG.md and other versioned files, following the principles of Keep a Changelog and Semantic Versioning."
55
license = "MIT"
66
authors = ["award28 <austin.ward@klaviyo.com>"]

tests/app/manage/commands/test_content.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ def mock_print(self):
2525
"version,prev_version,pretty",
2626
[
2727
(version, prev_version, pretty)
28-
for version, prev_version in zip(
29-
VERSIONS, VERSIONS[1:] + ["LINKS"]
30-
)
28+
for version, prev_version in zip(VERSIONS, VERSIONS[1:] + [None])
3129
for pretty in (True, False)
3230
],
3331
)
@@ -68,7 +66,7 @@ def test_content_version_not_found(
6866
mock_changelog,
6967
) -> None:
7068
mock_changelog.get_all_versions.side_effect = (self.VERSIONS,)
71-
version = "VERSION_DNE"
69+
version = "0.0.0"
7270
with pytest.raises(CommandException) as excinfo:
7371
content(version)
7472

tests/app/manage/commands/test_upgrade.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111

1212

13-
class TestManageContentCommand:
13+
class TestManageUpgradeCommand:
1414
VERSIONS = ["0.1.1", "0.2.0", "0.1.0"]
1515

1616
@pytest.fixture

0 commit comments

Comments
 (0)