From 3c7353c5d04c65c1de404d6a3ae4a124a6c97d4a Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 12:41:04 +0200
Subject: [PATCH 01/10] Add url to to_dict result if available
---
CHANGELOG.md | 2 ++
README.md | 3 +++
keepachangelog/_changelog.py | 14 +++++++++++++-
keepachangelog/_versioning.py | 10 +++++++---
tests/test_changelog.py | 3 +++
tests/test_changelog_unreleased.py | 4 ++++
tests/test_flask_restx.py | 3 +++
tests/test_starlette.py | 3 +++
8 files changed, 38 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56bfe58..491990b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Changed
+- `keepachangelog.to_dict` now contains `url` key for each item if a link is available for the version.
## [0.5.0] - 2021-04-19
### Added
diff --git a/README.md b/README.md
index 000265f..fb37f27 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ changes = {
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
"fixed": [
@@ -48,11 +49,13 @@ changes = {
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
```
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index aa699f7..23c8fc2 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -51,8 +51,12 @@ def add_category(release: dict, line: str) -> List[str]:
link_pattern = re.compile(r"^\[(.*)\]: (.*)$")
+def is_link(line: str) -> bool:
+ return link_pattern.fullmatch(line) is not None
+
+
def is_information(line: str) -> bool:
- return line and not link_pattern.fullmatch(line)
+ return line and not is_link(line)
def add_information(category: List[str], line: str):
@@ -61,6 +65,8 @@ def add_information(category: List[str], line: str):
def to_dict(changelog_path: str, *, show_unreleased: bool = False) -> Dict[str, dict]:
changes = {}
+ # As URLs can be defined before actual usage, maintain a separate dict
+ urls = {}
with open(changelog_path) as change_log:
current_release = {}
category = []
@@ -71,9 +77,15 @@ def to_dict(changelog_path: str, *, show_unreleased: bool = False) -> Dict[str,
current_release = add_release(changes, line, show_unreleased)
elif is_category(line):
category = add_category(current_release, line)
+ elif is_link(line):
+ link_match = link_pattern.fullmatch(line)
+ urls[link_match.group(1).lower()] = link_match.group(2)
elif is_information(line):
add_information(category, line)
+ for version, url in urls.items():
+ changes.get(version, {})["url"] = url
+
return changes
diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py
index be70fea..98f7ad9 100644
--- a/keepachangelog/_versioning.py
+++ b/keepachangelog/_versioning.py
@@ -7,8 +7,7 @@ def contains_breaking_changes(unreleased: dict) -> bool:
def only_contains_bug_fixes(unreleased: dict) -> bool:
- # unreleased contains at least 2 entries: version and release_date
- return "fixed" in unreleased and len(unreleased) == 3
+ return ["fixed"] == list(unreleased)
def bump_major(version: str) -> str:
@@ -44,7 +43,12 @@ def actual_version(changelog: dict) -> Optional[str]:
def guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]:
unreleased = changelog.get("unreleased", {})
- if not unreleased or len(unreleased) < 3:
+ # Only keep user provided entries
+ unreleased = unreleased.copy()
+ unreleased.pop("version", None)
+ unreleased.pop("release_date", None)
+ unreleased.pop("url", None)
+ if not unreleased:
raise Exception(
"Release content must be provided within changelog Unreleased section."
)
diff --git a/tests/test_changelog.py b/tests/test_changelog.py
index eaa991e..8e03beb 100644
--- a/tests/test_changelog.py
+++ b/tests/test_changelog.py
@@ -102,6 +102,7 @@ def test_changelog_with_versions_and_all_categories(changelog):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
"fixed": [
@@ -112,11 +113,13 @@ def test_changelog_with_versions_and_all_categories(changelog):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
diff --git a/tests/test_changelog_unreleased.py b/tests/test_changelog_unreleased.py
index 1495ebe..7280821 100644
--- a/tests/test_changelog_unreleased.py
+++ b/tests/test_changelog_unreleased.py
@@ -90,6 +90,7 @@ def test_changelog_with_versions_and_all_categories(changelog):
"security": ["Known issue 1", "Known issue 2"],
"deprecated": ["Deprecated feature 1", "Future removal 2"],
"removed": ["Deprecated feature 2", "Future removal 1"],
+ "url": "https://github.test_url/test_project/compare/v1.1.0...HEAD",
},
"1.1.0": {
"version": "1.1.0",
@@ -100,6 +101,7 @@ def test_changelog_with_versions_and_all_categories(changelog):
"sub enhancement 2",
"Enhancement 2 (1.1.0)",
],
+ "url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
"version": "1.0.1",
@@ -110,10 +112,12 @@ def test_changelog_with_versions_and_all_categories(changelog):
"sub bug 2",
"Bug fix 2 (1.0.1)",
],
+ "url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"version": "1.0.0",
"release_date": "2017-04-10",
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
+ "url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
diff --git a/tests/test_flask_restx.py b/tests/test_flask_restx.py
index 7c850a9..c201a1a 100644
--- a/tests/test_flask_restx.py
+++ b/tests/test_flask_restx.py
@@ -87,6 +87,7 @@ def test_changelog_endpoint_with_file(tmpdir):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
"fixed": [
@@ -97,11 +98,13 @@ def test_changelog_endpoint_with_file(tmpdir):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
diff --git a/tests/test_starlette.py b/tests/test_starlette.py
index f6f21b6..ef5777e 100644
--- a/tests/test_starlette.py
+++ b/tests/test_starlette.py
@@ -86,6 +86,7 @@ def test_changelog_endpoint_with_file(tmpdir):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
"fixed": [
@@ -96,11 +97,13 @@ def test_changelog_endpoint_with_file(tmpdir):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
From 5bd2512a476d146a1e219b568546fd44b798c5b9 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 13:01:01 +0200
Subject: [PATCH 02/10] Add url to to_raw_dict result if available
---
CHANGELOG.md | 1 +
keepachangelog/_changelog.py | 16 ++++++++++------
tests/test_changelog.py | 3 +++
3 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 491990b..62c9661 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- `keepachangelog.to_dict` now contains `url` key for each item if a link is available for the version.
+- `keepachangelog.to_raw_dict` now contains `url` key for each item if a link is available for the version.
## [0.5.0] - 2021-04-19
### Added
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index 23c8fc2..83975f9 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -55,10 +55,6 @@ def is_link(line: str) -> bool:
return link_pattern.fullmatch(line) is not None
-def is_information(line: str) -> bool:
- return line and not is_link(line)
-
-
def add_information(category: List[str], line: str):
category.append(line.lstrip(" *-").rstrip(" -"))
@@ -80,7 +76,7 @@ def to_dict(changelog_path: str, *, show_unreleased: bool = False) -> Dict[str,
elif is_link(line):
link_match = link_pattern.fullmatch(line)
urls[link_match.group(1).lower()] = link_match.group(2)
- elif is_information(line):
+ elif line:
add_information(category, line)
for version, url in urls.items():
@@ -91,6 +87,8 @@ def to_dict(changelog_path: str, *, show_unreleased: bool = False) -> Dict[str,
def to_raw_dict(changelog_path: str) -> Dict[str, dict]:
changes = {}
+ # As URLs can be defined before actual usage, maintain a separate dict
+ urls = {}
with open(changelog_path) as change_log:
current_release = {}
for line in change_log:
@@ -100,9 +98,15 @@ def to_raw_dict(changelog_path: str) -> Dict[str, dict]:
current_release = add_release(
changes, clean_line, show_unreleased=False
)
- elif is_category(clean_line) or is_information(clean_line):
+ elif is_link(clean_line):
+ link_match = link_pattern.fullmatch(clean_line)
+ urls[link_match.group(1).lower()] = link_match.group(2)
+ elif clean_line:
current_release["raw"] = current_release.get("raw", "") + line
+ for version, url in urls.items():
+ changes.get(version, {})["url"] = url
+
return changes
diff --git a/tests/test_changelog.py b/tests/test_changelog.py
index 8e03beb..128472e 100644
--- a/tests/test_changelog.py
+++ b/tests/test_changelog.py
@@ -162,6 +162,7 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "2018-05-31",
"version": "1.1.0",
+ "url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
"raw": """### Fixed
@@ -172,6 +173,7 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "2018-05-31",
"version": "1.0.1",
+ "url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"raw": """### Deprecated
@@ -180,5 +182,6 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "2017-04-10",
"version": "1.0.0",
+ "url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
From b87f3bdfd90ae3728941f1a0bde162397ff36102 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 15:19:14 +0200
Subject: [PATCH 03/10] Handle all kind of semantic version
---
CHANGELOG.md | 5 ++
keepachangelog/_versioning.py | 103 ++++++++++++++++++++++----------
tests/test_changelog_release.py | 86 +++++++++++++++++++++++++-
3 files changed, 161 insertions(+), 33 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62c9661..2a13482 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `keepachangelog.to_dict` now contains `url` key for each item if a link is available for the version.
- `keepachangelog.to_raw_dict` now contains `url` key for each item if a link is available for the version.
+### Fixed
+- `keepachangelog.release` now allows `pre-release` and `build metadata` information as part of valid semantic version. As per [semantic versioning specifications](https://semver.org).
+ To ensure compatibility with some python specific versioning, `pre-release` is also handled as not being prefixed with `-`, or prefixed with `.`.
+- `keepachangelog.release` will now bump a pre-release version to a stable version. It was previously failing.
+
## [0.5.0] - 2021-04-19
### Added
- `keepachangelog.release` function to guess new version number based on `Unreleased` section, update changelog and return new version number.
diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py
index 98f7ad9..4a6a4c2 100644
--- a/keepachangelog/_versioning.py
+++ b/keepachangelog/_versioning.py
@@ -2,6 +2,15 @@
from typing import Tuple, Optional
+initial_semantic_version = {
+ "major": 0,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ "buildmetadata": None,
+}
+
+
def contains_breaking_changes(unreleased: dict) -> bool:
return "removed" in unreleased or "changed" in unreleased
@@ -10,35 +19,59 @@ def only_contains_bug_fixes(unreleased: dict) -> bool:
return ["fixed"] == list(unreleased)
-def bump_major(version: str) -> str:
- major, *_ = to_semantic(version)
- return from_semantic(major + 1, 0, 0)
+def bump_major(semantic_version: dict):
+ semantic_version["major"] += 1
+ semantic_version["minor"] = 0
+ semantic_version["patch"] = 0
+ semantic_version["prerelease"] = None
+ semantic_version["buildmetadata"] = None
+
+
+def bump_minor(semantic_version: dict) -> str:
+ semantic_version["minor"] += 1
+ semantic_version["patch"] = 0
+ semantic_version["prerelease"] = None
+ semantic_version["buildmetadata"] = None
-def bump_minor(version: str) -> str:
- major, minor, _ = to_semantic(version)
- return from_semantic(major, minor + 1, 0)
+def bump_patch(semantic_version: dict) -> str:
+ semantic_version["patch"] += 1
+ semantic_version["prerelease"] = None
+ semantic_version["buildmetadata"] = None
-def bump_patch(version: str) -> str:
- major, minor, patch = to_semantic(version)
- return from_semantic(major, minor, patch + 1)
+def bump(unreleased: dict, semantic_version: dict) -> dict:
+ if semantic_version["prerelease"]:
+ semantic_version["prerelease"] = None
+ semantic_version["buildmetadata"] = None
+ elif contains_breaking_changes(unreleased):
+ bump_major(semantic_version)
+ elif only_contains_bug_fixes(unreleased):
+ bump_patch(semantic_version)
+ else:
+ bump_minor(semantic_version)
+ return semantic_version
-def bump(unreleased: dict, version: str) -> str:
- if contains_breaking_changes(unreleased):
- return bump_major(version)
- if only_contains_bug_fixes(unreleased):
- return bump_patch(version)
- return bump_minor(version)
+def semantic_order(version: Tuple[str, dict]) -> str:
+ _, semantic_version = version
+ # Ensure release is "bigger than" pre-release
+ pre_release_order = (
+ f"0{semantic_version['prerelease']}" if semantic_version["prerelease"] else "1"
+ )
+ return f"{semantic_version['major']}.{semantic_version['minor']}.{semantic_version['patch']}.{pre_release_order}"
-def actual_version(changelog: dict) -> Optional[str]:
- versions = sorted(changelog.keys())
- current_version = versions.pop() if versions else None
- while "unreleased" == current_version:
- current_version = versions.pop() if versions else None
- return current_version
+def actual_version(changelog: dict) -> Tuple[Optional[str], dict]:
+ versions = sorted(
+ [
+ (version, to_semantic(version))
+ for version in changelog.keys()
+ if version != "unreleased"
+ ],
+ key=semantic_order,
+ )
+ return versions.pop() if versions else (None, initial_semantic_version.copy())
def guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]:
@@ -53,24 +86,30 @@ def guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]:
"Release content must be provided within changelog Unreleased section."
)
- version = actual_version(changelog)
- return version, bump(unreleased, version)
+ version, semantic_version = actual_version(changelog)
+ return version, from_semantic(bump(unreleased, semantic_version))
-# Semantic versioning pattern should match version like 1.2.3"
-version_pattern = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
+semantic_versioning = re.compile(
+ r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:[-\.]?(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
+)
-def to_semantic(version: Optional[str]) -> Tuple[int, int, int]:
+def to_semantic(version: Optional[str]) -> dict:
if not version:
- return 0, 0, 0
+ return initial_semantic_version.copy()
- match = version_pattern.fullmatch(version)
+ match = semantic_versioning.fullmatch(version)
if match:
- return int(match.group(1)), int(match.group(2)), int(match.group(3))
+ return {
+ key: int(value) if key in ("major", "minor", "patch") else value
+ for key, value in match.groupdict().items()
+ }
- raise Exception(f"{version} is not following semantic versioning.")
+ raise Exception(
+ f"{version} is not following semantic versioning. Check https://semver.org for more information."
+ )
-def from_semantic(major: int, minor: int, patch: int) -> str:
- return f"{major}.{minor}.{patch}"
+def from_semantic(semantic_version: dict) -> str:
+ return f"{semantic_version['major']}.{semantic_version['minor']}.{semantic_version['patch']}"
diff --git a/tests/test_changelog_release.py b/tests/test_changelog_release.py
index 95e63eb..df9308d 100644
--- a/tests/test_changelog_release.py
+++ b/tests/test_changelog_release.py
@@ -79,6 +79,13 @@ def major_changelog(tmpdir):
- sub enhancement 2
- Enhancement 2 (1.1.0)
+## [1.1.0.dev0] - 2018-05-31
+### Changed
+- Enhancement 1 (1.1.0)
+- sub *enhancement 1*
+- sub enhancement 2
+- Enhancement 2 (1.1.0)
+
## [1.0.1] - 2018-05-31
### Fixed
- Bug fix 1 (1.0.1)
@@ -146,6 +153,13 @@ def minor_changelog(tmpdir):
- sub bug 2
- Bug fix 2 (1.0.1)
+## [1.0.1.dev0] - 2018-05-31
+### Fixed
+- Bug fix 1 (1.0.1)
+- sub bug 1
+- sub bug 2
+- Bug fix 2 (1.0.1)
+
## [1.0.0] - 2017-04-10
### Deprecated
- Known issue 1 (1.0.0)
@@ -377,6 +391,32 @@ def non_semantic_changelog(tmpdir):
return changelog_file_path
+@pytest.fixture
+def unstable_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "UNSTABLE_CHANGELOG.md")
+ with open(changelog_file_path, "wt") as file:
+ file.write(
+ """# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+### Changed
+- Enhancement 1 (1.1.0)
+
+## [2.5.0b51] - 2018-05-31
+### Changed
+- Enhancement 1 (1.1.0)
+- sub *enhancement 1*
+- sub enhancement 2
+- Enhancement 2 (1.1.0)
+"""
+ )
+ return changelog_file_path
+
+
def test_major_release(major_changelog, mock_date):
assert keepachangelog.release(major_changelog) == "2.0.0"
with open(major_changelog) as file:
@@ -426,6 +466,13 @@ def test_major_release(major_changelog, mock_date):
- sub enhancement 2
- Enhancement 2 (1.1.0)
+## [1.1.0.dev0] - 2018-05-31
+### Changed
+- Enhancement 1 (1.1.0)
+- sub *enhancement 1*
+- sub enhancement 2
+- Enhancement 2 (1.1.0)
+
## [1.0.1] - 2018-05-31
### Fixed
- Bug fix 1 (1.0.1)
@@ -495,6 +542,13 @@ def test_minor_release(minor_changelog, mock_date):
- sub bug 2
- Bug fix 2 (1.0.1)
+## [1.0.1.dev0] - 2018-05-31
+### Fixed
+- Bug fix 1 (1.0.1)
+- sub bug 1
+- sub bug 2
+- Bug fix 2 (1.0.1)
+
## [1.0.0] - 2017-04-10
### Deprecated
- Known issue 1 (1.0.0)
@@ -692,4 +746,34 @@ def test_empty_unreleased_release(empty_unreleased_changelog):
def test_non_semantic_release(non_semantic_changelog):
with pytest.raises(Exception) as exception_info:
keepachangelog.release(non_semantic_changelog)
- assert str(exception_info.value) == "20180531 is not following semantic versioning."
+ assert (
+ str(exception_info.value)
+ == "20180531 is not following semantic versioning. Check https://semver.org for more information."
+ )
+
+
+def test_first_stable_release(unstable_changelog, mock_date):
+ assert keepachangelog.release(unstable_changelog) == "2.5.0"
+ with open(unstable_changelog) as file:
+ assert (
+ file.read()
+ == """# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [2.5.0] - 2021-03-19
+### Changed
+- Enhancement 1 (1.1.0)
+
+## [2.5.0b51] - 2018-05-31
+### Changed
+- Enhancement 1 (1.1.0)
+- sub *enhancement 1*
+- sub enhancement 2
+- Enhancement 2 (1.1.0)
+"""
+ )
From cfdad0f49aa00d929501b3c8b9febb96d8f1a79b Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 15:49:45 +0200
Subject: [PATCH 04/10] Handle all kind of semantic version
---
CHANGELOG.md | 2 +
README.md | 21 +++++++
keepachangelog/_changelog.py | 18 ++++--
keepachangelog/_versioning.py | 11 +++-
tests/test_changelog.py | 56 ++++++++++++++++++
tests/test_changelog_no_added.py | 28 +++++++++
tests/test_changelog_no_changed.py | 33 ++++++++++-
tests/test_changelog_no_deprecated.py | 33 ++++++++++-
tests/test_changelog_no_fixed.py | 28 +++++++++
tests/test_changelog_no_removed.py | 28 +++++++++
tests/test_changelog_no_security.py | 28 +++++++++
tests/test_changelog_unreleased.py | 21 +++++++
tests/test_flask_restx.py | 21 +++++++
tests/test_non_standard_changelog.py | 84 +++++++++++++++++++++++++++
tests/test_starlette.py | 21 +++++++
15 files changed, 423 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a13482..6ff7364 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- `keepachangelog.to_dict` now contains `url` key for each item if a link is available for the version.
- `keepachangelog.to_raw_dict` now contains `url` key for each item if a link is available for the version.
+- `keepachangelog.to_dict` now contains `semantic_version` key for each item if the version follows semantic versioning.
+- `keepachangelog.to_raw_dict` now contains `semantic_version` key for each item if the version follows semantic versioning.
### Fixed
- `keepachangelog.release` now allows `pre-release` and `build metadata` information as part of valid semantic version. As per [semantic versioning specifications](https://semver.org).
diff --git a/README.md b/README.md
index fb37f27..4cee8bf 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,13 @@ changes = {
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ "buildmetadata": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
@@ -49,12 +56,26 @@ changes = {
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ "buildmetadata": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ "buildmetadata": None,
+ },
"url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index 83975f9..14c9103 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -2,7 +2,11 @@
import re
from typing import Dict, List, Optional
-from keepachangelog._versioning import guess_unreleased_version
+from keepachangelog._versioning import (
+ guess_unreleased_version,
+ to_semantic,
+ InvalidSemanticVersion,
+)
def is_release(line: str) -> bool:
@@ -21,10 +25,14 @@ def add_release(changes: Dict[str, dict], line: str, show_unreleased: bool) -> d
if not show_unreleased and not release_date:
return {}
version = unlink(version)
- return changes.setdefault(
- version,
- {"version": version, "release_date": extract_date(release_date)},
- )
+
+ release_details = {"version": version, "release_date": extract_date(release_date)}
+ try:
+ release_details["semantic_version"] = to_semantic(version)
+ except InvalidSemanticVersion:
+ pass
+
+ return changes.setdefault(version, release_details)
def unlink(value: str) -> str:
diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py
index 4a6a4c2..5a8d670 100644
--- a/keepachangelog/_versioning.py
+++ b/keepachangelog/_versioning.py
@@ -11,6 +11,13 @@
}
+class InvalidSemanticVersion(Exception):
+ def __init__(self, version: str):
+ super().__init__(
+ f"{version} is not following semantic versioning. Check https://semver.org for more information."
+ )
+
+
def contains_breaking_changes(unreleased: dict) -> bool:
return "removed" in unreleased or "changed" in unreleased
@@ -106,9 +113,7 @@ def to_semantic(version: Optional[str]) -> dict:
for key, value in match.groupdict().items()
}
- raise Exception(
- f"{version} is not following semantic versioning. Check https://semver.org for more information."
- )
+ raise InvalidSemanticVersion(version)
def from_semantic(semantic_version: dict) -> str:
diff --git a/tests/test_changelog.py b/tests/test_changelog.py
index 128472e..59159fe 100644
--- a/tests/test_changelog.py
+++ b/tests/test_changelog.py
@@ -92,6 +92,13 @@ def test_changelog_with_versions_and_all_categories(changelog):
"removed": ["Deprecated feature 2", "Future removal 1"],
"security": ["Known issue 1", "Known issue 2"],
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"changed": [
@@ -102,6 +109,13 @@ def test_changelog_with_versions_and_all_categories(changelog):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
@@ -113,12 +127,26 @@ def test_changelog_with_versions_and_all_categories(changelog):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
@@ -152,6 +180,13 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "2018-06-01",
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"raw": """### Changed
@@ -162,6 +197,13 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
@@ -173,6 +215,13 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
@@ -182,6 +231,13 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
diff --git a/tests/test_changelog_no_added.py b/tests/test_changelog_no_added.py
index 05726ac..32d6d03 100644
--- a/tests/test_changelog_no_added.py
+++ b/tests/test_changelog_no_added.py
@@ -75,6 +75,13 @@ def test_changelog_with_versions_and_no_added(changelog):
"removed": ["Deprecated feature 2", "Future removal 1"],
"security": ["Known issue 1", "Known issue 2"],
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"changed": [
@@ -85,6 +92,13 @@ def test_changelog_with_versions_and_no_added(changelog):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.0.1": {
"fixed": [
@@ -95,10 +109,24 @@ def test_changelog_with_versions_and_no_added(changelog):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
},
}
diff --git a/tests/test_changelog_no_changed.py b/tests/test_changelog_no_changed.py
index 2a77605..242363f 100644
--- a/tests/test_changelog_no_changed.py
+++ b/tests/test_changelog_no_changed.py
@@ -77,8 +77,25 @@ def test_changelog_with_versions_and_no_changed(changelog):
"removed": ["Deprecated feature 2", "Future removal 1"],
"security": ["Known issue 1", "Known issue 2"],
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
+ },
+ "1.1.0": {
+ "release_date": "2018-05-31",
+ "version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
},
- "1.1.0": {"release_date": "2018-05-31", "version": "1.1.0"},
"1.0.1": {
"fixed": [
"Bug fix 1 (1.0.1)",
@@ -88,10 +105,24 @@ def test_changelog_with_versions_and_no_changed(changelog):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
},
}
diff --git a/tests/test_changelog_no_deprecated.py b/tests/test_changelog_no_deprecated.py
index fb1f27e..cc70e0a 100644
--- a/tests/test_changelog_no_deprecated.py
+++ b/tests/test_changelog_no_deprecated.py
@@ -79,6 +79,13 @@ def test_changelog_with_versions_and_no_deprecated(changelog):
"removed": ["Deprecated feature 2", "Future removal 1"],
"security": ["Known issue 1", "Known issue 2"],
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"changed": [
@@ -89,6 +96,13 @@ def test_changelog_with_versions_and_no_deprecated(changelog):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.0.1": {
"fixed": [
@@ -99,6 +113,23 @@ def test_changelog_with_versions_and_no_deprecated(changelog):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
+ },
+ "1.0.0": {
+ "release_date": "2017-04-10",
+ "version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
},
- "1.0.0": {"release_date": "2017-04-10", "version": "1.0.0"},
}
diff --git a/tests/test_changelog_no_fixed.py b/tests/test_changelog_no_fixed.py
index cae056e..ecde986 100644
--- a/tests/test_changelog_no_fixed.py
+++ b/tests/test_changelog_no_fixed.py
@@ -80,6 +80,13 @@ def test_changelog_with_versions_and_no_fixed(changelog):
"removed": ["Deprecated feature 2", "Future removal 1"],
"security": ["Known issue 1", "Known issue 2"],
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"changed": [
@@ -90,6 +97,13 @@ def test_changelog_with_versions_and_no_fixed(changelog):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.0.1": {
"fixed": [
@@ -100,10 +114,24 @@ def test_changelog_with_versions_and_no_fixed(changelog):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
},
}
diff --git a/tests/test_changelog_no_removed.py b/tests/test_changelog_no_removed.py
index dc97af2..18de621 100644
--- a/tests/test_changelog_no_removed.py
+++ b/tests/test_changelog_no_removed.py
@@ -82,6 +82,13 @@ def test_changelog_with_versions_and_no_removed(changelog):
"release_date": "2018-06-01",
"security": ["Known issue 1", "Known issue 2"],
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"changed": [
@@ -92,6 +99,13 @@ def test_changelog_with_versions_and_no_removed(changelog):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.0.1": {
"fixed": [
@@ -102,10 +116,24 @@ def test_changelog_with_versions_and_no_removed(changelog):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
},
}
diff --git a/tests/test_changelog_no_security.py b/tests/test_changelog_no_security.py
index 8724509..1ddd0e8 100644
--- a/tests/test_changelog_no_security.py
+++ b/tests/test_changelog_no_security.py
@@ -82,6 +82,13 @@ def test_changelog_with_versions_and_no_security(changelog):
"release_date": "2018-06-01",
"removed": ["Deprecated feature 2", "Future removal 1"],
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"changed": [
@@ -92,6 +99,13 @@ def test_changelog_with_versions_and_no_security(changelog):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.0.1": {
"fixed": [
@@ -102,10 +116,24 @@ def test_changelog_with_versions_and_no_security(changelog):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
},
}
diff --git a/tests/test_changelog_unreleased.py b/tests/test_changelog_unreleased.py
index 7280821..11fcdc1 100644
--- a/tests/test_changelog_unreleased.py
+++ b/tests/test_changelog_unreleased.py
@@ -94,6 +94,13 @@ def test_changelog_with_versions_and_all_categories(changelog):
},
"1.1.0": {
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
"release_date": "2018-05-31",
"changed": [
"Enhancement 1 (1.1.0)",
@@ -105,6 +112,13 @@ def test_changelog_with_versions_and_all_categories(changelog):
},
"1.0.1": {
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
"release_date": "2018-05-31",
"fixed": [
"Bug fix 1 (1.0.1)",
@@ -116,6 +130,13 @@ def test_changelog_with_versions_and_all_categories(changelog):
},
"1.0.0": {
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
"release_date": "2017-04-10",
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"url": "https://github.test_url/test_project/releases/tag/v1.0.0",
diff --git a/tests/test_flask_restx.py b/tests/test_flask_restx.py
index c201a1a..1a042fb 100644
--- a/tests/test_flask_restx.py
+++ b/tests/test_flask_restx.py
@@ -87,6 +87,13 @@ def test_changelog_endpoint_with_file(tmpdir):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
@@ -98,12 +105,26 @@ def test_changelog_endpoint_with_file(tmpdir):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
diff --git a/tests/test_non_standard_changelog.py b/tests/test_non_standard_changelog.py
index d5c9b90..27272d7 100644
--- a/tests/test_non_standard_changelog.py
+++ b/tests/test_non_standard_changelog.py
@@ -87,6 +87,13 @@ def test_changelog_with_versions_and_all_categories(changelog):
"removed": ["Deprecated feature 2", "Future removal 1"],
"security": ["Known issue 1", "Known issue 2"],
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"changed": [
@@ -97,6 +104,13 @@ def test_changelog_with_versions_and_all_categories(changelog):
],
"release_date": "may 03, 2018",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.0.1": {
"fixed": [
@@ -107,11 +121,25 @@ def test_changelog_with_versions_and_all_categories(changelog):
],
"release_date": "may 01, 2018",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-01-01",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
},
}
@@ -133,6 +161,13 @@ def test_changelog_with_unreleased_versions_and_all_categories(changelog):
"removed": ["Deprecated feature 2", "Future removal 1"],
"security": ["Known issue 1", "Known issue 2"],
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"changed": [
@@ -143,6 +178,13 @@ def test_changelog_with_unreleased_versions_and_all_categories(changelog):
],
"release_date": "may 03, 2018",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.0.1": {
"fixed": [
@@ -153,11 +195,25 @@ def test_changelog_with_unreleased_versions_and_all_categories(changelog):
],
"release_date": "may 01, 2018",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-01-01",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
},
}
@@ -190,6 +246,13 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "august 28, 2019",
"version": "1.2.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 2,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.1.0": {
"raw": """### Changed
@@ -200,6 +263,13 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "may 03, 2018",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
},
"1.0.1": {
"raw": """### Fixed
@@ -210,6 +280,13 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "may 01, 2018",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
},
"1.0.0": {
"raw": """### Deprecated
@@ -218,5 +295,12 @@ def test_raw_changelog_with_versions_and_all_categories(changelog):
""",
"release_date": "2017-01-01",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
},
}
diff --git a/tests/test_starlette.py b/tests/test_starlette.py
index ef5777e..7641aa9 100644
--- a/tests/test_starlette.py
+++ b/tests/test_starlette.py
@@ -86,6 +86,13 @@ def test_changelog_endpoint_with_file(tmpdir):
],
"release_date": "2018-05-31",
"version": "1.1.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
},
"1.0.1": {
@@ -97,12 +104,26 @@ def test_changelog_endpoint_with_file(tmpdir):
],
"release_date": "2018-05-31",
"version": "1.0.1",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
},
"1.0.0": {
"deprecated": ["Known issue 1 (1.0.0)", "Known issue 2 (1.0.0)"],
"release_date": "2017-04-10",
"version": "1.0.0",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
"url": "https://github.test_url/test_project/releases/tag/v1.0.0",
},
}
From a6f4d1487fd8463351d03818ab2fb5539a8047de Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 15:56:45 +0200
Subject: [PATCH 05/10] Split version guessing and retrieval
---
README.md | 2 +-
keepachangelog/_changelog.py | 4 +++-
keepachangelog/_versioning.py | 5 ++---
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 4cee8bf..1f96a56 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index 14c9103..33cfdd6 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -3,6 +3,7 @@
from typing import Dict, List, Optional
from keepachangelog._versioning import (
+ actual_version,
guess_unreleased_version,
to_semantic,
InvalidSemanticVersion,
@@ -120,7 +121,8 @@ def to_raw_dict(changelog_path: str) -> Dict[str, dict]:
def release(changelog_path: str) -> str:
changelog = to_dict(changelog_path, show_unreleased=True)
- current_version, new_version = guess_unreleased_version(changelog)
+ current_version, current_semantic_version = actual_version(changelog)
+ new_version = guess_unreleased_version(changelog, current_semantic_version)
release_version(changelog_path, current_version, new_version)
return new_version
diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py
index 5a8d670..216e426 100644
--- a/keepachangelog/_versioning.py
+++ b/keepachangelog/_versioning.py
@@ -81,7 +81,7 @@ def actual_version(changelog: dict) -> Tuple[Optional[str], dict]:
return versions.pop() if versions else (None, initial_semantic_version.copy())
-def guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]:
+def guess_unreleased_version(changelog: dict, current_semantic_version: dict) -> str:
unreleased = changelog.get("unreleased", {})
# Only keep user provided entries
unreleased = unreleased.copy()
@@ -93,8 +93,7 @@ def guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]:
"Release content must be provided within changelog Unreleased section."
)
- version, semantic_version = actual_version(changelog)
- return version, from_semantic(bump(unreleased, semantic_version))
+ return from_semantic(bump(unreleased, current_semantic_version))
semantic_versioning = re.compile(
From fda564ac8acdcb27cec80637d339b3baaf79cc09 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 16:01:23 +0200
Subject: [PATCH 06/10] Allow to provide a custom release version
---
CHANGELOG.md | 3 +++
README.md | 2 +-
keepachangelog/_changelog.py | 5 +++--
tests/test_changelog_release.py | 29 +++++++++++++++++++++++++++++
4 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ff7364..9d4d20c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `keepachangelog.to_dict` now contains `semantic_version` key for each item if the version follows semantic versioning.
- `keepachangelog.to_raw_dict` now contains `semantic_version` key for each item if the version follows semantic versioning.
+### Added
+- `keepachangelog.release` is now allowing to provide a custom new version thanks to the new `new_version` parameter.
+
### Fixed
- `keepachangelog.release` now allows `pre-release` and `build metadata` information as part of valid semantic version. As per [semantic versioning specifications](https://semver.org).
To ensure compatibility with some python specific versioning, `pre-release` is also handled as not being prefixed with `-`, or prefixed with `.`.
diff --git a/README.md b/README.md
index 1f96a56..ed9c0ba 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index 33cfdd6..ce729e3 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -119,10 +119,11 @@ def to_raw_dict(changelog_path: str) -> Dict[str, dict]:
return changes
-def release(changelog_path: str) -> str:
+def release(changelog_path: str, new_version: str = None) -> str:
changelog = to_dict(changelog_path, show_unreleased=True)
current_version, current_semantic_version = actual_version(changelog)
- new_version = guess_unreleased_version(changelog, current_semantic_version)
+ if not new_version:
+ new_version = guess_unreleased_version(changelog, current_semantic_version)
release_version(changelog_path, current_version, new_version)
return new_version
diff --git a/tests/test_changelog_release.py b/tests/test_changelog_release.py
index df9308d..af7a48d 100644
--- a/tests/test_changelog_release.py
+++ b/tests/test_changelog_release.py
@@ -777,3 +777,32 @@ def test_first_stable_release(unstable_changelog, mock_date):
- Enhancement 2 (1.1.0)
"""
)
+
+
+def test_custom_release(unstable_changelog, mock_date):
+ assert (
+ keepachangelog.release(unstable_changelog, new_version="2.5.0b52") == "2.5.0b52"
+ )
+ with open(unstable_changelog) as file:
+ assert (
+ file.read()
+ == """# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [2.5.0b52] - 2021-03-19
+### Changed
+- Enhancement 1 (1.1.0)
+
+## [2.5.0b51] - 2018-05-31
+### Changed
+- Enhancement 1 (1.1.0)
+- sub *enhancement 1*
+- sub enhancement 2
+- Enhancement 2 (1.1.0)
+"""
+ )
From 95e9c522b2b30a4e0e0396664c1196329c85d453 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 16:05:37 +0200
Subject: [PATCH 07/10] Release version 1 today
---
CHANGELOG.md | 5 ++++-
keepachangelog/version.py | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d4d20c..a33ab6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+
+## [1.0.0] - 2021-05-21
### Changed
- `keepachangelog.to_dict` now contains `url` key for each item if a link is available for the version.
- `keepachangelog.to_raw_dict` now contains `url` key for each item if a link is available for the version.
@@ -55,7 +57,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Initial release.
-[Unreleased]: https://github.com/Colin-b/keepachangelog/compare/v0.5.0...HEAD
+[Unreleased]: https://github.com/Colin-b/keepachangelog/compare/v1.0.0...HEAD
+[1.0.0]: https://github.com/Colin-b/keepachangelog/compare/v0.5.0...v1.0.0
[0.5.0]: https://github.com/Colin-b/keepachangelog/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/Colin-b/keepachangelog/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/Colin-b/keepachangelog/compare/v0.3.0...v0.3.1
diff --git a/keepachangelog/version.py b/keepachangelog/version.py
index c4b9ff7..09fdcc6 100644
--- a/keepachangelog/version.py
+++ b/keepachangelog/version.py
@@ -3,4 +3,4 @@
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
-__version__ = "0.5.0"
+__version__ = "1.0.0"
From cb52402b78528be26e7a572b47fab8cce27dc537 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 16:11:12 +0200
Subject: [PATCH 08/10] Document to_raw_dict and new parameter for release
---
README.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 67 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index ed9c0ba..4ef9b66 100644
--- a/README.md
+++ b/README.md
@@ -147,6 +147,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`show_unreleased` parameter can be specified in order to include `Unreleased` section information.
Note that `release_date` will be set to None in such as case.
+### Retrieving the raw content
+
+If for some reason you would like to retrieve the raw content of a release you can use `to_raw_dict` instead.
+
+```python
+import keepachangelog
+
+changes = keepachangelog.to_raw_dict("path/to/CHANGELOG.md")
+```
+
+`changes` would look like:
+
+```python
+changes = {
+ "1.1.0": {
+ "raw": """### Changed
+- Enhancement 1 (1.1.0)
+ - sub enhancement 1
+ - sub enhancement 2
+- Enhancement 2 (1.1.0)""",
+ "release_date": "2018-05-31",
+ "version": "1.1.0",
+ "semantic_version": {
+ "major": 1,
+ "minor": 1,
+ "patch": 0,
+ "prerelease": None,
+ "buildmetadata": None,
+ },
+ "url": "https://github.test_url/test_project/compare/v1.0.1...v1.1.0",
+ },
+ "1.0.1": {
+ "raw": """### Fixed
+- Bug fix 1 (1.0.1)
+ - sub bug 1
+ - sub bug 2
+- Bug fix 2 (1.0.1)""",
+ "release_date": "2018-05-31",
+ "version": "1.0.1",
+ "semantic_version": {
+ "major": 1,
+ "minor": 0,
+ "patch": 1,
+ "prerelease": None,
+ "buildmetadata": None,
+ },
+ "url": "https://github.test_url/test_project/compare/v1.0.0...v1.0.1",
+ },
+ "1.0.0": {
+ "raw": """### Deprecated
+- Known issue 1 (1.0.0)
+- Known issue 2 (1.0.0)""",
+ "release_date": "2017-04-10",
+ "version": "1.0.0",
+ "semantic_version": {
+ "major": 1,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ "buildmetadata": None,
+ },
+ "url": "https://github.test_url/test_project/releases/tag/v1.0.0",
+ },
+}
+```
+
## Release
You can create a new release by using `keepachangelog.release` function.
@@ -158,7 +224,7 @@ new_version = keepachangelog.release("path/to/CHANGELOG.md")
```
This will:
-* Guess the new version number and return it:
+* If `new_version` parameter is not provided, guess the new version number and return it:
* `Removed` or `Changed` sections will be considered as breaking changes, thus incrementing the major version.
* If the only section is `Fixed`, only patch will be incremented.
* Otherwise, minor will be incremented.
From cb0ee86cf0d5aa7a0f2281ebf1a604dbec946b68 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 16:18:16 +0200
Subject: [PATCH 09/10] pin flask to version 1 as flask-restx does not
---
setup.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/setup.py b/setup.py
index 13df3fb..3d7ac26 100644
--- a/setup.py
+++ b/setup.py
@@ -42,6 +42,7 @@
"requests",
"starlette==0.13.*",
# Used to check flask-restx endpoint
+ "flask==1.*",
"flask-restx==0.2.*",
# Used to check coverage
"pytest-cov==2.*",
From d1aa49dc3819d660313b7b5e8222953ca76b840f Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 21 May 2021 16:28:17 +0200
Subject: [PATCH 10/10] Ensure empty version is properly handled
---
README.md | 2 +-
tests/test_changelog_empty_version.py | 67 +++++++++++++++++++++++++++
2 files changed, 68 insertions(+), 1 deletion(-)
create mode 100644 tests/test_changelog_empty_version.py
diff --git a/README.md b/README.md
index 4ef9b66..195bb22 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/tests/test_changelog_empty_version.py b/tests/test_changelog_empty_version.py
new file mode 100644
index 0000000..43845c8
--- /dev/null
+++ b/tests/test_changelog_empty_version.py
@@ -0,0 +1,67 @@
+import os
+import os.path
+
+import pytest
+
+import keepachangelog
+
+
+@pytest.fixture
+def changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "CHANGELOG.md")
+ with open(changelog_file_path, "wt") as file:
+ file.write(
+ """# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [] - 2018-06-01
+### Changed
+- Release note 1.
+- Release note 2.
+
+### Fixed
+- Bug fix 1
+- sub bug 1
+- sub bug 2
+- Bug fix 2
+
+### Security
+- Known issue 1
+- Known issue 2
+
+### Deprecated
+- Deprecated feature 1
+- Future removal 2
+
+### Removed
+- Deprecated feature 2
+- Future removal 1
+"""
+ )
+ return changelog_file_path
+
+
+def test_changelog_with_empty_version(changelog):
+ assert keepachangelog.to_dict(changelog) == {
+ "": {
+ "changed": ["Release note 1.", "Release note 2."],
+ "deprecated": ["Deprecated feature 1", "Future removal 2"],
+ "fixed": ["Bug fix 1", "sub bug 1", "sub bug 2", "Bug fix 2"],
+ "release_date": "2018-06-01",
+ "removed": ["Deprecated feature 2", "Future removal 1"],
+ "security": ["Known issue 1", "Known issue 2"],
+ "version": "",
+ "semantic_version": {
+ "buildmetadata": None,
+ "major": 0,
+ "minor": 0,
+ "patch": 0,
+ "prerelease": None,
+ },
+ },
+ }