diff --git a/CHANGELOG.md b/CHANGELOG.md index 56bfe58..a33ab6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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. +- `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 `.`. +- `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. @@ -42,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/README.md b/README.md index 000265f..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

@@ -38,6 +38,14 @@ 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": { "fixed": [ @@ -48,11 +56,27 @@ 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", }, } ``` @@ -123,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. @@ -134,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. diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py index aa699f7..ce729e3 100644 --- a/keepachangelog/_changelog.py +++ b/keepachangelog/_changelog.py @@ -2,7 +2,12 @@ import re from typing import Dict, List, Optional -from keepachangelog._versioning import guess_unreleased_version +from keepachangelog._versioning import ( + actual_version, + guess_unreleased_version, + to_semantic, + InvalidSemanticVersion, +) def is_release(line: str) -> bool: @@ -21,10 +26,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: @@ -51,8 +60,8 @@ def add_category(release: dict, line: str) -> List[str]: link_pattern = re.compile(r"^\[(.*)\]: (.*)$") -def is_information(line: str) -> bool: - return line and not link_pattern.fullmatch(line) +def is_link(line: str) -> bool: + return link_pattern.fullmatch(line) is not None def add_information(category: List[str], line: str): @@ -61,6 +70,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,14 +82,22 @@ 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_information(line): + elif is_link(line): + link_match = link_pattern.fullmatch(line) + urls[link_match.group(1).lower()] = link_match.group(2) + elif line: add_information(category, line) + for version, url in urls.items(): + changes.get(version, {})["url"] = url + return changes 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: @@ -88,15 +107,23 @@ 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 -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, new_version = guess_unreleased_version(changelog) + current_version, current_semantic_version = actual_version(changelog) + 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/keepachangelog/_versioning.py b/keepachangelog/_versioning.py index be70fea..216e426 100644 --- a/keepachangelog/_versioning.py +++ b/keepachangelog/_versioning.py @@ -2,71 +2,118 @@ from typing import Tuple, Optional +initial_semantic_version = { + "major": 0, + "minor": 0, + "patch": 0, + "prerelease": None, + "buildmetadata": None, +} + + +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 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(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_major(version: str) -> str: - major, *_ = to_semantic(version) - return from_semantic(major + 1, 0, 0) +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]: +def guess_unreleased_version(changelog: dict, current_semantic_version: dict) -> 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." ) - version = actual_version(changelog) - return version, bump(unreleased, version) + return from_semantic(bump(unreleased, current_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 InvalidSemanticVersion(version) -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/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" 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.*", diff --git a/tests/test_changelog.py b/tests/test_changelog.py index eaa991e..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,14 @@ 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": { "fixed": [ @@ -112,11 +127,27 @@ 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", }, } @@ -149,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 @@ -159,6 +197,14 @@ 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": { "raw": """### Fixed @@ -169,6 +215,14 @@ 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": { "raw": """### Deprecated @@ -177,5 +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_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, + }, + }, + } 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_release.py b/tests/test_changelog_release.py index 95e63eb..af7a48d 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,63 @@ 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) +""" + ) + + +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) +""" + ) diff --git a/tests/test_changelog_unreleased.py b/tests/test_changelog_unreleased.py index 1495ebe..11fcdc1 100644 --- a/tests/test_changelog_unreleased.py +++ b/tests/test_changelog_unreleased.py @@ -90,9 +90,17 @@ 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", + "semantic_version": { + "buildmetadata": None, + "major": 1, + "minor": 1, + "patch": 0, + "prerelease": None, + }, "release_date": "2018-05-31", "changed": [ "Enhancement 1 (1.1.0)", @@ -100,9 +108,17 @@ 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", + "semantic_version": { + "buildmetadata": None, + "major": 1, + "minor": 0, + "patch": 1, + "prerelease": None, + }, "release_date": "2018-05-31", "fixed": [ "Bug fix 1 (1.0.1)", @@ -110,10 +126,19 @@ 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", + "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 7c850a9..1a042fb 100644 --- a/tests/test_flask_restx.py +++ b/tests/test_flask_restx.py @@ -87,6 +87,14 @@ 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": { "fixed": [ @@ -97,11 +105,27 @@ 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 f6f21b6..7641aa9 100644 --- a/tests/test_starlette.py +++ b/tests/test_starlette.py @@ -86,6 +86,14 @@ 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": { "fixed": [ @@ -96,11 +104,27 @@ 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", }, }