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 @@ Build status Coverage Code style: black -Number of tests +Number of tests Number of downloads

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 @@ Build status Coverage Code style: black -Number of tests +Number of tests Number of downloads

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 @@ Build status Coverage Code style: black -Number of tests +Number of tests Number of downloads

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, + }, + }, + }