From 563bde1b604af93ced8c2981ab9283862c5e04f4 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Thu, 18 Mar 2021 19:12:12 +0100
Subject: [PATCH 01/11] Allow to guess upcoming version
---
.pre-commit-config.yaml | 4 +-
CHANGELOG.md | 2 +
keepachangelog/__init__.py | 2 +-
keepachangelog/_changelog.py | 9 +
keepachangelog/_versioning.py | 68 ++++++
tests/test_changelog_release.py | 369 ++++++++++++++++++++++++++++++++
6 files changed, 451 insertions(+), 3 deletions(-)
create mode 100644 keepachangelog/_versioning.py
create mode 100644 tests/test_changelog_release.py
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9c42b63..ee3d6a7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/psf/black
- rev: master
+ rev: 20.8b1
hooks:
- - id: black
\ No newline at end of file
+ - id: black
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2feb7af..feaeb66 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]
+### Added
+- `keepachangelog.release` function to guess new version number based on `Unreleased` section.
## [0.4.0] - 2020-09-21
### Added
diff --git a/keepachangelog/__init__.py b/keepachangelog/__init__.py
index 1ee1b48..ac00948 100644
--- a/keepachangelog/__init__.py
+++ b/keepachangelog/__init__.py
@@ -1,2 +1,2 @@
from keepachangelog.version import __version__
-from keepachangelog._changelog import to_dict
+from keepachangelog._changelog import to_dict, release
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index 4629bd2..f1712b7 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -1,6 +1,8 @@
import re
from typing import Dict, List
+from keepachangelog._versioning import guess_unreleased_version
+
# Release pattern should match lines like: "## [0.0.1] - 2020-12-31" or ## [Unreleased]
release_pattern = re.compile(r"^## \[(.*)\](?: - (.*))?$")
@@ -66,3 +68,10 @@ def to_dict(changelog_path: str, *, show_unreleased: bool = False) -> Dict[str,
add_information(category, line)
return changes
+
+
+def release(changelog_path: str) -> str:
+ changelog = to_dict(changelog_path, show_unreleased=True)
+ current_version, new_version = guess_unreleased_version(changelog)
+
+ return new_version
diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py
new file mode 100644
index 0000000..eaced3e
--- /dev/null
+++ b/keepachangelog/_versioning.py
@@ -0,0 +1,68 @@
+import re
+from typing import Tuple, Optional
+
+
+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
+
+
+def bump_major(version: str) -> str:
+ major, *_ = to_semantic(version)
+ return from_semantic(major + 1, 0, 0)
+
+
+def bump_minor(version: str) -> str:
+ major, minor, _ = to_semantic(version)
+ return from_semantic(major, minor + 1, 0)
+
+
+def bump_patch(version: str) -> str:
+ major, minor, patch = to_semantic(version)
+ return from_semantic(major, minor, patch + 1)
+
+
+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 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 guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]:
+ unreleased = changelog.get("Unreleased", {})
+ if not unreleased:
+ raise Exception(
+ "Unable to guess unreleased version because there is not Unreleased section within changelog."
+ )
+
+ version = actual_version(changelog)
+ return version, bump(unreleased, version)
+
+
+def to_semantic(version: Optional[str]) -> Tuple[int, int, int]:
+ if not version:
+ return 0, 0, 0
+
+ match = re.search(r"(\d+)\.(\d+)\.(\d+)", version)
+ if match:
+ return int(match.group(1)), int(match.group(2)), int(match.group(3))
+
+ raise Exception(f"{version} is not following semantic versioning.")
+
+
+def from_semantic(major: int, minor: int, patch: int) -> str:
+ return f"{major}.{minor}.{patch}"
diff --git a/tests/test_changelog_release.py b/tests/test_changelog_release.py
new file mode 100644
index 0000000..ee9614c
--- /dev/null
+++ b/tests/test_changelog_release.py
@@ -0,0 +1,369 @@
+import os
+import os.path
+
+import pytest
+
+import keepachangelog
+
+
+@pytest.fixture
+def major_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "MAJOR_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
+- Release note 1.
+* Release note 2.
+
+### Added
+- Enhancement 1
+ - sub enhancement 1
+ * sub enhancement 2
+- Enhancement 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
+
+## [1.1.0] - 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)
+- 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)
+- Known issue 2 (1.0.0)
+
+[Unreleased]: https://github.test_url/test_project/compare/v1.1.0...HEAD
+[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0
+[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1
+[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0
+"""
+ )
+ return changelog_file_path
+
+
+@pytest.fixture
+def minor_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "MINOR_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]
+### Added
+- Enhancement 1
+ - sub enhancement 1
+ * sub enhancement 2
+- Enhancement 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
+
+## [1.1.0] - 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)
+- 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)
+- Known issue 2 (1.0.0)
+
+[Unreleased]: https://github.test_url/test_project/compare/v1.1.0...HEAD
+[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0
+[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1
+[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0
+"""
+ )
+ return changelog_file_path
+
+
+@pytest.fixture
+def patch_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "PATCH_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]
+### Fixed
+- Bug fix 1
+ - sub bug 1
+ * sub bug 2
+- Bug fix 2
+
+## [1.1.0] - 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)
+- 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)
+- Known issue 2 (1.0.0)
+
+[Unreleased]: https://github.test_url/test_project/compare/v1.1.0...HEAD
+[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0
+[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1
+[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0
+"""
+ )
+ return changelog_file_path
+
+
+@pytest.fixture
+def first_major_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "FIRST_MAJOR_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
+- Release note 1.
+* Release note 2.
+
+### Added
+- Enhancement 1
+ - sub enhancement 1
+ * sub enhancement 2
+- Enhancement 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
+
+[Unreleased]: https://github.test_url/test_project
+"""
+ )
+ return changelog_file_path
+
+
+@pytest.fixture
+def first_minor_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "FIRST_MINOR_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]
+### Added
+- Enhancement 1
+ - sub enhancement 1
+ * sub enhancement 2
+- Enhancement 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
+
+[Unreleased]: https://github.test_url/test_project
+"""
+ )
+ return changelog_file_path
+
+
+@pytest.fixture
+def first_patch_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "FIRST_PATCH_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]
+### Fixed
+- Bug fix 1
+ - sub bug 1
+ * sub bug 2
+- Bug fix 2
+
+[Unreleased]: https://github.test_url/test_project
+"""
+ )
+ return changelog_file_path
+
+
+@pytest.fixture
+def empty_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "EMPTY_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).
+"""
+ )
+ return changelog_file_path
+
+
+@pytest.fixture
+def non_semantic_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "NON_SEMANTIC_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]
+
+## [20180531] - 2018-05-31
+### Changed
+- Enhancement 1 (1.1.0)
+- sub *enhancement 1*
+- sub enhancement 2
+- Enhancement 2 (1.1.0)
+
+[Unreleased]: https://github.test_url/test_project/compare/v20180531...HEAD
+[20180531]: https://github.test_url/test_project/releases/tag/v20180531
+"""
+ )
+ return changelog_file_path
+
+
+def test_major_release(major_changelog):
+ assert keepachangelog.release(major_changelog) == "2.0.0"
+
+
+def test_minor_release(minor_changelog):
+ assert keepachangelog.release(minor_changelog) == "1.2.0"
+
+
+def test_patch_release(patch_changelog):
+ assert keepachangelog.release(patch_changelog) == "1.1.1"
+
+
+def test_first_major_release(first_major_changelog):
+ assert keepachangelog.release(first_major_changelog) == "1.0.0"
+
+
+def test_first_minor_release(first_minor_changelog):
+ assert keepachangelog.release(first_minor_changelog) == "0.1.0"
+
+
+def test_first_patch_release(first_patch_changelog):
+ assert keepachangelog.release(first_patch_changelog) == "0.0.1"
+
+
+def test_empty_release(empty_changelog):
+ with pytest.raises(Exception) as exception_info:
+ keepachangelog.release(empty_changelog)
+ assert (
+ str(exception_info.value)
+ == "Unable to guess unreleased version because there is not Unreleased section within 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."
From 28c3f8aaa3c9d0ab02f6c62d0878e17656ef2425 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Thu, 18 Mar 2021 19:13:02 +0100
Subject: [PATCH 02/11] Add explicit python 3.9 support
---
.travis.yml | 3 ++-
setup.py | 1 +
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index a4fe5a1..64f9fd5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,6 +3,7 @@ python:
- "3.6"
- "3.7"
- "3.8"
+ - "3.9"
install:
- pip install .[testing]
script:
@@ -12,4 +13,4 @@ deploy:
username: __token__
edge: true
distributions: "sdist bdist_wheel"
- skip_existing: true
\ No newline at end of file
+ skip_existing: true
diff --git a/setup.py b/setup.py
index 8931555..4cb654e 100644
--- a/setup.py
+++ b/setup.py
@@ -30,6 +30,7 @@
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
"Topic :: Software Development :: Build Tools",
],
keywords=["changelog", "CHANGELOG.md", "markdown"],
From c219c90182def23813ff67b8eb3ffaf930d1acaf Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 19 Mar 2021 13:33:14 +0100
Subject: [PATCH 03/11] Update changelog upon release
---
keepachangelog/_changelog.py | 46 +++++-
keepachangelog/_versioning.py | 6 +-
tests/test_changelog_release.py | 270 ++++++++++++++++++++++++++++++++
3 files changed, 319 insertions(+), 3 deletions(-)
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index f1712b7..7e02569 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -1,5 +1,6 @@
+import datetime
import re
-from typing import Dict, List
+from typing import Dict, List, Optional
from keepachangelog._versioning import guess_unreleased_version
@@ -73,5 +74,46 @@ def to_dict(changelog_path: str, *, show_unreleased: bool = False) -> Dict[str,
def release(changelog_path: str) -> str:
changelog = to_dict(changelog_path, show_unreleased=True)
current_version, new_version = guess_unreleased_version(changelog)
-
+ release_version(changelog_path, current_version, new_version)
return new_version
+
+
+def release_version(
+ changelog_path: str, current_version: Optional[str], new_version: str
+):
+ unreleased_link_pattern = re.compile(r"^\[Unreleased\]: (.*)$", re.DOTALL)
+ lines = []
+ with open(changelog_path) as change_log:
+ for line in change_log.readlines():
+ # Move Unreleased section to new version
+ if re.fullmatch(r"^## \[Unreleased\].*$", line, re.DOTALL):
+ lines.append(line)
+ lines.append("\n")
+ lines.append(
+ f"## [{new_version}] - {datetime.date.today().isoformat()}\n"
+ )
+ # Add new version link and update Unreleased link
+ elif unreleased_link_pattern.fullmatch(line):
+ unreleased_compare_pattern = re.fullmatch(
+ "^.*/(.*)...HEAD.*$", line, re.DOTALL
+ )
+ # Unreleased link compare previous version to HEAD
+ if unreleased_compare_pattern:
+ new_unreleased_link = line.replace(current_version, new_version)
+ lines.append(new_unreleased_link)
+ current_tag = unreleased_compare_pattern.group(1)
+ new_tag = current_tag.replace(current_version, new_version)
+ lines.append(
+ line.replace(new_version, current_version)
+ .replace("HEAD", new_tag)
+ .replace("Unreleased", new_version)
+ )
+ # Consider that there is no way to know how to create a link to compare versions
+ else:
+ lines.append(line)
+ lines.append(line.replace("Unreleased", new_version))
+ else:
+ lines.append(line)
+
+ with open(changelog_path, "wt") as change_log:
+ change_log.writelines(lines)
diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py
index eaced3e..bb5e096 100644
--- a/keepachangelog/_versioning.py
+++ b/keepachangelog/_versioning.py
@@ -53,11 +53,15 @@ def guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]:
return version, bump(unreleased, version)
+# Semantic versioning pattern should match version like 1.2.3"
+version_pattern = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
+
+
def to_semantic(version: Optional[str]) -> Tuple[int, int, int]:
if not version:
return 0, 0, 0
- match = re.search(r"(\d+)\.(\d+)\.(\d+)", version)
+ match = version_pattern.fullmatch(version)
if match:
return int(match.group(1)), int(match.group(2)), int(match.group(3))
diff --git a/tests/test_changelog_release.py b/tests/test_changelog_release.py
index ee9614c..e5dd93b 100644
--- a/tests/test_changelog_release.py
+++ b/tests/test_changelog_release.py
@@ -332,26 +332,296 @@ def non_semantic_changelog(tmpdir):
def test_major_release(major_changelog):
assert keepachangelog.release(major_changelog) == "2.0.0"
+ with open(major_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.0.0] - 2021-03-19
+### Changed
+- Release note 1.
+* Release note 2.
+
+### Added
+- Enhancement 1
+ - sub enhancement 1
+ * sub enhancement 2
+- Enhancement 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
+
+## [1.1.0] - 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)
+- 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)
+- Known issue 2 (1.0.0)
+
+[Unreleased]: https://github.test_url/test_project/compare/v2.0.0...HEAD
+[2.0.0]: https://github.test_url/test_project/compare/v1.1.0...v2.0.0
+[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0
+[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1
+[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0
+"""
+ )
def test_minor_release(minor_changelog):
assert keepachangelog.release(minor_changelog) == "1.2.0"
+ with open(minor_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]
+
+## [1.2.0] - 2021-03-19
+### Added
+- Enhancement 1
+ - sub enhancement 1
+ * sub enhancement 2
+- Enhancement 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
+
+## [1.1.0] - 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)
+- 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)
+- Known issue 2 (1.0.0)
+
+[Unreleased]: https://github.test_url/test_project/compare/v1.2.0...HEAD
+[1.2.0]: https://github.test_url/test_project/compare/v1.1.0...v1.2.0
+[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0
+[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1
+[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0
+"""
+ )
def test_patch_release(patch_changelog):
assert keepachangelog.release(patch_changelog) == "1.1.1"
+ with open(patch_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]
+
+## [1.1.1] - 2021-03-19
+### Fixed
+- Bug fix 1
+ - sub bug 1
+ * sub bug 2
+- Bug fix 2
+
+## [1.1.0] - 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)
+- 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)
+- Known issue 2 (1.0.0)
+
+[Unreleased]: https://github.test_url/test_project/compare/v1.1.1...HEAD
+[1.1.1]: https://github.test_url/test_project/compare/v1.1.0...v1.1.1
+[1.1.0]: https://github.test_url/test_project/compare/v1.0.1...v1.1.0
+[1.0.1]: https://github.test_url/test_project/compare/v1.0.0...v1.0.1
+[1.0.0]: https://github.test_url/test_project/releases/tag/v1.0.0
+"""
+ )
def test_first_major_release(first_major_changelog):
assert keepachangelog.release(first_major_changelog) == "1.0.0"
+ with open(first_major_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]
+
+## [1.0.0] - 2021-03-19
+### Changed
+- Release note 1.
+* Release note 2.
+
+### Added
+- Enhancement 1
+ - sub enhancement 1
+ * sub enhancement 2
+- Enhancement 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
+
+[Unreleased]: https://github.test_url/test_project
+[1.0.0]: https://github.test_url/test_project
+"""
+ )
def test_first_minor_release(first_minor_changelog):
assert keepachangelog.release(first_minor_changelog) == "0.1.0"
+ with open(first_minor_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]
+
+## [0.1.0] - 2021-03-19
+### Added
+- Enhancement 1
+ - sub enhancement 1
+ * sub enhancement 2
+- Enhancement 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
+
+[Unreleased]: https://github.test_url/test_project
+[0.1.0]: https://github.test_url/test_project
+"""
+ )
def test_first_patch_release(first_patch_changelog):
assert keepachangelog.release(first_patch_changelog) == "0.0.1"
+ with open(first_patch_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]
+
+## [0.0.1] - 2021-03-19
+### Fixed
+- Bug fix 1
+ - sub bug 1
+ * sub bug 2
+- Bug fix 2
+
+[Unreleased]: https://github.test_url/test_project
+[0.0.1]: https://github.test_url/test_project
+"""
+ )
def test_empty_release(empty_changelog):
From c0ab1dd902a931c3f019662ec32d5e23eaf440d0 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 19 Mar 2021 14:15:30 +0100
Subject: [PATCH 04/11] Document release feature
---
CHANGELOG.md | 2 +-
README.md | 22 +++++++++++++++++++---
2 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index feaeb66..8f7393f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
-- `keepachangelog.release` function to guess new version number based on `Unreleased` section.
+- `keepachangelog.release` function to guess new version number based on `Unreleased` section, update changelog and return new version number.
## [0.4.0] - 2020-09-21
### Added
diff --git a/README.md b/README.md
index 571ea24..7776ea9 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,10 @@
-
-
+
+
-
+
@@ -115,6 +115,22 @@ 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.
+## Release
+
+You can create a new release by using `keepachangelog.release` function.
+
+```python
+import keepachangelog
+
+new_version = keepachangelog.release("path/to/CHANGELOG.md")
+```
+
+This will:
+* Guess the new version number and return it:
+ * TODO explain how version is guessed.
+* Update changelog.
+ * TODO explain what is performed.
+
## Endpoint
### Starlette
From 378ae064fbbb5bdc4a5aa980719e0605b5e77f0a Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Fri, 19 Mar 2021 16:26:00 +0100
Subject: [PATCH 05/11] Document release feature
---
README.md | 22 +++++++++++++++++-----
setup.py | 2 +-
2 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 7776ea9..b0432c3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-Convert changelog into dict
+Manipulate keep a changelog files
@@ -9,7 +9,15 @@
-Convert changelog markdown file following [keep a changelog](https://keepachangelog.com/en/1.0.0/) format into python dict.
+* [Convert to dict](#convert-changelog-to-dict)
+* [Release a new version](#release)
+* [Add changelog retrieval REST API endpoint](#endpoint)
+ * [Starlette](#starlette)
+ * [Flask-RestX](#flask-restx)
+
+## Convert changelog to dict
+
+Convert changelog markdown file following [keep a changelog](https://keepachangelog.com/en/1.1.0/) format into python dict.
```python
import keepachangelog
@@ -55,7 +63,7 @@ For a markdown file with the following content:
# 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/),
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
@@ -127,9 +135,13 @@ new_version = keepachangelog.release("path/to/CHANGELOG.md")
This will:
* Guess the new version number and return it:
- * TODO explain how version is guessed.
+ * `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.
* Update changelog.
- * TODO explain what is performed.
+ * Unreleased section content will be moved into a new section.
+ * `[Unreleased]` link will be updated.
+ * New link will be created corresponding to the new section (based on the format of the Unreleased link).
## Endpoint
diff --git a/setup.py b/setup.py
index 4cb654e..13df3fb 100644
--- a/setup.py
+++ b/setup.py
@@ -15,7 +15,7 @@
maintainer="Colin Bounouar",
maintainer_email="colin.bounouar.dev@gmail.com",
url="https://colin-b.github.io/keepachangelog/",
- description="Convert changelog into dict.",
+ description="Manipulate keep a changelog files.",
long_description=long_description,
long_description_content_type="text/markdown",
download_url="https://pypi.org/project/keepachangelog/",
From 97825ad667609bd84814d7ba1ecfcf4c7f12c492 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Tue, 23 Mar 2021 16:36:01 +0100
Subject: [PATCH 06/11] Handle unreleased URL not comparing to HEAD
---
keepachangelog/_changelog.py | 7 ++--
keepachangelog/_versioning.py | 4 +-
tests/test_changelog_release.py | 70 +++++++++++++++++++++++++++++----
3 files changed, 69 insertions(+), 12 deletions(-)
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index 7e02569..69b7e3d 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -95,17 +95,18 @@ def release_version(
# Add new version link and update Unreleased link
elif unreleased_link_pattern.fullmatch(line):
unreleased_compare_pattern = re.fullmatch(
- "^.*/(.*)...HEAD.*$", line, re.DOTALL
+ r"^.*/(.*)\.\.\.(\w*).*$", line, re.DOTALL
)
- # Unreleased link compare previous version to HEAD
+ # Unreleased link compare previous version to HEAD (unreleased tag)
if unreleased_compare_pattern:
new_unreleased_link = line.replace(current_version, new_version)
lines.append(new_unreleased_link)
current_tag = unreleased_compare_pattern.group(1)
+ unreleased_tag = unreleased_compare_pattern.group(2)
new_tag = current_tag.replace(current_version, new_version)
lines.append(
line.replace(new_version, current_version)
- .replace("HEAD", new_tag)
+ .replace(unreleased_tag, new_tag)
.replace("Unreleased", new_version)
)
# Consider that there is no way to know how to create a link to compare versions
diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py
index bb5e096..7645a14 100644
--- a/keepachangelog/_versioning.py
+++ b/keepachangelog/_versioning.py
@@ -44,9 +44,9 @@ def actual_version(changelog: dict) -> Optional[str]:
def guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]:
unreleased = changelog.get("Unreleased", {})
- if not unreleased:
+ if not unreleased or len(unreleased) < 3:
raise Exception(
- "Unable to guess unreleased version because there is not Unreleased section within changelog."
+ "Release content must be provided within changelog Unreleased section."
)
version = actual_version(changelog)
diff --git a/tests/test_changelog_release.py b/tests/test_changelog_release.py
index e5dd93b..95e63eb 100644
--- a/tests/test_changelog_release.py
+++ b/tests/test_changelog_release.py
@@ -1,9 +1,35 @@
+import datetime
import os
import os.path
import pytest
import keepachangelog
+import keepachangelog._changelog
+
+_date_time_for_tests = datetime.datetime(2021, 3, 19, 15, 5, 5, 663979)
+
+
+class DateTimeModuleMock:
+ class DateTimeMock(datetime.datetime):
+ @classmethod
+ def now(cls, tz=None):
+ return _date_time_for_tests.replace(tzinfo=tz)
+
+ class DateMock(datetime.date):
+ @classmethod
+ def today(cls):
+ return _date_time_for_tests.date()
+
+ timedelta = datetime.timedelta
+ timezone = datetime.timezone
+ datetime = DateTimeMock
+ date = DateMock
+
+
+@pytest.fixture
+def mock_date(monkeypatch):
+ monkeypatch.setattr(keepachangelog._changelog, "datetime", DateTimeModuleMock)
@pytest.fixture
@@ -288,6 +314,25 @@ def first_patch_changelog(tmpdir):
return changelog_file_path
+@pytest.fixture
+def empty_unreleased_changelog(tmpdir):
+ changelog_file_path = os.path.join(tmpdir, "EMPTY_UNRELEASED_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]
+
+[Unreleased]: https://github.test_url/test_project
+"""
+ )
+ return changelog_file_path
+
+
@pytest.fixture
def empty_changelog(tmpdir):
changelog_file_path = os.path.join(tmpdir, "EMPTY_CHANGELOG.md")
@@ -315,6 +360,8 @@ def non_semantic_changelog(tmpdir):
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Changed
+- Enhancement 1 (1.1.0)
## [20180531] - 2018-05-31
### Changed
@@ -330,7 +377,7 @@ def non_semantic_changelog(tmpdir):
return changelog_file_path
-def test_major_release(major_changelog):
+def test_major_release(major_changelog, mock_date):
assert keepachangelog.release(major_changelog) == "2.0.0"
with open(major_changelog) as file:
assert (
@@ -400,7 +447,7 @@ def test_major_release(major_changelog):
)
-def test_minor_release(minor_changelog):
+def test_minor_release(minor_changelog, mock_date):
assert keepachangelog.release(minor_changelog) == "1.2.0"
with open(minor_changelog) as file:
assert (
@@ -462,7 +509,7 @@ def test_minor_release(minor_changelog):
)
-def test_patch_release(patch_changelog):
+def test_patch_release(patch_changelog, mock_date):
assert keepachangelog.release(patch_changelog) == "1.1.1"
with open(patch_changelog) as file:
assert (
@@ -510,7 +557,7 @@ def test_patch_release(patch_changelog):
)
-def test_first_major_release(first_major_changelog):
+def test_first_major_release(first_major_changelog, mock_date):
assert keepachangelog.release(first_major_changelog) == "1.0.0"
with open(first_major_changelog) as file:
assert (
@@ -558,7 +605,7 @@ def test_first_major_release(first_major_changelog):
)
-def test_first_minor_release(first_minor_changelog):
+def test_first_minor_release(first_minor_changelog, mock_date):
assert keepachangelog.release(first_minor_changelog) == "0.1.0"
with open(first_minor_changelog) as file:
assert (
@@ -598,7 +645,7 @@ def test_first_minor_release(first_minor_changelog):
)
-def test_first_patch_release(first_patch_changelog):
+def test_first_patch_release(first_patch_changelog, mock_date):
assert keepachangelog.release(first_patch_changelog) == "0.0.1"
with open(first_patch_changelog) as file:
assert (
@@ -629,7 +676,16 @@ def test_empty_release(empty_changelog):
keepachangelog.release(empty_changelog)
assert (
str(exception_info.value)
- == "Unable to guess unreleased version because there is not Unreleased section within changelog."
+ == "Release content must be provided within changelog Unreleased section."
+ )
+
+
+def test_empty_unreleased_release(empty_unreleased_changelog):
+ with pytest.raises(Exception) as exception_info:
+ keepachangelog.release(empty_unreleased_changelog)
+ assert (
+ str(exception_info.value)
+ == "Release content must be provided within changelog Unreleased section."
)
From aa01d110a94a317854f0cf1c758875aeeca0cf7a Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Thu, 15 Apr 2021 17:52:01 +0200
Subject: [PATCH 07/11] Add to_raw_dict function
---
CHANGELOG.md | 1 +
keepachangelog/__init__.py | 2 +-
keepachangelog/_changelog.py | 21 +++++++++++--
tests/test_changelog.py | 60 ++++++++++++++++++++++++++++++++++++
4 files changed, 80 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f7393f..d5aa2c7 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]
### Added
- `keepachangelog.release` function to guess new version number based on `Unreleased` section, update changelog and return new version number.
+- `keepachangelog.to_raw_dict` function returning a raw markdown description of the release under `raw` dict.
## [0.4.0] - 2020-09-21
### Added
diff --git a/keepachangelog/__init__.py b/keepachangelog/__init__.py
index ac00948..31e7d59 100644
--- a/keepachangelog/__init__.py
+++ b/keepachangelog/__init__.py
@@ -1,2 +1,2 @@
from keepachangelog.version import __version__
-from keepachangelog._changelog import to_dict, release
+from keepachangelog._changelog import to_dict, to_raw_dict, release
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index 69b7e3d..e877ff5 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -56,21 +56,36 @@ def add_information(category: List[str], line: str):
def to_dict(changelog_path: str, *, show_unreleased: bool = False) -> Dict[str, dict]:
changes = {}
with open(changelog_path) as change_log:
- release = {}
+ current_release = {}
category = []
for line in change_log:
line = line.strip(" \n")
if is_release(line, show_unreleased):
- release = add_release(changes, line)
+ current_release = add_release(changes, line)
elif is_category(line):
- category = add_category(release, line)
+ category = add_category(current_release, line)
elif is_information(line):
add_information(category, line)
return changes
+def to_raw_dict(changelog_path: str) -> Dict[str, dict]:
+ changes = {}
+ with open(changelog_path) as change_log:
+ current_release = {}
+ for line in change_log:
+ clean_line = line.strip(" \n")
+
+ if is_release(clean_line, show_unreleased=False):
+ current_release = add_release(changes, clean_line)
+ elif is_category(clean_line) or is_information(clean_line):
+ current_release["raw"] = current_release.get("raw", "") + line
+
+ return changes
+
+
def release(changelog_path: str) -> str:
changelog = to_dict(changelog_path, show_unreleased=True)
current_version, new_version = guess_unreleased_version(changelog)
diff --git a/tests/test_changelog.py b/tests/test_changelog.py
index 798fbaa..eaa991e 100644
--- a/tests/test_changelog.py
+++ b/tests/test_changelog.py
@@ -119,3 +119,63 @@ def test_changelog_with_versions_and_all_categories(changelog):
"version": "1.0.0",
},
}
+
+
+def test_raw_changelog_with_versions_and_all_categories(changelog):
+ assert keepachangelog.to_raw_dict(changelog) == {
+ "1.2.0": {
+ "raw": """### Changed
+- Release note 1.
+- Release note 2.
+### Added
+- Enhancement 1
+- sub enhancement 1
+- sub enhancement 2
+- Enhancement 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
+""",
+ "release_date": "2018-06-01",
+ "version": "1.2.0",
+ },
+ "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",
+ },
+ "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",
+ },
+ "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",
+ },
+ }
From 1ec14b3345deebba7c464d3720cd91f4acd90ffc Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 19 Apr 2021 18:27:23 +0200
Subject: [PATCH 08/11] Handle various changelog variation
---
CHANGELOG.md | 7 +
keepachangelog/_changelog.py | 63 ++++----
keepachangelog/_versioning.py | 4 +-
tests/test_changelog_no_version.py | 4 +
tests/test_changelog_unreleased.py | 4 +-
tests/test_non_standard_changelog.py | 222 +++++++++++++++++++++++++++
6 files changed, 274 insertions(+), 30 deletions(-)
create mode 100644 tests/test_non_standard_changelog.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5aa2c7..99bab4c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `keepachangelog.release` function to guess new version number based on `Unreleased` section, update changelog and return new version number.
- `keepachangelog.to_raw_dict` function returning a raw markdown description of the release under `raw` dict.
+### Fixed
+- Handle any category name.
+- Add more flexibility for release format.
+
+### Changed
+- `Unreleased` is now reported as lower cased `unreleased`.
+
## [0.4.0] - 2020-09-21
### Added
- `keepachangelog.flask_restx.add_changelog_endpoint` function to add a changelog endpoint to a [Flask-RestX](https://flask-restx.readthedocs.io/en/latest/) application.
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index e877ff5..bbe816e 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -4,41 +4,50 @@
from keepachangelog._versioning import guess_unreleased_version
-# Release pattern should match lines like: "## [0.0.1] - 2020-12-31" or ## [Unreleased]
-release_pattern = re.compile(r"^## \[(.*)\](?: - (.*))?$")
+def is_release(line: str) -> bool:
+ return line.startswith("## ")
-def is_release(line: str, show_unreleased: bool) -> bool:
- match = release_pattern.fullmatch(line)
- if match and (not show_unreleased and match.group(1) == "Unreleased"):
- return False
- return match is not None
-
-def add_release(changes: Dict[str, dict], line: str) -> dict:
- release_info = release_pattern.fullmatch(line)
+def add_release(changes: Dict[str, dict], line: str, show_unreleased: bool) -> dict:
+ release_line = line[3:].lower().strip(" ")
+ # A release is separated by a space between version and release date
+ # Release pattern should match lines like: "[0.0.1] - 2020-12-31" or [Unreleased]
+ version, release_date = (
+ release_line.split(" ", maxsplit=1)
+ if " " in release_line
+ else (release_line, None)
+ )
+ if not show_unreleased and not release_date:
+ return {}
+ version = unlink(version)
return changes.setdefault(
- release_info.group(1),
- {"version": release_info.group(1), "release_date": release_info.group(2)},
+ version,
+ {"version": version, "release_date": extract_date(release_date)},
)
-categories = {
- "### Added": "added",
- "### Changed": "changed",
- "### Deprecated": "deprecated",
- "### Removed": "removed",
- "### Fixed": "fixed",
- "### Security": "security",
-}
+def unlink(value: str) -> str:
+ if not value:
+ return value
+
+ return value.lstrip("[").rstrip("]")
+
+
+def extract_date(date: str) -> str:
+ if not date:
+ return date
+
+ return date.lstrip(" -(").rstrip(" )")
def is_category(line: str) -> bool:
- return line in categories
+ return line.startswith("### ")
def add_category(release: dict, line: str) -> List[str]:
- return release.setdefault(categories[line], [])
+ category = line[4:].lower().strip(" ")
+ return release.setdefault(category, [])
# Link pattern should match lines like: "[1.2.3]: https://github.com/user/project/releases/tag/v0.0.1"
@@ -61,8 +70,8 @@ def to_dict(changelog_path: str, *, show_unreleased: bool = False) -> Dict[str,
for line in change_log:
line = line.strip(" \n")
- if is_release(line, show_unreleased):
- current_release = add_release(changes, line)
+ if is_release(line):
+ current_release = add_release(changes, line, show_unreleased)
elif is_category(line):
category = add_category(current_release, line)
elif is_information(line):
@@ -78,8 +87,10 @@ def to_raw_dict(changelog_path: str) -> Dict[str, dict]:
for line in change_log:
clean_line = line.strip(" \n")
- if is_release(clean_line, show_unreleased=False):
- current_release = add_release(changes, clean_line)
+ if is_release(clean_line):
+ current_release = add_release(
+ changes, clean_line, show_unreleased=False
+ )
elif is_category(clean_line) or is_information(clean_line):
current_release["raw"] = current_release.get("raw", "") + line
diff --git a/keepachangelog/_versioning.py b/keepachangelog/_versioning.py
index 7645a14..be70fea 100644
--- a/keepachangelog/_versioning.py
+++ b/keepachangelog/_versioning.py
@@ -37,13 +37,13 @@ def bump(unreleased: dict, version: str) -> str:
def actual_version(changelog: dict) -> Optional[str]:
versions = sorted(changelog.keys())
current_version = versions.pop() if versions else None
- while "Unreleased" == current_version:
+ while "unreleased" == current_version:
current_version = versions.pop() if versions else None
return current_version
def guess_unreleased_version(changelog: dict) -> Tuple[Optional[str], str]:
- unreleased = changelog.get("Unreleased", {})
+ unreleased = changelog.get("unreleased", {})
if not unreleased or len(unreleased) < 3:
raise Exception(
"Release content must be provided within changelog Unreleased section."
diff --git a/tests/test_changelog_no_version.py b/tests/test_changelog_no_version.py
index 9c41a47..b88cbc2 100644
--- a/tests/test_changelog_no_version.py
+++ b/tests/test_changelog_no_version.py
@@ -17,3 +17,7 @@ def changelog(tmpdir):
def test_changelog_without_versions(changelog):
assert keepachangelog.to_dict(changelog) == {}
+
+
+def test_raw_changelog_without_versions(changelog):
+ assert keepachangelog.to_raw_dict(changelog) == {}
diff --git a/tests/test_changelog_unreleased.py b/tests/test_changelog_unreleased.py
index 76950a9..1495ebe 100644
--- a/tests/test_changelog_unreleased.py
+++ b/tests/test_changelog_unreleased.py
@@ -76,8 +76,8 @@ def changelog(tmpdir):
def test_changelog_with_versions_and_all_categories(changelog):
assert keepachangelog.to_dict(changelog, show_unreleased=True) == {
- "Unreleased": {
- "version": "Unreleased",
+ "unreleased": {
+ "version": "unreleased",
"release_date": None,
"changed": ["Release note 1.", "Release note 2."],
"added": [
diff --git a/tests/test_non_standard_changelog.py b/tests/test_non_standard_changelog.py
new file mode 100644
index 0000000..d5c9b90
--- /dev/null
+++ b/tests/test_non_standard_changelog.py
@@ -0,0 +1,222 @@
+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).
+
+## Master
+
+## 1.2.0 (August 28, 2019)
+### Changed
+- Release note 1.
+- Release note 2.
+
+### Added
+- Enhancement 1
+- sub enhancement 1
+- sub enhancement 2
+- Enhancement 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
+
+## [1.1.0] (May 03, 2018)
+### Changed
+- Enhancement 1 (1.1.0)
+- sub enhancement 1
+- sub enhancement 2
+- Enhancement 2 (1.1.0)
+
+## [1.0.1] - May 01, 2018
+### Fixed
+- Bug fix 1 (1.0.1)
+- sub bug 1
+- sub bug 2
+- Bug fix 2 (1.0.1)
+
+## 1.0.0 (2017-01-01)
+### Deprecated
+- Known issue 1 (1.0.0)
+- Known issue 2 (1.0.0)
+"""
+ )
+ return changelog_file_path
+
+
+def test_changelog_with_versions_and_all_categories(changelog):
+ assert keepachangelog.to_dict(changelog) == {
+ "1.2.0": {
+ "added": [
+ "Enhancement 1",
+ "sub enhancement 1",
+ "sub enhancement 2",
+ "Enhancement 2",
+ ],
+ "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": "august 28, 2019",
+ "removed": ["Deprecated feature 2", "Future removal 1"],
+ "security": ["Known issue 1", "Known issue 2"],
+ "version": "1.2.0",
+ },
+ "1.1.0": {
+ "changed": [
+ "Enhancement 1 (1.1.0)",
+ "sub enhancement 1",
+ "sub enhancement 2",
+ "Enhancement 2 (1.1.0)",
+ ],
+ "release_date": "may 03, 2018",
+ "version": "1.1.0",
+ },
+ "1.0.1": {
+ "fixed": [
+ "Bug fix 1 (1.0.1)",
+ "sub bug 1",
+ "sub bug 2",
+ "Bug fix 2 (1.0.1)",
+ ],
+ "release_date": "may 01, 2018",
+ "version": "1.0.1",
+ },
+ "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",
+ },
+ }
+
+
+def test_changelog_with_unreleased_versions_and_all_categories(changelog):
+ assert keepachangelog.to_dict(changelog, show_unreleased=True) == {
+ "master": {"release_date": None, "version": "master"},
+ "1.2.0": {
+ "added": [
+ "Enhancement 1",
+ "sub enhancement 1",
+ "sub enhancement 2",
+ "Enhancement 2",
+ ],
+ "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": "august 28, 2019",
+ "removed": ["Deprecated feature 2", "Future removal 1"],
+ "security": ["Known issue 1", "Known issue 2"],
+ "version": "1.2.0",
+ },
+ "1.1.0": {
+ "changed": [
+ "Enhancement 1 (1.1.0)",
+ "sub enhancement 1",
+ "sub enhancement 2",
+ "Enhancement 2 (1.1.0)",
+ ],
+ "release_date": "may 03, 2018",
+ "version": "1.1.0",
+ },
+ "1.0.1": {
+ "fixed": [
+ "Bug fix 1 (1.0.1)",
+ "sub bug 1",
+ "sub bug 2",
+ "Bug fix 2 (1.0.1)",
+ ],
+ "release_date": "may 01, 2018",
+ "version": "1.0.1",
+ },
+ "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",
+ },
+ }
+
+
+def test_raw_changelog_with_versions_and_all_categories(changelog):
+ assert keepachangelog.to_raw_dict(changelog) == {
+ "1.2.0": {
+ "raw": """### Changed
+- Release note 1.
+- Release note 2.
+### Added
+- Enhancement 1
+- sub enhancement 1
+- sub enhancement 2
+- Enhancement 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
+""",
+ "release_date": "august 28, 2019",
+ "version": "1.2.0",
+ },
+ "1.1.0": {
+ "raw": """### Changed
+- Enhancement 1 (1.1.0)
+- sub enhancement 1
+- sub enhancement 2
+- Enhancement 2 (1.1.0)
+""",
+ "release_date": "may 03, 2018",
+ "version": "1.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": "may 01, 2018",
+ "version": "1.0.1",
+ },
+ "1.0.0": {
+ "raw": """### Deprecated
+- Known issue 1 (1.0.0)
+- Known issue 2 (1.0.0)
+""",
+ "release_date": "2017-01-01",
+ "version": "1.0.0",
+ },
+ }
From 3486ae9c53ca886ee09f00f726f06b8149f83869 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 19 Apr 2021 18:39:03 +0200
Subject: [PATCH 09/11] Version must always be provided
---
keepachangelog/_changelog.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/keepachangelog/_changelog.py b/keepachangelog/_changelog.py
index bbe816e..aa699f7 100644
--- a/keepachangelog/_changelog.py
+++ b/keepachangelog/_changelog.py
@@ -28,9 +28,6 @@ def add_release(changes: Dict[str, dict], line: str, show_unreleased: bool) -> d
def unlink(value: str) -> str:
- if not value:
- return value
-
return value.lstrip("[").rstrip("]")
From f37bd3883d9ed46cc8043922d187f08e93435256 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 19 Apr 2021 18:55:52 +0200
Subject: [PATCH 10/11] Keep number of test case up to date
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index b0432c3..000265f 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
From 74dd9f7c5e5d71d243967826c242888151798dc5 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 19 Apr 2021 19:00:26 +0200
Subject: [PATCH 11/11] Release version 0.5.0 today
---
CHANGELOG.md | 5 ++++-
keepachangelog/version.py | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99bab4c..56bfe58 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]
+
+## [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.
- `keepachangelog.to_raw_dict` function returning a raw markdown description of the release under `raw` dict.
@@ -40,7 +42,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.4.0...HEAD
+[Unreleased]: https://github.com/Colin-b/keepachangelog/compare/v0.5.0...HEAD
+[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
[0.3.0]: https://github.com/Colin-b/keepachangelog/compare/v0.2.0...v0.3.0
diff --git a/keepachangelog/version.py b/keepachangelog/version.py
index 151e68f..c4b9ff7 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.4.0"
+__version__ = "0.5.0"