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